package com.qsjnic.demo.radio;

import android.app.Activity;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.GradientDrawable;
import android.media.AudioAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;

import com.qsjnic.mcu.contract.IMcuFrameListener;
import com.qsjnic.mcu.contract.RadioState;
import com.qsjnic.mcu.sdk.McuClient;
import com.qsjnic.mcu.sdk.McuRadioClient;
import com.qsjnic.mcu.sdk.RadioListener;

/**
 * Radio Demo with FM Tuner audio passthrough and MCU frame monitor.
 */
public class MainActivity extends Activity {

    private static final String TAG = "RadioDemo";
    private static final int PERMISSION_REQ = 100;

    private final Handler ui = new Handler(Looper.getMainLooper());

    // Data panel
    private TextView tvBand, tvFreq, tvFreqMhz, tvSignal, tvStereo, tvSeek, tvAudioStatus;

    // Frame Monitor fields
    private TextView tvValid, tvPty, tvTa, tvTp, tvPs, tvRt, tvWorkStatus, tvTaPtyTips;
    private TextView tvFreqHex, tvPsPtyHex, tvRtHex;
    private String lastPs = "";
    private String lastRt = "";

    // Control buttons
    private TextView tvStatus;
    private Button btnFm, btnAm, btn875, btn900, btnSeekFw, btnSeekStop, btnPlay875;
    private Button btnAudioStart, btnAudioStop;

    private final RadioListener listener = new RadioListener() {
        @Override public void onFreqChanged(RadioState s) {
            ui.post(() -> updateRadio(s));
        }
        @Override public void onPsPty(int pi, int pty, String ps) {
            Log.i(TAG, "PS/PTY pi=" + pi + " pty=" + pty + " ps=" + ps);
            ui.post(() -> {
                lastPs = (ps != null) ? ps : "";
                tvPs.setText("PS: " + (lastPs.isEmpty() ? "(empty)" : lastPs));
                tvPs.setTextColor(lastPs.isEmpty() ? Color.parseColor("#F44336") : Color.parseColor("#4CAF50"));
                tvPty.setText("PTY: " + pty);
            });
        }
        @Override public void onRadioText(String rt) {
            Log.i(TAG, "RT=" + rt);
            ui.post(() -> {
                lastRt = (rt != null) ? rt : "";
                String display = lastRt.isEmpty() ? "(empty)" : (lastRt.length() > 80 ? lastRt.substring(0, 80) + "..." : lastRt);
                tvRt.setText("RT: " + display);
                tvRt.setTextColor(lastRt.isEmpty() ? Color.parseColor("#F44336") : Color.parseColor("#4CAF50"));
            });
        }
        @Override public void onWorkStatus(int status) {
            Log.i(TAG, "WorkStatus=" + status);
            ui.post(() -> {
                tvWorkStatus.setText("Work: " + status + (status == 0 ? " (OK)" : " (ERR)"));
                tvWorkStatus.setTextColor(status == 0 ? Color.parseColor("#4CAF50") : Color.parseColor("#F44336"));
            });
        }
        @Override public void onTaPtyTips(int mode, int value) {
            Log.i(TAG, "TA/PTY Tips mode=" + mode + " value=" + value);
            ui.post(() -> {
                String modeName = mode == 0 ? "TA" : (mode == 1 ? "PTY" : "?");
                String valName = value == 0 ? "Clear" : (value == 1 ? "NoResult" : (value == 2 ? "Searching" : "?"));
                tvTaPtyTips.setText("Tips: " + modeName + " / " + valName);
            });
        }
    };

    // ----- raw frame listener for hex dump -----
    private final IMcuFrameListener.Stub rawFrameListener = new IMcuFrameListener.Stub() {
        @Override
        public void onMcuFrame(int frameType, int msgType, int cmd, byte[] body) {
            if (msgType != 0x05) return; // RADIO only
            String hex = bytesToHex(body);
            ui.post(() -> {
                switch (cmd) {
                    case 0x01: // FREQ
                        tvFreqHex.setText("FREQ (" + body.length + "B): " + hex);
                        break;
                    case 0x02: // PS_PTY
                        tvPsPtyHex.setText("PS/PTY (" + body.length + "B): " + hex);
                        break;
                    case 0x03: // RT
                        String rtHex = body.length > 32
                                ? bytesToHex(body, 32) + " ..."
                                : hex;
                        tvRtHex.setText("RT (" + body.length + "B): " + rtHex);
                        break;
                }
            });
        }
        @Override public void onVehicleStateChanged(com.qsjnic.mcu.contract.VehicleSnapshot s) {}
        @Override public void onRadioStateChanged(RadioState s) {}
        @Override public void onUpgradeProgress(com.qsjnic.mcu.contract.UpgradeProgress p) {}
    };

    // ----- Audio passthrough -----
    private AudioRecord audioRecord;
    private AudioTrack audioTrack;
    private Thread audioThread;
    private volatile boolean audioRunning = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ScrollView scroll = new ScrollView(this);
        LinearLayout root = new LinearLayout(this);
        root.setOrientation(LinearLayout.VERTICAL);
        root.setPadding(32, 32, 32, 32);
        root.setBackgroundColor(Color.parseColor("#F5F5F5"));

        // ===== Title =====
        TextView title = new TextView(this);
        title.setText("Radio Tuner + Audio");
        title.setTextSize(24f);
        title.setTypeface(null, Typeface.BOLD);
        title.setTextColor(Color.parseColor("#333333"));
        title.setPadding(0, 0, 0, 16);
        root.addView(title);

        tvStatus = new TextView(this);
        tvStatus.setText("Service: connecting");
        tvStatus.setTextSize(14f);
        tvStatus.setTextColor(Color.parseColor("#555555"));
        tvStatus.setPadding(0, 0, 0, 16);
        root.addView(tvStatus);

        // ===== Frequency Display Card =====
        LinearLayout freqCard = card(root);
        tvBand = new TextView(this);
        tvBand.setText("--");
        tvBand.setTextSize(16f);
        tvBand.setTextColor(Color.parseColor("#888888"));
        tvBand.setGravity(Gravity.CENTER);
        freqCard.addView(tvBand);

        tvFreq = new TextView(this);
        tvFreq.setText("----");
        tvFreq.setTextSize(56f);
        tvFreq.setTypeface(null, Typeface.BOLD);
        tvFreq.setTextColor(Color.parseColor("#1976D2"));
        tvFreq.setGravity(Gravity.CENTER);
        freqCard.addView(tvFreq);

        tvFreqMhz = new TextView(this);
        tvFreqMhz.setText("MHz");
        tvFreqMhz.setTextSize(14f);
        tvFreqMhz.setTextColor(Color.parseColor("#888888"));
        tvFreqMhz.setGravity(Gravity.CENTER);
        freqCard.addView(tvFreqMhz);

        LinearLayout detailRow = new LinearLayout(this);
        detailRow.setOrientation(LinearLayout.HORIZONTAL);
        detailRow.setGravity(Gravity.CENTER);
        detailRow.setPadding(0, 16, 0, 0);
        freqCard.addView(detailRow);

        tvSignal = chip(detailRow, "Signal: --");
        tvStereo = chip(detailRow, "Stereo: --");
        tvSeek   = chip(detailRow, "Seek: --");

        // ===== Frame Monitor Card =====
        root.addView(sectionTitle("Frame Monitor"));
        LinearLayout monCard = card(root);

        // Row 1: valid + pty + TA + TP flags
        LinearLayout monRow1 = new LinearLayout(this);
        monRow1.setOrientation(LinearLayout.HORIZONTAL);
        monRow1.setGravity(Gravity.CENTER_VERTICAL);
        monRow1.setPadding(0, 0, 0, 8);
        monCard.addView(monRow1);

        tvValid = chip(monRow1, "valid: --");
        tvPty   = chip(monRow1, "PTY: --");
        tvTa    = chip(monRow1, "TA: --");
        tvTp    = chip(monRow1, "TP: --");

        // Row 2: work status + TA/PTY tips
        LinearLayout monRow2 = new LinearLayout(this);
        monRow2.setOrientation(LinearLayout.HORIZONTAL);
        monRow2.setGravity(Gravity.CENTER_VERTICAL);
        monRow2.setPadding(0, 0, 0, 4);
        monCard.addView(monRow2);

        tvWorkStatus = monText(monRow2, "Work: --");
        tvTaPtyTips  = monText(monRow2, "Tips: --");

        // Row 3: PS station name
        tvPs = monText(monCard, "PS: --");
        tvPs.setPadding(0, 4, 0, 2);

        // Row 4: RT radio text
        tvRt = monText(monCard, "RT: --");
        tvRt.setPadding(0, 2, 0, 8);

        // Divider
        View divider = new View(this);
        divider.setBackgroundColor(Color.parseColor("#E0E0E0"));
        LinearLayout.LayoutParams dlp = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT, 1);
        dlp.setMargins(0, 0, 0, 8);
        divider.setLayoutParams(dlp);
        monCard.addView(divider);

        // Hex dump section title
        TextView hexLabel = new TextView(this);
        hexLabel.setText("Raw Frame Hex");
        hexLabel.setTextSize(12f);
        hexLabel.setTypeface(null, Typeface.BOLD);
        hexLabel.setTextColor(Color.parseColor("#888888"));
        hexLabel.setPadding(0, 0, 0, 4);
        monCard.addView(hexLabel);

        // Hex rows (monospace, small)
        tvFreqHex = hexText(monCard);
        tvPsPtyHex = hexText(monCard);
        tvRtHex = hexText(monCard);

        // ===== Audio Status =====
        root.addView(sectionTitle("Tuner Audio"));
        LinearLayout audioCard = card(root);
        tvAudioStatus = new TextView(this);
        tvAudioStatus.setText("Audio: stopped");
        tvAudioStatus.setTextSize(14f);
        tvAudioStatus.setTextColor(Color.parseColor("#555555"));
        tvAudioStatus.setPadding(0, 8, 0, 8);
        audioCard.addView(tvAudioStatus);

        LinearLayout audioRow = buttonRow(audioCard);
        btnAudioStart = btn(audioRow, "Start Audio", v -> startTunerAudio());
        btnAudioStop  = btn(audioRow, "Stop Audio",  v -> stopTunerAudio());

        // ===== Band Selection =====
        root.addView(sectionTitle("Band"));
        LinearLayout bandCard = card(root);
        LinearLayout bandRow = buttonRow(bandCard);
        btnFm = btn(bandRow, "FM", v -> McuClient.getInstance().radio().setBand(McuRadioClient.BAND_FM));
        btnAm = btn(bandRow, "AM", v -> McuClient.getInstance().radio().setBand(McuRadioClient.BAND_AM));

        // ===== Frequency Presets =====
        root.addView(sectionTitle("Frequency"));
        LinearLayout freqCtrlCard = card(root);
        LinearLayout freqRow1 = buttonRow(freqCtrlCard);
        btn875 = btn(freqRow1, "87.5 MHz", v -> McuClient.getInstance().radio().setFrequency(8750));
        btn900 = btn(freqRow1, "90.0 MHz", v -> McuClient.getInstance().radio().setFrequency(9000));

        // ===== Seek Control =====
        root.addView(sectionTitle("Seek"));
        LinearLayout seekCard = card(root);
        LinearLayout seekRow = buttonRow(seekCard);
        btnSeekFw   = btn(seekRow, "Forward",  v -> McuClient.getInstance().radio().seekForward());
        btnSeekStop = btn(seekRow, "Stop",     v -> McuClient.getInstance().radio().seekStop());

        // ===== Other =====
        root.addView(sectionTitle("Other"));
        LinearLayout otherCard = card(root);
        LinearLayout otherRow = buttonRow(otherCard);
        btnPlay875 = btn(otherRow, "Play 90.0", v -> playFm900());
        btn(otherRow, "Ant ON",  v -> McuClient.getInstance().radio().setAntPower(true));
        btn(otherRow, "Init Range", v -> McuClient.getInstance().radio().setFreqRange(0, 522, 1670, 8750, 10800, 9, 1));

        scroll.addView(root);
        setContentView(scroll);

        McuClient.getInstance().init(getApplicationContext());
        McuClient.getInstance().addReadyListener(() -> ui.post(() -> {
            tvStatus.setText("Service: connected, linkAlive=" + McuClient.getInstance().isLinkAlive());
            playFm900();
            ui.postDelayed(this::startTunerAudio, 1000);
            McuClient.getInstance().addFrameListener(rawFrameListener);
        }));
        McuClient.getInstance().radio().addListener(listener);

        // Request audio permission
        if (checkSelfPermission(android.Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{android.Manifest.permission.RECORD_AUDIO}, PERMISSION_REQ);
        }
    }

    // ---------- Audio Passthrough ----------

    private void startTunerAudio() {
        if (audioRunning) return;

        AudioManager am = (AudioManager) getSystemService(AUDIO_SERVICE);
        AudioDeviceInfo tunerDevice = null;
        for (AudioDeviceInfo dev : am.getDevices(AudioManager.GET_DEVICES_INPUTS)) {
            Log.i(TAG, "Input device: type=" + dev.getType() + " name=" + dev.getProductName());
            if (dev.getType() == AudioDeviceInfo.TYPE_FM_TUNER) {
                tunerDevice = dev;
                break;
            }
        }

        if (tunerDevice == null) {
            tvAudioStatus.setText("Audio: FM Tuner device NOT FOUND");
            Log.e(TAG, "TYPE_FM_TUNER not found in input devices");
            return;
        }

        tvAudioStatus.setText("Audio: starting...");
        Log.i(TAG, "Found FM Tuner: " + tunerDevice.getProductName());

        final int sampleRate = 48000;
        final int channelConfig = AudioFormat.CHANNEL_IN_STEREO;
        final int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
        final int minBufSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
        final int bufferSize = Math.max(minBufSize, 16384);
        final int AUDIO_SOURCE_FM_TUNER = 1998;

        try {
            audioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT,
                    sampleRate, channelConfig, audioFormat, bufferSize);

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && audioRecord.setPreferredDevice(tunerDevice)) {
                Log.i(TAG, "setPreferredDevice(tuner) succeeded");
            }

            audioTrack = new AudioTrack.Builder()
                    .setAudioAttributes(new AudioAttributes.Builder()
                            .setUsage(AudioAttributes.USAGE_MEDIA)
                            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                            .build())
                    .setAudioFormat(new AudioFormat.Builder()
                            .setSampleRate(sampleRate)
                            .setEncoding(audioFormat)
                            .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
                            .build())
                    .setBufferSizeInBytes(bufferSize)
                    .setTransferMode(AudioTrack.MODE_STREAM)
                    .build();

            audioRecord.startRecording();
            audioTrack.play();
            audioRunning = true;

            final int playbackBufSize = bufferSize;
            audioThread = new Thread(() -> {
                android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
                byte[] buf = new byte[playbackBufSize];
                int totalRead = 0;
                long lastLog = System.currentTimeMillis();
                while (audioRunning) {
                    int read = audioRecord.read(buf, 0, buf.length);
                    if (read > 0) {
                        totalRead += read;
                        audioTrack.write(buf, 0, read);
                        long now = System.currentTimeMillis();
                        if (now - lastLog > 3000) {
                            short sample = (short) ((buf[0] & 0xFF) | ((buf[1] & 0xFF) << 8));
                            Log.i(TAG, "Audio passthrough: read=" + read
                                    + " total=" + totalRead
                                    + " sample=" + sample
                                    + " trackState=" + audioTrack.getPlayState());
                            lastLog = now;
                        }
                    } else if (read < 0) {
                        Log.e(TAG, "AudioRecord read error: " + read);
                        break;
                    }
                }
            }, "TunerAudio");
            audioThread.setPriority(Thread.MAX_PRIORITY);
            audioThread.start();

            tvAudioStatus.setText("Audio: PLAYING (" + tunerDevice.getProductName() + ")");
            tvAudioStatus.setTextColor(Color.parseColor("#4CAF50"));

        } catch (Exception e) {
            Log.e(TAG, "startTunerAudio failed", e);
            tvAudioStatus.setText("Audio: FAILED - " + e.getMessage());
            tvAudioStatus.setTextColor(Color.parseColor("#F44336"));
            stopTunerAudio();
        }
    }

    private void stopTunerAudio() {
        audioRunning = false;
        if (audioThread != null) {
            try { audioThread.join(500); } catch (InterruptedException ignored) {}
            audioThread = null;
        }
        if (audioRecord != null) {
            try { audioRecord.stop(); } catch (Exception ignored) {}
            try { audioRecord.release(); } catch (Exception ignored) {}
            audioRecord = null;
        }
        if (audioTrack != null) {
            try { audioTrack.stop(); } catch (Exception ignored) {}
            try { audioTrack.release(); } catch (Exception ignored) {}
            audioTrack = null;
        }
        tvAudioStatus.setText("Audio: stopped");
        tvAudioStatus.setTextColor(Color.parseColor("#555555"));
        Log.i(TAG, "Tuner audio stopped");
    }

    // ---------- UI helpers ----------

    private TextView sectionTitle(String text) {
        TextView t = new TextView(this);
        t.setText(text);
        t.setTextSize(14f);
        t.setTypeface(null, Typeface.BOLD);
        t.setTextColor(Color.parseColor("#666666"));
        t.setPadding(8, 24, 0, 8);
        return t;
    }

    private LinearLayout card(LinearLayout parent) {
        LinearLayout card = new LinearLayout(this);
        card.setOrientation(LinearLayout.VERTICAL);
        card.setPadding(24, 24, 24, 24);
        card.setBackgroundColor(Color.WHITE);
        GradientDrawable bg = new GradientDrawable();
        bg.setColor(Color.WHITE);
        bg.setCornerRadius(16f);
        card.setBackground(bg);
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        lp.setMargins(0, 0, 0, 16);
        card.setLayoutParams(lp);
        parent.addView(card);
        return card;
    }

    private TextView chip(LinearLayout parent, String text) {
        TextView t = new TextView(this);
        t.setText(text);
        t.setTextSize(12f);
        t.setTextColor(Color.parseColor("#555555"));
        t.setPadding(16, 8, 16, 8);
        GradientDrawable bg = new GradientDrawable();
        bg.setColor(Color.parseColor("#E8E8E8"));
        bg.setCornerRadius(20f);
        t.setBackground(bg);
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        lp.setMargins(8, 0, 8, 0);
        t.setLayoutParams(lp);
        parent.addView(t);
        return t;
    }

    private TextView monText(LinearLayout parent, String text) {
        TextView t = new TextView(this);
        t.setText(text);
        t.setTextSize(13f);
        t.setTextColor(Color.parseColor("#555555"));
        t.setPadding(8, 2, 8, 2);
        parent.addView(t);
        return t;
    }

    private TextView hexText(LinearLayout parent) {
        TextView t = new TextView(this);
        t.setText("--");
        t.setTextSize(10f);
        t.setTypeface(Typeface.MONOSPACE);
        t.setTextColor(Color.parseColor("#888888"));
        t.setPadding(0, 2, 0, 2);
        parent.addView(t);
        return t;
    }

    private LinearLayout buttonRow(LinearLayout parent) {
        LinearLayout row = new LinearLayout(this);
        row.setOrientation(LinearLayout.HORIZONTAL);
        row.setPadding(0, 8, 0, 8);
        parent.addView(row);
        return row;
    }

    private Button btn(LinearLayout parent, String label, View.OnClickListener l) {
        Button b = new Button(this);
        b.setText(label);
        b.setOnClickListener(l);
        b.setAllCaps(false);
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
        lp.setMargins(8, 0, 8, 0);
        b.setLayoutParams(lp);
        parent.addView(b);
        return b;
    }

    private static String bytesToHex(byte[] bytes) {
        if (bytes == null || bytes.length == 0) return "(empty)";
        StringBuilder sb = new StringBuilder(bytes.length * 3);
        for (byte b : bytes) {
            sb.append(String.format("%02X ", b & 0xFF));
        }
        sb.setLength(sb.length() - 1);
        return sb.toString();
    }

    private static String bytesToHex(byte[] bytes, int limit) {
        if (bytes == null || bytes.length == 0) return "(empty)";
        int n = Math.min(bytes.length, limit);
        StringBuilder sb = new StringBuilder(n * 3);
        for (int i = 0; i < n; i++) {
            sb.append(String.format("%02X ", bytes[i] & 0xFF));
        }
        sb.setLength(sb.length() - 1);
        if (bytes.length > limit) sb.append(" ...");
        return sb.toString();
    }

    private void playFm900() {
        McuRadioClient radio = McuClient.getInstance().radio();
        int rcBand = radio.setBand(McuRadioClient.BAND_FM);
        int rcRange = radio.setFreqRange(0, 522, 1670, 8750, 10800, 9, 1);
        int rcAnt = radio.setAntPower(true);
        int rcStereo = radio.setSwitch(McuRadioClient.SWITCH_STEREO, 1);
        int rcRds = radio.setSwitch(McuRadioClient.SWITCH_RDS, 1);
        int rcMute = radio.setRegMute(false);
        int rcFreq = radio.setFrequency(9344);
        int rcStop = radio.seekStop();
        String msg = "Play 93.4 rc band=" + rcBand
                + " range=" + rcRange
                + " ant=" + rcAnt
                + " stereo=" + rcStereo
                + " rds=" + rcRds
                + " freq=" + rcFreq
                + " stop=" + rcStop
                + " muteOff=" + rcMute;
        Log.i(TAG, msg);
        tvStatus.setText(msg);
    }

    private int lastSignal = 0;
    private int strongFreq = 0;

    private void updateRadio(RadioState s) {
        String bandName = s.band == 0 ? "FM" : (s.band == 1 ? "AM" : "?");
        tvBand.setText(bandName + " Radio");

        double mhz = s.band == 0 ? s.freq / 100.0 : s.freq / 1000.0;
        tvFreq.setText(String.format("%.1f", mhz));
        tvFreqMhz.setText(s.band == 0 ? "MHz" : "KHz");

        tvSignal.setText("Signal: " + (s.signal >= 0 ? s.signal : "--"));
        tvStereo.setText(s.flagStereo == 1 ? "Stereo" : "Mono");
        tvStereo.setTextColor(s.flagStereo == 1 ? Color.parseColor("#4CAF50") : Color.parseColor("#999999"));

        String[] seekNames = {"Stop", "AMS", "Fwd", "Bwd", "PTY", "TA"};
        tvSeek.setText("Seek: " + (s.seekMode >= 0 && s.seekMode < seekNames.length ? seekNames[s.seekMode] : "--"));

        // ---- Frame Monitor updates ----
        if (s.valid == 1) {
            tvValid.setText("valid: LOCK");
            tvValid.setTextColor(Color.parseColor("#4CAF50"));
        } else if (s.valid == 0) {
            tvValid.setText("valid: 0");
            tvValid.setTextColor(Color.parseColor("#F44336"));
        } else {
            tvValid.setText("valid: --");
            tvValid.setTextColor(Color.parseColor("#999999"));
        }

        tvPty.setText("PTY: " + (s.pty >= 0 ? s.pty : "--"));

        tvTa.setText("TA: " + (s.flagTa == 1 ? "ON" : (s.flagTa == 0 ? "OFF" : "--")));
        tvTa.setTextColor(s.flagTa == 1 ? Color.parseColor("#4CAF50") : Color.parseColor("#999999"));
        tvTp.setText("TP: " + (s.flagTp == 1 ? "ON" : (s.flagTp == 0 ? "OFF" : "--")));
        tvTp.setTextColor(s.flagTp == 1 ? Color.parseColor("#4CAF50") : Color.parseColor("#999999"));

        if (s.signal > lastSignal) {
            lastSignal = s.signal;
            strongFreq = s.freq;
            Log.i(TAG, "*** STRONG SIGNAL: " + s.freq + " signal=" + s.signal + " ***");
        }
    }

    @Override
    protected void onDestroy() {
        stopTunerAudio();
        McuClient.getInstance().radio().removeListener(listener);
        McuClient.getInstance().removeFrameListener(rawFrameListener);
        super.onDestroy();
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (!McuClient.getInstance().isReady()) {
            android.widget.Toast.makeText(this, "Service 未连接，5s后重试", android.widget.Toast.LENGTH_SHORT).show();
            new Handler(Looper.getMainLooper()).postDelayed(this::onResume, 5000);
        }
    }
}

- * Radio Demo with FM Tuner audio passthrough and comprehensive MCU frame monitor.

- * Covers: Radio (FREQ/PS_PTY/RT), SWC Key events, Vehicle Speed, raw frame hex.

+ * Radio Demo with FM Tuner audio passthrough and MCU frame monitor.

-    private TextView tvFreqHex, tvPsPtyHex, tvRtHex, tvKeyHex, tvSpeedHex;

+    private TextView tvFreqHex, tvPsPtyHex, tvRtHex;

-    private TextView tvLastKey, tvSpeed;

-    // ----- Pajero SWC key name mapping (MCU → Android) -----

-    private static String swcName(int keyValue) {

-            case 0x03: return "SPEECH";

-            case 0x18: return "ANSWER";

-            case 0x19: return "REJECT";

-            case 0x25: return "CAMERA";

-            default:   return String.format("0x%02X", keyValue);

-    private static String swcStatus(int status) {

-            case 0x01: return "TRIGGER";

-            default:   return String.format("0x%02X", status);

-    private final RadioListener radioListener = new RadioListener() {

+    private final RadioListener listener = new RadioListener() {

-    // ----- Key Listener (M2A — MCU → Android SWC events) -----

-    private final KeyListener keyListener = new KeyListener() {

-        @Override public void onKeyEvent(int keyValue, int keyStatus) {

-            Log.i(TAG, "SWC keyValue=0x" + Integer.toHexString(keyValue) + " status=" + keyStatus);

-                String name = swcName(keyValue);

-                String state = swcStatus(keyStatus);

-                tvLastKey.setText("Key: " + name + " " + state);

-                tvLastKey.setTextColor(keyStatus == 0x01 ? Color.parseColor("#FF5722") : Color.parseColor("#4CAF50"));

-    // ----- Vehicle Listener (speed) -----

-    private final VehicleListener vehicleListener = new VehicleListener() {

-        @Override public void onCarSpeed(int speedKmh) {

-            Log.i(TAG, "Speed: " + speedKmh + " km/h");

-                tvSpeed.setText("Speed: " + speedKmh + " km/h");

-                tvSpeed.setTextColor(speedKmh > 0 ? Color.parseColor("#1976D2") : Color.parseColor("#999999"));

-    // ----- Raw Frame Listener for hex dump (RADIO + KEY + SPEED) -----

+    // ----- raw frame listener for hex dump -----

+            if (msgType != 0x05) return; // RADIO only

-                    case 0x01: // PUBLIC — speed, etc.

-                        if (cmd == 0x0A) { // CAR_SPEED

-                            tvSpeedHex.setText("SPEED (" + body.length + "B): " + hex);

+                        tvFreqHex.setText("FREQ (" + body.length + "B): " + hex);

-                            tvKeyHex.setText("KEY (" + body.length + "B): " + hex);

+                    case 0x02: // PS_PTY

+                        tvPsPtyHex.setText("PS/PTY (" + body.length + "B): " + hex);

-                    case 0x05: // RADIO

-                                tvFreqHex.setText("FREQ (" + body.length + "B): " + hex);

-                                tvPsPtyHex.setText("PS/PTY (" + body.length + "B): " + hex);

-                                String rtHex = body.length > 32

-                                        ? bytesToHex(body, 32) + " ..."

-                                        : hex;

-                                tvRtHex.setText("RT (" + body.length + "B): " + rtHex);

+                        String rtHex = body.length > 32

+                                ? bytesToHex(body, 32) + " ..."

+                        tvRtHex.setText("RT (" + body.length + "B): " + rtHex);

+        title.setText("Radio Tuner + Audio");

-        root.addView(sectionTitle("SWC Keys & Speed"));

-        LinearLayout swcCard = card(root);

-        tvLastKey = monText(swcCard, "Key: --");

-        tvLastKey.setTypeface(null, Typeface.BOLD);

-        tvSpeed = monText(swcCard, "Speed: -- km/h");

-        tvSpeed.setTypeface(null, Typeface.BOLD);

-        // ===== Frame Monitor — Radio =====

-        root.addView(sectionTitle("Radio Monitor"));

+        // ===== Frame Monitor Card =====

+        root.addView(sectionTitle("Frame Monitor"));

-        // Row 1: valid + pty + TA + TP

+        // Row 1: valid + pty + TA + TP flags

-        McuClient.getInstance().radio().addListener(radioListener);

-        McuClient.getInstance().key().addListener(keyListener);

-        McuClient.getInstance().vehicle().addListener(vehicleListener);

+        McuClient.getInstance().radio().addListener(listener);

+        // ---- Frame Monitor updates ----

-        McuClient.getInstance().radio().removeListener(radioListener);

-        McuClient.getInstance().key().removeListener(keyListener);

-        McuClient.getInstance().vehicle().removeListener(vehicleListener);

+        McuClient.getInstance().radio().removeListener(listener);


'/Users/admin/workspace/jyq/qsjnic/docs/MCU_ARM_Reference/1.方控和车速自测.txt'，结合这个文档中说的模拟测试，我本机已经安装了picocom和minicom，你能否优化测试demo，然后我们终端中输入测试命令，然后安卓车机的demo里面看看能否收到数据，或者你先用串口工具结合文档的测试，先看看返回值是什么


Good, radio-demo is reverted to radio-only. Now let me enhance the vehicle-demo to add:
1. Speed display (onCarSpeed handler)
2. Raw frame hex for KEY and PUBLIC msgTypes
3. Maybe a combined "Frame Monitor" section for KEY and SPEED hex

Let me now read the current vehicle-demo more carefully to plan the additions. I already read it earlier. Let me plan what changes to make.

Existing vehicle-demo has:
- MCU Version card
- Line Detection card
- System Status card
- Key Domain card (key display + key test buttons)
- Control Commands card (mute, backlight, refresh)

What I need to add:
1. Speed display in Key Domain or a new card
2. Raw frame hex for KEY and SPEED frames

I'll add:
- Speed display in the Key Domain card (rename to "Key & Speed"?)
- A "Frame Hex" section at the bottom showing KEY and SPEED raw bytes

Let me modify the vehicle-demo now.


Now let me enhance the vehicle-demo with speed display and raw frame hex for KEY/PUBLIC.


1	package com.qsjnic.demo.vehicle;
2
3	import android.app.Activity;
4	import android.graphics.Color;
5	import android.graphics.Typeface;
6	import android.graphics.drawable.GradientDrawable;
7	import android.os.Bundle;
8	import android.os.Handler;
9	import android.os.Looper;
10	import android.util.Log;
11	import android.view.Gravity;
12	import android.view.View;
13	import android.widget.Button;
14	import android.widget.LinearLayout;
15	import android.widget.ScrollView;
16	import android.widget.TextView;
17
18	import com.qsjnic.mcu.contract.LineDetectInfo;
19	import com.qsjnic.mcu.contract.McuVersion;
20	import com.qsjnic.mcu.contract.VehicleSnapshot;
21	import com.qsjnic.mcu.sdk.McuClient;
22	import com.qsjnic.mcu.sdk.McuKeyClient;
23	import com.qsjnic.mcu.sdk.KeyListener;
24	import com.qsjnic.mcu.sdk.VehicleListener;
25
26	/**
27	 * Vehicle Demo — clean data panel + control grid.
28	 */
29	public class MainActivity extends Activity {
30
31	    private static final String TAG = "VehicleDemo";
32
33	    private final Handler ui = new Handler(Looper.getMainLooper());
34
35	    // Data panel fields
36	    private TextView tvHwVer, tvSwVer, tvBootVer;
37	    private TextView tvVoltage, tvAcc, tvIg, tvIll, tvReverse;
38	    private TextView tvSysStatus, tvBacklight, tvTime;
39	    private TextView tvKeyDomain;
40
41	    // Control buttons
42	    private Button btnMuteOn, btnMuteOff, btnBl50, btnBl100, btnRefresh;
43	    private Button btnKeyMode, btnKeyMute, btnKeyWorkMode, btnKeyCall;
44	    private Button btnKeyVoice, btnKeyHome, btnKeyOk;
45	    private Button btnKeyChUp, btnKeyChDown, btnKeyVolUp, btnKeyVolDown;
46
47	    private final VehicleListener listener = new VehicleListener() {
48	        @Override public void onLineDetectChanged(LineDetectInfo info) {
49	            ui.post(() -> updateLine(info));
50	        }
51	        @Override public void onVoltage(int mv) {
52	            ui.post(() -> tvVoltage.setText(mv >= 0 ? String.format("%.1f V", mv / 1000.0) : "--"));
53	        }
54	        @Override public void onSysStatus(int firstPwrOn) {
55	            ui.post(() -> tvSysStatus.setText(firstPwrOn >= 0 ? (firstPwrOn == 1 ? "YES" : "NO") : "--"));
56	        }
57	        @Override public void onBacklightChanged(int duty) {
58	            ui.post(() -> tvBacklight.setText(duty >= 0 ? duty + "%" : "--"));
59	        }
60	        @Override public void onMcuVersion(McuVersion v) {
61	            ui.post(() -> updateVersion(v));
62	        }
63	        @Override public void onTimeChanged(long epochMillis, boolean isSet) {
64	            ui.post(() -> tvTime.setText(isSet ? String.valueOf(epochMillis) : "--"));
65	        }
66	    };
67
68	    private final KeyListener keyListener = new KeyListener() {
69	        @Override public void onKeyEvent(int keyValue, int keyStatus) {
70	            ui.post(() -> tvKeyDomain.setText(String.format("value=0x%02X status=0x%02X", keyValue, keyStatus)));
71	        }
72	    };
73
74	    @Override
75	    protected void onCreate(Bundle savedInstanceState) {
76	        super.onCreate(savedInstanceState);
77
78	        ScrollView scroll = new ScrollView(this);
79	        LinearLayout root = new LinearLayout(this);
80	        root.setOrientation(LinearLayout.VERTICAL);
81	        root.setPadding(32, 32, 32, 32);
82	        root.setBackgroundColor(Color.parseColor("#F5F5F5"));
83
84	        // ===== Title =====
85	        TextView title = new TextView(this);
86	        title.setText("Vehicle Monitor");
87	        title.setTextSize(24f);
88	        title.setTypeface(null, Typeface.BOLD);
89	        title.setTextColor(Color.parseColor("#333333"));
90	        title.setPadding(0, 0, 0, 16);
91	        root.addView(title);
92
93	        // ===== MCU Version Card =====
94	        root.addView(sectionTitle("MCU Version"));
95	        LinearLayout verCard = card(root);
96	        tvHwVer  = row(verCard, "HW:", "--");
97	        tvSwVer  = row(verCard, "SW:", "--");
98	        tvBootVer = row(verCard, "Boot:", "--");
99
100	        // ===== Line Detect Card =====
101	        root.addView(sectionTitle("Line Detection"));
102	        LinearLayout lineCard = card(root);
103	        tvIg      = row(lineCard, "IG (Ignition):", "--");
104	        tvAcc     = row(lineCard, "ACC:", "--");
105	        tvIll     = row(lineCard, "Illumination:", "--");
106	        tvReverse = row(lineCard, "Reverse:", "--");
107
108	        // ===== System Status Card =====
109	        root.addView(sectionTitle("System Status"));
110	        LinearLayout sysCard = card(root);
111	        tvVoltage   = row(sysCard, "Voltage:", "--");
112	        tvSysStatus = row(sysCard, "First Power On:", "--");
113	        tvBacklight = row(sysCard, "Backlight:", "--");
114	        tvTime      = row(sysCard, "Time:", "--");
115
116	        // ===== Key Domain Card =====
117	        root.addView(sectionTitle("Key Domain"));
118	        LinearLayout keyCard = card(root);
119	        tvKeyDomain = row(keyCard, "Last Key:", "--");
120
121	        LinearLayout keyRow1 = buttonRow(keyCard);
122	        btnKeyMode     = btn(keyRow1, "MODE", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_MODE, McuKeyClient.KEY_STATUS_SHORT_PRESS));
123	        btnKeyMute     = btn(keyRow1, "MUTE", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_MUTE, McuKeyClient.KEY_STATUS_SHORT_PRESS));
124	        btnKeyWorkMode = btn(keyRow1, "WORKMODE", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_WORKMODE, McuKeyClient.KEY_STATUS_SHORT_PRESS));
125	        btnKeyCall     = btn(keyRow1, "CALL", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_CALL, McuKeyClient.KEY_STATUS_SHORT_PRESS));
126
127	        LinearLayout keyRow2 = buttonRow(keyCard);
128	        btnKeyVoice  = btn(keyRow2, "VOICE", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_VOICE_ASSISTANT, McuKeyClient.KEY_STATUS_SHORT_PRESS));
129	        btnKeyHome   = btn(keyRow2, "HOME", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_HOME, McuKeyClient.KEY_STATUS_SHORT_PRESS));
130	        btnKeyOk     = btn(keyRow2, "OK", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_OK, McuKeyClient.KEY_STATUS_SHORT_PRESS));
131
132	        LinearLayout keyRow3 = buttonRow(keyCard);
133	        btnKeyChUp    = btn(keyRow3, "上一曲", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_CHANNEL_UP, McuKeyClient.KEY_STATUS_SHORT_PRESS));
134	        btnKeyChDown  = btn(keyRow3, "下一曲", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_CHANNEL_DOWN, McuKeyClient.KEY_STATUS_SHORT_PRESS));
135	        btnKeyVolUp   = btn(keyRow3, "音量+", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_VOLUME_UP, McuKeyClient.KEY_STATUS_SHORT_PRESS));
136	        btnKeyVolDown = btn(keyRow3, "音量-", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_VOLUME_DOWN, McuKeyClient.KEY_STATUS_SHORT_PRESS));
137
138	        // ===== Control Panel =====
139	        root.addView(sectionTitle("Control Commands"));
140	        LinearLayout ctrlCard = card(root);
141
142	        // Row 1: Mute
143	        LinearLayout row1 = buttonRow(ctrlCard);
144	        btnMuteOn  = btn(row1, "Mute ON",  v -> McuClient.getInstance().vehicle().setMute(true));
145	        btnMuteOff = btn(row1, "Mute OFF", v -> McuClient.getInstance().vehicle().setMute(false));
146
147	        // Row 2: Backlight
148	        LinearLayout row2 = buttonRow(ctrlCard);
149	        btnBl50  = btn(row2, "BL 50%", v -> McuClient.getInstance().vehicle().setBacklight(50, 50));
150	        btnBl100 = btn(row2, "BL 100%", v -> McuClient.getInstance().vehicle().setBacklight(100, 100));
151
152	        // Row 3: Refresh
153	        LinearLayout row3 = buttonRow(ctrlCard);
154	        btnRefresh = btn(row3, "Refresh Init", v -> McuClient.getInstance().vehicle().requestInitInfo());
155
156	        scroll.addView(root);
157	        setContentView(scroll);
158
159	        McuClient.getInstance().init(getApplicationContext());
160	        McuClient.getInstance().vehicle().addListener(listener);
161	        McuClient.getInstance().key().addListener(keyListener);
162	    }
163
164	    // ---------- UI helpers ----------
165
166	    private TextView sectionTitle(String text) {
167	        TextView t = new TextView(this);
168	        t.setText(text);
169	        t.setTextSize(14f);
170	        t.setTypeface(null, Typeface.BOLD);
171	        t.setTextColor(Color.parseColor("#666666"));
172	        t.setPadding(8, 24, 0, 8);
173	        return t;
174	    }
175
176	    private LinearLayout card(LinearLayout parent) {
177	        LinearLayout card = new LinearLayout(this);
178	        card.setOrientation(LinearLayout.VERTICAL);
179	        card.setPadding(24, 24, 24, 24);
180	        card.setBackgroundColor(Color.WHITE);
181	        GradientDrawable bg = new GradientDrawable();
182	        bg.setColor(Color.WHITE);
183	        bg.setCornerRadius(16f);
184	        card.setBackground(bg);
185	        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
186	                LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
187	        lp.setMargins(0, 0, 0, 16);
188	        card.setLayoutParams(lp);
189	        parent.addView(card);
190	        return card;
191	    }
192
193	    private TextView row(LinearLayout parent, String label, String value) {
194	        LinearLayout row = new LinearLayout(this);
195	        row.setOrientation(LinearLayout.HORIZONTAL);
196	        row.setPadding(0, 8, 0, 8);
197
198	        TextView tvLabel = new TextView(this);
199	        tvLabel.setText(label);
200	        tvLabel.setTextSize(14f);
201	        tvLabel.setTextColor(Color.parseColor("#888888"));
202	        tvLabel.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f));
203	        row.addView(tvLabel);
204
205	        TextView tvValue = new TextView(this);
206	        tvValue.setText(value);
207	        tvValue.setTextSize(16f);
208	        tvValue.setTypeface(null, Typeface.BOLD);
209	        tvValue.setTextColor(Color.parseColor("#333333"));
210	        tvValue.setGravity(Gravity.END);
211	        row.addView(tvValue);
212
213	        parent.addView(row);
214	        return tvValue;
215	    }
216
217	    private LinearLayout buttonRow(LinearLayout parent) {
218	        LinearLayout row = new LinearLayout(this);
219	        row.setOrientation(LinearLayout.HORIZONTAL);
220	        row.setPadding(0, 8, 0, 8);
221	        parent.addView(row);
222	        return row;
223	    }
224
225	    private Button btn(LinearLayout parent, String label, View.OnClickListener l) {
226	        Button b = new Button(this);
227	        b.setText(label);
228	        b.setOnClickListener(l);
229	        b.setAllCaps(false);
230	        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
231	        lp.setMargins(8, 0, 8, 0);
232	        b.setLayoutParams(lp);
233	        parent.addView(b);
234	        return b;
235	    }
236
237	    // ---------- Data update ----------
238
239	    private void updateVersion(McuVersion v) {
240	        if (v == null) return;
241	        tvHwVer.setText(v.hwVersion != null ? v.hwVersion : "--");
242	        tvSwVer.setText(v.swVersion != null ? v.swVersion : "--");
243	        tvBootVer.setText(v.bootVersion != null ? v.bootVersion : "--");
244	    }
245
246	    private void updateLine(LineDetectInfo info) {
247	        if (info == null) return;
248	        tvIg.setText(info.ig ? "ON" : "OFF");
249	        tvIg.setTextColor(info.ig ? Color.parseColor("#4CAF50") : Color.parseColor("#999999"));
250	        tvAcc.setText(info.acc ? "ON" : "OFF");
251	        tvAcc.setTextColor(info.acc ? Color.parseColor("#4CAF50") : Color.parseColor("#999999"));
252	        tvIll.setText(info.illumination ? "ON" : "OFF");
253	        tvIll.setTextColor(info.illumination ? Color.parseColor("#4CAF50") : Color.parseColor("#999999"));
254	        tvReverse.setText(info.reverse ? "ON" : "OFF");
255	        tvReverse.setTextColor(info.reverse ? Color.parseColor("#FF5722") : Color.parseColor("#999999"));
256	    }
257
258	    @Override
259	    protected void onDestroy() {
260	        McuClient.getInstance().vehicle().removeListener(listener);
261	        McuClient.getInstance().key().removeListener(keyListener);
262	        super.onDestroy();
263	    }
264
265	    @Override
266	    protected void onResume() {
267	        super.onResume();
268	        if (!McuClient.getInstance().isReady()) {
269	            android.widget.Toast.makeText(this, "Service 未连接，5s后重试", android.widget.Toast.LENGTH_SHORT).show();
270	            new Handler(Looper.getMainLooper()).postDelayed(this::onResume, 5000);
271	        }
272	    }
273	}
274

package com.qsjnic.demo.vehicle;

import android.app.Activity;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.GradientDrawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;

import com.qsjnic.mcu.contract.LineDetectInfo;
import com.qsjnic.mcu.contract.McuVersion;
import com.qsjnic.mcu.contract.VehicleSnapshot;
import com.qsjnic.mcu.sdk.McuClient;
import com.qsjnic.mcu.sdk.McuKeyClient;
import com.qsjnic.mcu.sdk.KeyListener;
import com.qsjnic.mcu.sdk.VehicleListener;

/**
 * Vehicle Demo — clean data panel + control grid.
 */
public class MainActivity extends Activity {

    private static final String TAG = "VehicleDemo";

    private final Handler ui = new Handler(Looper.getMainLooper());

    // Data panel fields
    private TextView tvHwVer, tvSwVer, tvBootVer;
    private TextView tvVoltage, tvAcc, tvIg, tvIll, tvReverse;
    private TextView tvSysStatus, tvBacklight, tvTime;
    private TextView tvKeyDomain;

    // Control buttons
    private Button btnMuteOn, btnMuteOff, btnBl50, btnBl100, btnRefresh;
    private Button btnKeyMode, btnKeyMute, btnKeyWorkMode, btnKeyCall;
    private Button btnKeyVoice, btnKeyHome, btnKeyOk;
    private Button btnKeyChUp, btnKeyChDown, btnKeyVolUp, btnKeyVolDown;

    private final VehicleListener listener = new VehicleListener() {
        @Override public void onLineDetectChanged(LineDetectInfo info) {
            ui.post(() -> updateLine(info));
        }
        @Override public void onVoltage(int mv) {
            ui.post(() -> tvVoltage.setText(mv >= 0 ? String.format("%.1f V", mv / 1000.0) : "--"));
        }
        @Override public void onSysStatus(int firstPwrOn) {
            ui.post(() -> tvSysStatus.setText(firstPwrOn >= 0 ? (firstPwrOn == 1 ? "YES" : "NO") : "--"));
        }
        @Override public void onBacklightChanged(int duty) {
            ui.post(() -> tvBacklight.setText(duty >= 0 ? duty + "%" : "--"));
        }
        @Override public void onMcuVersion(McuVersion v) {
            ui.post(() -> updateVersion(v));
        }
        @Override public void onTimeChanged(long epochMillis, boolean isSet) {
            ui.post(() -> tvTime.setText(isSet ? String.valueOf(epochMillis) : "--"));
        }
    };

    private final KeyListener keyListener = new KeyListener() {
        @Override public void onKeyEvent(int keyValue, int keyStatus) {
            ui.post(() -> tvKeyDomain.setText(String.format("value=0x%02X status=0x%02X", keyValue, keyStatus)));
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ScrollView scroll = new ScrollView(this);
        LinearLayout root = new LinearLayout(this);
        root.setOrientation(LinearLayout.VERTICAL);
        root.setPadding(32, 32, 32, 32);
        root.setBackgroundColor(Color.parseColor("#F5F5F5"));

        // ===== Title =====
        TextView title = new TextView(this);
        title.setText("Vehicle Monitor");
        title.setTextSize(24f);
        title.setTypeface(null, Typeface.BOLD);
        title.setTextColor(Color.parseColor("#333333"));
        title.setPadding(0, 0, 0, 16);
        root.addView(title);

        // ===== MCU Version Card =====
        root.addView(sectionTitle("MCU Version"));
        LinearLayout verCard = card(root);
        tvHwVer  = row(verCard, "HW:", "--");
        tvSwVer  = row(verCard, "SW:", "--");
        tvBootVer = row(verCard, "Boot:", "--");

        // ===== Line Detect Card =====
        root.addView(sectionTitle("Line Detection"));
        LinearLayout lineCard = card(root);
        tvIg      = row(lineCard, "IG (Ignition):", "--");
        tvAcc     = row(lineCard, "ACC:", "--");
        tvIll     = row(lineCard, "Illumination:", "--");
        tvReverse = row(lineCard, "Reverse:", "--");

        // ===== System Status Card =====
        root.addView(sectionTitle("System Status"));
        LinearLayout sysCard = card(root);
        tvVoltage   = row(sysCard, "Voltage:", "--");
        tvSysStatus = row(sysCard, "First Power On:", "--");
        tvBacklight = row(sysCard, "Backlight:", "--");
        tvTime      = row(sysCard, "Time:", "--");

        // ===== Key Domain Card =====
        root.addView(sectionTitle("Key Domain"));
        LinearLayout keyCard = card(root);
        tvKeyDomain = row(keyCard, "Last Key:", "--");

        LinearLayout keyRow1 = buttonRow(keyCard);
        btnKeyMode     = btn(keyRow1, "MODE", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_MODE, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyMute     = btn(keyRow1, "MUTE", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_MUTE, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyWorkMode = btn(keyRow1, "WORKMODE", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_WORKMODE, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyCall     = btn(keyRow1, "CALL", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_CALL, McuKeyClient.KEY_STATUS_SHORT_PRESS));

        LinearLayout keyRow2 = buttonRow(keyCard);
        btnKeyVoice  = btn(keyRow2, "VOICE", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_VOICE_ASSISTANT, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyHome   = btn(keyRow2, "HOME", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_HOME, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyOk     = btn(keyRow2, "OK", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_OK, McuKeyClient.KEY_STATUS_SHORT_PRESS));

        LinearLayout keyRow3 = buttonRow(keyCard);
        btnKeyChUp    = btn(keyRow3, "上一曲", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_CHANNEL_UP, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyChDown  = btn(keyRow3, "下一曲", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_CHANNEL_DOWN, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyVolUp   = btn(keyRow3, "音量+", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_VOLUME_UP, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyVolDown = btn(keyRow3, "音量-", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_VOLUME_DOWN, McuKeyClient.KEY_STATUS_SHORT_PRESS));

        // ===== Control Panel =====
        root.addView(sectionTitle("Control Commands"));
        LinearLayout ctrlCard = card(root);

        // Row 1: Mute
        LinearLayout row1 = buttonRow(ctrlCard);
        btnMuteOn  = btn(row1, "Mute ON",  v -> McuClient.getInstance().vehicle().setMute(true));
        btnMuteOff = btn(row1, "Mute OFF", v -> McuClient.getInstance().vehicle().setMute(false));

        // Row 2: Backlight
        LinearLayout row2 = buttonRow(ctrlCard);
        btnBl50  = btn(row2, "BL 50%", v -> McuClient.getInstance().vehicle().setBacklight(50, 50));
        btnBl100 = btn(row2, "BL 100%", v -> McuClient.getInstance().vehicle().setBacklight(100, 100));

        // Row 3: Refresh
        LinearLayout row3 = buttonRow(ctrlCard);
        btnRefresh = btn(row3, "Refresh Init", v -> McuClient.getInstance().vehicle().requestInitInfo());

        scroll.addView(root);
        setContentView(scroll);

        McuClient.getInstance().init(getApplicationContext());
        McuClient.getInstance().vehicle().addListener(listener);
        McuClient.getInstance().key().addListener(keyListener);
    }

    // ---------- UI helpers ----------

    private TextView sectionTitle(String text) {
        TextView t = new TextView(this);
        t.setText(text);
        t.setTextSize(14f);
        t.setTypeface(null, Typeface.BOLD);
        t.setTextColor(Color.parseColor("#666666"));
        t.setPadding(8, 24, 0, 8);
        return t;
    }

    private LinearLayout card(LinearLayout parent) {
        LinearLayout card = new LinearLayout(this);
        card.setOrientation(LinearLayout.VERTICAL);
        card.setPadding(24, 24, 24, 24);
        card.setBackgroundColor(Color.WHITE);
        GradientDrawable bg = new GradientDrawable();
        bg.setColor(Color.WHITE);
        bg.setCornerRadius(16f);
        card.setBackground(bg);
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        lp.setMargins(0, 0, 0, 16);
        card.setLayoutParams(lp);
        parent.addView(card);
        return card;
    }

    private TextView row(LinearLayout parent, String label, String value) {
        LinearLayout row = new LinearLayout(this);
        row.setOrientation(LinearLayout.HORIZONTAL);
        row.setPadding(0, 8, 0, 8);

        TextView tvLabel = new TextView(this);
        tvLabel.setText(label);
        tvLabel.setTextSize(14f);
        tvLabel.setTextColor(Color.parseColor("#888888"));
        tvLabel.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f));
        row.addView(tvLabel);

        TextView tvValue = new TextView(this);
        tvValue.setText(value);
        tvValue.setTextSize(16f);
        tvValue.setTypeface(null, Typeface.BOLD);
        tvValue.setTextColor(Color.parseColor("#333333"));
        tvValue.setGravity(Gravity.END);
        row.addView(tvValue);

        parent.addView(row);
        return tvValue;
    }

    private LinearLayout buttonRow(LinearLayout parent) {
        LinearLayout row = new LinearLayout(this);
        row.setOrientation(LinearLayout.HORIZONTAL);
        row.setPadding(0, 8, 0, 8);
        parent.addView(row);
        return row;
    }

    private Button btn(LinearLayout parent, String label, View.OnClickListener l) {
        Button b = new Button(this);
        b.setText(label);
        b.setOnClickListener(l);
        b.setAllCaps(false);
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
        lp.setMargins(8, 0, 8, 0);
        b.setLayoutParams(lp);
        parent.addView(b);
        return b;
    }

    // ---------- Data update ----------

    private void updateVersion(McuVersion v) {
        if (v == null) return;
        tvHwVer.setText(v.hwVersion != null ? v.hwVersion : "--");
        tvSwVer.setText(v.swVersion != null ? v.swVersion : "--");
        tvBootVer.setText(v.bootVersion != null ? v.bootVersion : "--");
    }

    private void updateLine(LineDetectInfo info) {
        if (info == null) return;
        tvIg.setText(info.ig ? "ON" : "OFF");
        tvIg.setTextColor(info.ig ? Color.parseColor("#4CAF50") : Color.parseColor("#999999"));
        tvAcc.setText(info.acc ? "ON" : "OFF");
        tvAcc.setTextColor(info.acc ? Color.parseColor("#4CAF50") : Color.parseColor("#999999"));
        tvIll.setText(info.illumination ? "ON" : "OFF");
        tvIll.setTextColor(info.illumination ? Color.parseColor("#4CAF50") : Color.parseColor("#999999"));
        tvReverse.setText(info.reverse ? "ON" : "OFF");
        tvReverse.setTextColor(info.reverse ? Color.parseColor("#FF5722") : Color.parseColor("#999999"));
    }

    @Override
    protected void onDestroy() {
        McuClient.getInstance().vehicle().removeListener(listener);
        McuClient.getInstance().key().removeListener(keyListener);
        super.onDestroy();
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (!McuClient.getInstance().isReady()) {
            android.widget.Toast.makeText(this, "Service 未连接，5s后重试", android.widget.Toast.LENGTH_SHORT).show();
            new Handler(Looper.getMainLooper()).postDelayed(this::onResume, 5000);
        }
    }
}


Add SWC key events and vehicle speed display to radio-demo

Extend MainActivity.java to include: (1) KeyListener to display steering wheel key events with key name mapping, (2) VehicleListener to display car speed, (3) Extended raw frame hex monitor for msgType 0x01 (PUBLIC) and 0x02 (KEY), (4) SWC key test buttons matching the Pajero key values from the doc, (5) Speed test input. Then build APK and deploy.


Now let me plan the vehicle-demo modifications:

1. Add speed display to the VehicleListener
2. Add raw frame hex for KEY (msgType 0x02) and SPEED (msgType 0x01 cmd 0x0A)
3. Enhance the key display to show Pajero SWC key names
4. Add IMcuFrameListener for raw frame hex

Here's what I'll change:

### New fields:
- `tvSpeed` - speed display
- `tvKeyHex`, `tvSpeedHex` - raw frame hex

### Updated VehicleListener:
- Add `onCarSpeed(int speedKmh)` handler

### Updated KeyListener:
- Map key values to Pajero SWC names

### New IMcuFrameListener:
- Monitor msgType 0x01 (cmd 0x0A = CAR_SPEED) and msgType 0x02 (cmd 0x01 = M2A_KEY)

### New UI section:
- Key Domain card: add speed display
- Add Frame Hex card at the bottom

Let me make other improvements too:
- The key display row should show the Pajero SWC key name
- Add a speed display row

Let me write the edits.

Actually, since the changes are substantial, let me write the whole file. This is more reliable than multiple edits.

Let me plan the exact UI changes:

1. In Key Domain card:
   - Keep "Last Key:" row
   - Add "Speed:" row
   - Keep key test buttons
   - Add key hex and speed hex text

2. Add a "Frame Hex" section after Control Commands (or merge into Key Domain)

Actually, let me restructure slightly:
- Rename "Key Domain" to "Key & Speed"
- Add speed display
- Add hex displays

Or better: keep "Key Domain", add speed display to System Status, and add a new "Frame Hex" card.

Actually, let me keep it simple:
1. Add speed row to System Status card
2. Add "Last Key:" enhancement with SWC name
3. Add new "Frame Hex" card for KEY/SPEED raw hex

Let me write the code.


Write

package com.qsjnic.demo.vehicle;

import android.app.Activity;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.GradientDrawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;

import com.qsjnic.mcu.contract.IMcuFrameListener;
import com.qsjnic.mcu.contract.LineDetectInfo;
import com.qsjnic.mcu.contract.McuVersion;
import com.qsjnic.mcu.contract.RadioState;
import com.qsjnic.mcu.contract.VehicleSnapshot;
import com.qsjnic.mcu.sdk.McuClient;
import com.qsjnic.mcu.sdk.McuKeyClient;
import com.qsjnic.mcu.sdk.KeyListener;
import com.qsjnic.mcu.sdk.VehicleListener;

/**
 * Vehicle Demo — MCU data monitor with key/vehicle/hex display.
 */
public class MainActivity extends Activity {

    private static final String TAG = "VehicleDemo";

    private final Handler ui = new Handler(Looper.getMainLooper());

    // Data panel fields
    private TextView tvHwVer, tvSwVer, tvBootVer;
    private TextView tvVoltage, tvAcc, tvIg, tvIll, tvReverse;
    private TextView tvSysStatus, tvBacklight, tvTime;
    private TextView tvKeyDomain, tvSpeed;
    private TextView tvKeyHex, tvSpeedHex;

    // Control buttons
    private Button btnMuteOn, btnMuteOff, btnBl50, btnBl100, btnRefresh;
    private Button btnKeyMode, btnKeyMute, btnKeyWorkMode, btnKeyCall;
    private Button btnKeyVoice, btnKeyHome, btnKeyOk;
    private Button btnKeyChUp, btnKeyChDown, btnKeyVolUp, btnKeyVolDown;

    // ----- Pajero SWC key name mapping (MCU M2A direction) -----
    private static String swcName(int keyValue) {
        switch (keyValue) {
            case 0x00: return "IDLE";
            case 0x03: return "SPEECH";
            case 0x04: return "MUSIC";
            case 0x08: return "VOL-";
            case 0x09: return "VOL+";
            case 0x0E: return "PREV";
            case 0x0F: return "NEXT";
            case 0x18: return "ANSWER";
            case 0x19: return "REJECT";
            case 0x25: return "CAMERA";
            default:   return String.format("0x%02X", keyValue);
        }
    }

    private static String swcStatus(int status) {
        switch (status) {
            case 0x00: return "IDLE";
            case 0x01: return "TRIGGER";
            default:   return String.format("0x%02X", status);
        }
    }

    private final VehicleListener listener = new VehicleListener() {
        @Override public void onLineDetectChanged(LineDetectInfo info) {
            ui.post(() -> updateLine(info));
        }
        @Override public void onVoltage(int mv) {
            ui.post(() -> tvVoltage.setText(mv >= 0 ? String.format("%.1f V", mv / 1000.0) : "--"));
        }
        @Override public void onSysStatus(int firstPwrOn) {
            ui.post(() -> tvSysStatus.setText(firstPwrOn >= 0 ? (firstPwrOn == 1 ? "YES" : "NO") : "--"));
        }
        @Override public void onBacklightChanged(int duty) {
            ui.post(() -> tvBacklight.setText(duty >= 0 ? duty + "%" : "--"));
        }
        @Override public void onMcuVersion(McuVersion v) {
            ui.post(() -> updateVersion(v));
        }
        @Override public void onTimeChanged(long epochMillis, boolean isSet) {
            ui.post(() -> tvTime.setText(isSet ? String.valueOf(epochMillis) : "--"));
        }
        @Override public void onCarSpeed(int speedKmh) {
            Log.i(TAG, "Speed: " + speedKmh + " km/h");
            ui.post(() -> {
                tvSpeed.setText("Speed: " + speedKmh + " km/h");
                tvSpeed.setTextColor(speedKmh > 0 ? Color.parseColor("#1976D2") : Color.parseColor("#999999"));
            });
        }
    };

    private final KeyListener keyListener = new KeyListener() {
        @Override public void onKeyEvent(int keyValue, int keyStatus) {
            Log.i(TAG, "SWC keyValue=0x" + Integer.toHexString(keyValue) + " status=" + keyStatus);
            ui.post(() -> {
                String name = swcName(keyValue);
                String state = swcStatus(keyStatus);
                tvKeyDomain.setText(String.format("%s %s  [0x%02X/0x%02X]", name, state, keyValue, keyStatus));
                tvKeyDomain.setTextColor(keyStatus == 0x01 ? Color.parseColor("#FF5722") : Color.parseColor("#4CAF50"));
            });
        }
    };

    // ----- Raw frame listener for KEY + SPEED hex -----
    private final IMcuFrameListener.Stub rawFrameListener = new IMcuFrameListener.Stub() {
        @Override
        public void onMcuFrame(int frameType, int msgType, int cmd, byte[] body) {
            String hex = bytesToHex(body);
            ui.post(() -> {
                switch (msgType) {
                    case 0x01: // PUBLIC
                        if (cmd == 0x0A) { // CAR_SPEED
                            tvSpeedHex.setText("SPEED (" + body.length + "B): " + hex);
                        }
                        break;
                    case 0x02: // KEY
                        if (cmd == 0x01) { // M2A_KEY
                            tvKeyHex.setText("KEY (" + body.length + "B): " + hex);
                        }
                        break;
                }
            });
        }
        @Override public void onVehicleStateChanged(VehicleSnapshot s) {}
        @Override public void onRadioStateChanged(RadioState s) {}
        @Override public void onUpgradeProgress(com.qsjnic.mcu.contract.UpgradeProgress p) {}
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ScrollView scroll = new ScrollView(this);
        LinearLayout root = new LinearLayout(this);
        root.setOrientation(LinearLayout.VERTICAL);
        root.setPadding(32, 32, 32, 32);
        root.setBackgroundColor(Color.parseColor("#F5F5F5"));

        // ===== Title =====
        TextView title = new TextView(this);
        title.setText("Vehicle Monitor");
        title.setTextSize(24f);
        title.setTypeface(null, Typeface.BOLD);
        title.setTextColor(Color.parseColor("#333333"));
        title.setPadding(0, 0, 0, 16);
        root.addView(title);

        // ===== MCU Version Card =====
        root.addView(sectionTitle("MCU Version"));
        LinearLayout verCard = card(root);
        tvHwVer  = row(verCard, "HW:", "--");
        tvSwVer  = row(verCard, "SW:", "--");
        tvBootVer = row(verCard, "Boot:", "--");

        // ===== Line Detect Card =====
        root.addView(sectionTitle("Line Detection"));
        LinearLayout lineCard = card(root);
        tvIg      = row(lineCard, "IG (Ignition):", "--");
        tvAcc     = row(lineCard, "ACC:", "--");
        tvIll     = row(lineCard, "Illumination:", "--");
        tvReverse = row(lineCard, "Reverse:", "--");

        // ===== System Status Card =====
        root.addView(sectionTitle("System Status"));
        LinearLayout sysCard = card(root);
        tvVoltage   = row(sysCard, "Voltage:", "--");
        tvSysStatus = row(sysCard, "First Power On:", "--");
        tvBacklight = row(sysCard, "Backlight:", "--");
        tvTime      = row(sysCard, "Time:", "--");
        tvSpeed     = row(sysCard, "Speed:", "-- km/h");

        // ===== Key Domain Card =====
        root.addView(sectionTitle("Key Domain"));
        LinearLayout keyCard = card(root);
        tvKeyDomain = row(keyCard, "Last SWC:", "--");

        LinearLayout keyRow1 = buttonRow(keyCard);
        btnKeyMode     = btn(keyRow1, "MODE", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_MODE, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyMute     = btn(keyRow1, "MUTE", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_MUTE, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyWorkMode = btn(keyRow1, "WORKMODE", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_WORKMODE, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyCall     = btn(keyRow1, "CALL", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_CALL, McuKeyClient.KEY_STATUS_SHORT_PRESS));

        LinearLayout keyRow2 = buttonRow(keyCard);
        btnKeyVoice  = btn(keyRow2, "VOICE", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_VOICE_ASSISTANT, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyHome   = btn(keyRow2, "HOME", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_HOME, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyOk     = btn(keyRow2, "OK", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_OK, McuKeyClient.KEY_STATUS_SHORT_PRESS));

        LinearLayout keyRow3 = buttonRow(keyCard);
        btnKeyChUp    = btn(keyRow3, "上一曲", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_CHANNEL_UP, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyChDown  = btn(keyRow3, "下一曲", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_CHANNEL_DOWN, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyVolUp   = btn(keyRow3, "音量+", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_VOLUME_UP, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyVolDown = btn(keyRow3, "音量-", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_VOLUME_DOWN, McuKeyClient.KEY_STATUS_SHORT_PRESS));

        // ===== Frame Hex Card =====
        root.addView(sectionTitle("Frame Hex (KEY / SPEED)"));
        LinearLayout hexCard = card(root);
        tvKeyHex   = hexText(hexCard);
        tvSpeedHex = hexText(hexCard);

        // ===== Control Panel =====
        root.addView(sectionTitle("Control Commands"));
        LinearLayout ctrlCard = card(root);

        // Row 1: Mute
        LinearLayout row1 = buttonRow(ctrlCard);
        btnMuteOn  = btn(row1, "Mute ON",  v -> McuClient.getInstance().vehicle().setMute(true));
        btnMuteOff = btn(row1, "Mute OFF", v -> McuClient.getInstance().vehicle().setMute(false));

        // Row 2: Backlight
        LinearLayout row2 = buttonRow(ctrlCard);
        btnBl50  = btn(row2, "BL 50%", v -> McuClient.getInstance().vehicle().setBacklight(50, 50));
        btnBl100 = btn(row2, "BL 100%", v -> McuClient.getInstance().vehicle().setBacklight(100, 100));

        // Row 3: Refresh
        LinearLayout row3 = buttonRow(ctrlCard);
        btnRefresh = btn(row3, "Refresh Init", v -> McuClient.getInstance().vehicle().requestInitInfo());

        scroll.addView(root);
        setContentView(scroll);

        McuClient.getInstance().init(getApplicationContext());
        McuClient.getInstance().addReadyListener(() -> ui.post(() -> {
            McuClient.getInstance().addFrameListener(rawFrameListener);
        }));
        McuClient.getInstance().vehicle().addListener(listener);
        McuClient.getInstance().key().addListener(keyListener);
    }

    // ---------- UI helpers ----------

    private TextView sectionTitle(String text) {
        TextView t = new TextView(this);
        t.setText(text);
        t.setTextSize(14f);
        t.setTypeface(null, Typeface.BOLD);
        t.setTextColor(Color.parseColor("#666666"));
        t.setPadding(8, 24, 0, 8);
        return t;
    }

    private LinearLayout card(LinearLayout parent) {
        LinearLayout card = new LinearLayout(this);
        card.setOrientation(LinearLayout.VERTICAL);
        card.setPadding(24, 24, 24, 24);
        card.setBackgroundColor(Color.WHITE);
        GradientDrawable bg = new GradientDrawable();
        bg.setColor(Color.WHITE);
        bg.setCornerRadius(16f);
        card.setBackground(bg);
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        lp.setMargins(0, 0, 0, 16);
        card.setLayoutParams(lp);
        parent.addView(card);
        return card;
    }

    private TextView row(LinearLayout parent, String label, String value) {
        LinearLayout row = new LinearLayout(this);
        row.setOrientation(LinearLayout.HORIZONTAL);
        row.setPadding(0, 8, 0, 8);

        TextView tvLabel = new TextView(this);
        tvLabel.setText(label);
        tvLabel.setTextSize(14f);
        tvLabel.setTextColor(Color.parseColor("#888888"));
        tvLabel.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f));
        row.addView(tvLabel);

        TextView tvValue = new TextView(this);
        tvValue.setText(value);
        tvValue.setTextSize(16f);
        tvValue.setTypeface(null, Typeface.BOLD);
        tvValue.setTextColor(Color.parseColor("#333333"));
        tvValue.setGravity(Gravity.END);
        row.addView(tvValue);

        parent.addView(row);
        return tvValue;
    }

    private TextView hexText(LinearLayout parent) {
        TextView t = new TextView(this);
        t.setText("--");
        t.setTextSize(10f);
        t.setTypeface(Typeface.MONOSPACE);
        t.setTextColor(Color.parseColor("#888888"));
        t.setPadding(0, 4, 0, 4);
        parent.addView(t);
        return t;
    }

    private LinearLayout buttonRow(LinearLayout parent) {
        LinearLayout row = new LinearLayout(this);
        row.setOrientation(LinearLayout.HORIZONTAL);
        row.setPadding(0, 8, 0, 8);
        parent.addView(row);
        return row;
    }

    private Button btn(LinearLayout parent, String label, View.OnClickListener l) {
        Button b = new Button(this);
        b.setText(label);
        b.setOnClickListener(l);
        b.setAllCaps(false);
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
        lp.setMargins(8, 0, 8, 0);
        b.setLayoutParams(lp);
        parent.addView(b);
        return b;
    }

    private static String bytesToHex(byte[] bytes) {
        if (bytes == null || bytes.length == 0) return "(empty)";
        StringBuilder sb = new StringBuilder(bytes.length * 3);
        for (byte b : bytes) {
            sb.append(String.format("%02X ", b & 0xFF));
        }
        sb.setLength(sb.length() - 1);
        return sb.toString();
    }

    // ---------- Data update ----------

    private void updateVersion(McuVersion v) {
        if (v == null) return;
        tvHwVer.setText(v.hwVersion != null ? v.hwVersion : "--");
        tvSwVer.setText(v.swVersion != null ? v.swVersion : "--");
        tvBootVer.setText(v.bootVersion != null ? v.bootVersion : "--");
    }

    private void updateLine(LineDetectInfo info) {
        if (info == null) return;
        tvIg.setText(info.ig ? "ON" : "OFF");
        tvIg.setTextColor(info.ig ? Color.parseColor("#4CAF50") : Color.parseColor("#999999"));
        tvAcc.setText(info.acc ? "ON" : "OFF");
        tvAcc.setTextColor(info.acc ? Color.parseColor("#4CAF50") : Color.parseColor("#999999"));
        tvIll.setText(info.illumination ? "ON" : "OFF");
        tvIll.setTextColor(info.illumination ? Color.parseColor("#4CAF50") : Color.parseColor("#999999"));
        tvReverse.setText(info.reverse ? "ON" : "OFF");
        tvReverse.setTextColor(info.reverse ? Color.parseColor("#FF5722") : Color.parseColor("#999999"));
    }

    @Override
    protected void onDestroy() {
        McuClient.getInstance().vehicle().removeListener(listener);
        McuClient.getInstance().key().removeListener(keyListener);
        McuClient.getInstance().removeFrameListener(rawFrameListener);
        super.onDestroy();
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (!McuClient.getInstance().isReady()) {
            android.widget.Toast.makeText(this, "Service 未连接，5s后重试", android.widget.Toast.LENGTH_SHORT).show();
            new Handler(Looper.getMainLooper()).postDelayed(this::onResume, 5000);
        }
    }
}


The file /Users/admin/workspace/jyq/qsjnic/demos/vehicle-demo/app/src/main/java/com/qsjnic/demo/vehicle/MainActivity.java has been updated successfully. (file state is current in your context — no need to Read it back)

package com.qsjnic.demo.vehicle;

import android.app.Activity;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.GradientDrawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;

import com.qsjnic.mcu.contract.IMcuFrameListener;
import com.qsjnic.mcu.contract.LineDetectInfo;
import com.qsjnic.mcu.contract.McuVersion;
import com.qsjnic.mcu.contract.RadioState;
import com.qsjnic.mcu.contract.VehicleSnapshot;
import com.qsjnic.mcu.sdk.McuClient;
import com.qsjnic.mcu.sdk.McuKeyClient;
import com.qsjnic.mcu.sdk.KeyListener;
import com.qsjnic.mcu.sdk.VehicleListener;

/**
 * Vehicle Demo — MCU data monitor with key/vehicle/hex display.
 */
public class MainActivity extends Activity {

    private static final String TAG = "VehicleDemo";

    private final Handler ui = new Handler(Looper.getMainLooper());

    // Data panel fields
    private TextView tvHwVer, tvSwVer, tvBootVer;
    private TextView tvVoltage, tvAcc, tvIg, tvIll, tvReverse;
    private TextView tvSysStatus, tvBacklight, tvTime;
    private TextView tvKeyDomain, tvSpeed;
    private TextView tvKeyHex, tvSpeedHex;

    // Control buttons
    private Button btnMuteOn, btnMuteOff, btnBl50, btnBl100, btnRefresh;
    private Button btnKeyMode, btnKeyMute, btnKeyWorkMode, btnKeyCall;
    private Button btnKeyVoice, btnKeyHome, btnKeyOk;
    private Button btnKeyChUp, btnKeyChDown, btnKeyVolUp, btnKeyVolDown;

    // ----- Pajero SWC key name mapping (MCU M2A direction) -----
    private static String swcName(int keyValue) {
        switch (keyValue) {
            case 0x00: return "IDLE";
            case 0x03: return "SPEECH";
            case 0x04: return "MUSIC";
            case 0x08: return "VOL-";
            case 0x09: return "VOL+";
            case 0x0E: return "PREV";
            case 0x0F: return "NEXT";
            case 0x18: return "ANSWER";
            case 0x19: return "REJECT";
            case 0x25: return "CAMERA";
            default:   return String.format("0x%02X", keyValue);
        }
    }

    private static String swcStatus(int status) {
        switch (status) {
            case 0x00: return "IDLE";
            case 0x01: return "TRIGGER";
            default:   return String.format("0x%02X", status);
        }
    }

    private final VehicleListener listener = new VehicleListener() {
        @Override public void onLineDetectChanged(LineDetectInfo info) {
            ui.post(() -> updateLine(info));
        }
        @Override public void onVoltage(int mv) {
            ui.post(() -> tvVoltage.setText(mv >= 0 ? String.format("%.1f V", mv / 1000.0) : "--"));
        }
        @Override public void onSysStatus(int firstPwrOn) {
            ui.post(() -> tvSysStatus.setText(firstPwrOn >= 0 ? (firstPwrOn == 1 ? "YES" : "NO") : "--"));
        }
        @Override public void onBacklightChanged(int duty) {
            ui.post(() -> tvBacklight.setText(duty >= 0 ? duty + "%" : "--"));
        }
        @Override public void onMcuVersion(McuVersion v) {
            ui.post(() -> updateVersion(v));
        }
        @Override public void onTimeChanged(long epochMillis, boolean isSet) {
            ui.post(() -> tvTime.setText(isSet ? String.valueOf(epochMillis) : "--"));
        }
        @Override public void onCarSpeed(int speedKmh) {
            Log.i(TAG, "Speed: " + speedKmh + " km/h");
            ui.post(() -> {
                tvSpeed.setText("Speed: " + speedKmh + " km/h");
                tvSpeed.setTextColor(speedKmh > 0 ? Color.parseColor("#1976D2") : Color.parseColor("#999999"));
            });
        }
    };

    private final KeyListener keyListener = new KeyListener() {
        @Override public void onKeyEvent(int keyValue, int keyStatus) {
            Log.i(TAG, "SWC keyValue=0x" + Integer.toHexString(keyValue) + " status=" + keyStatus);
            ui.post(() -> {
                String name = swcName(keyValue);
                String state = swcStatus(keyStatus);
                tvKeyDomain.setText(String.format("%s %s  [0x%02X/0x%02X]", name, state, keyValue, keyStatus));
                tvKeyDomain.setTextColor(keyStatus == 0x01 ? Color.parseColor("#FF5722") : Color.parseColor("#4CAF50"));
            });
        }
    };

    // ----- Raw frame listener for KEY + SPEED hex -----
    private final IMcuFrameListener.Stub rawFrameListener = new IMcuFrameListener.Stub() {
        @Override
        public void onMcuFrame(int frameType, int msgType, int cmd, byte[] body) {
            String hex = bytesToHex(body);
            ui.post(() -> {
                switch (msgType) {
                    case 0x01: // PUBLIC
                        if (cmd == 0x0A) { // CAR_SPEED
                            tvSpeedHex.setText("SPEED (" + body.length + "B): " + hex);
                        }
                        break;
                    case 0x02: // KEY
                        if (cmd == 0x01) { // M2A_KEY
                            tvKeyHex.setText("KEY (" + body.length + "B): " + hex);
                        }
                        break;
                }
            });
        }
        @Override public void onVehicleStateChanged(VehicleSnapshot s) {}
        @Override public void onRadioStateChanged(RadioState s) {}
        @Override public void onUpgradeProgress(com.qsjnic.mcu.contract.UpgradeProgress p) {}
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ScrollView scroll = new ScrollView(this);
        LinearLayout root = new LinearLayout(this);
        root.setOrientation(LinearLayout.VERTICAL);
        root.setPadding(32, 32, 32, 32);
        root.setBackgroundColor(Color.parseColor("#F5F5F5"));

        // ===== Title =====
        TextView title = new TextView(this);
        title.setText("Vehicle Monitor");
        title.setTextSize(24f);
        title.setTypeface(null, Typeface.BOLD);
        title.setTextColor(Color.parseColor("#333333"));
        title.setPadding(0, 0, 0, 16);
        root.addView(title);

        // ===== MCU Version Card =====
        root.addView(sectionTitle("MCU Version"));
        LinearLayout verCard = card(root);
        tvHwVer  = row(verCard, "HW:", "--");
        tvSwVer  = row(verCard, "SW:", "--");
        tvBootVer = row(verCard, "Boot:", "--");

        // ===== Line Detect Card =====
        root.addView(sectionTitle("Line Detection"));
        LinearLayout lineCard = card(root);
        tvIg      = row(lineCard, "IG (Ignition):", "--");
        tvAcc     = row(lineCard, "ACC:", "--");
        tvIll     = row(lineCard, "Illumination:", "--");
        tvReverse = row(lineCard, "Reverse:", "--");

        // ===== System Status Card =====
        root.addView(sectionTitle("System Status"));
        LinearLayout sysCard = card(root);
        tvVoltage   = row(sysCard, "Voltage:", "--");
        tvSysStatus = row(sysCard, "First Power On:", "--");
        tvBacklight = row(sysCard, "Backlight:", "--");
        tvTime      = row(sysCard, "Time:", "--");
        tvSpeed     = row(sysCard, "Speed:", "-- km/h");

        // ===== Key Domain Card =====
        root.addView(sectionTitle("Key Domain"));
        LinearLayout keyCard = card(root);
        tvKeyDomain = row(keyCard, "Last SWC:", "--");

        LinearLayout keyRow1 = buttonRow(keyCard);
        btnKeyMode     = btn(keyRow1, "MODE", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_MODE, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyMute     = btn(keyRow1, "MUTE", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_MUTE, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyWorkMode = btn(keyRow1, "WORKMODE", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_WORKMODE, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyCall     = btn(keyRow1, "CALL", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_CALL, McuKeyClient.KEY_STATUS_SHORT_PRESS));

        LinearLayout keyRow2 = buttonRow(keyCard);
        btnKeyVoice  = btn(keyRow2, "VOICE", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_VOICE_ASSISTANT, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyHome   = btn(keyRow2, "HOME", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_HOME, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyOk     = btn(keyRow2, "OK", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_OK, McuKeyClient.KEY_STATUS_SHORT_PRESS));

        LinearLayout keyRow3 = buttonRow(keyCard);
        btnKeyChUp    = btn(keyRow3, "上一曲", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_CHANNEL_UP, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyChDown  = btn(keyRow3, "下一曲", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_CHANNEL_DOWN, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyVolUp   = btn(keyRow3, "音量+", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_VOLUME_UP, McuKeyClient.KEY_STATUS_SHORT_PRESS));
        btnKeyVolDown = btn(keyRow3, "音量-", v -> McuClient.getInstance().key().sendKeyEvent(McuKeyClient.KEY_VALUE_VOLUME_DOWN, McuKeyClient.KEY_STATUS_SHORT_PRESS));

        // ===== Frame Hex Card =====
        root.addView(sectionTitle("Frame Hex (KEY / SPEED)"));
        LinearLayout hexCard = card(root);
        tvKeyHex   = hexText(hexCard);
        tvSpeedHex = hexText(hexCard);

        // ===== Control Panel =====
        root.addView(sectionTitle("Control Commands"));
        LinearLayout ctrlCard = card(root);

        // Row 1: Mute
        LinearLayout row1 = buttonRow(ctrlCard);
        btnMuteOn  = btn(row1, "Mute ON",  v -> McuClient.getInstance().vehicle().setMute(true));
        btnMuteOff = btn(row1, "Mute OFF", v -> McuClient.getInstance().vehicle().setMute(false));

        // Row 2: Backlight
        LinearLayout row2 = buttonRow(ctrlCard);
        btnBl50  = btn(row2, "BL 50%", v -> McuClient.getInstance().vehicle().setBacklight(50, 50));
        btnBl100 = btn(row2, "BL 100%", v -> McuClient.getInstance().vehicle().setBacklight(100, 100));

        // Row 3: Refresh
        LinearLayout row3 = buttonRow(ctrlCard);
        btnRefresh = btn(row3, "Refresh Init", v -> McuClient.getInstance().vehicle().requestInitInfo());

        scroll.addView(root);
        setContentView(scroll);

        McuClient.getInstance().init(getApplicationContext());
        McuClient.getInstance().addReadyListener(() -> ui.post(() -> {
            McuClient.getInstance().addFrameListener(rawFrameListener);
        }));
        McuClient.getInstance().vehicle().addListener(listener);
        McuClient.getInstance().key().addListener(keyListener);
    }

    // ---------- UI helpers ----------

    private TextView sectionTitle(String text) {
        TextView t = new TextView(this);
        t.setText(text);
        t.setTextSize(14f);
        t.setTypeface(null, Typeface.BOLD);
        t.setTextColor(Color.parseColor("#666666"));
        t.setPadding(8, 24, 0, 8);
        return t;
    }

    private LinearLayout card(LinearLayout parent) {
        LinearLayout card = new LinearLayout(this);
        card.setOrientation(LinearLayout.VERTICAL);
        card.setPadding(24, 24, 24, 24);
        card.setBackgroundColor(Color.WHITE);
        GradientDrawable bg = new GradientDrawable();
        bg.setColor(Color.WHITE);
        bg.setCornerRadius(16f);
        card.setBackground(bg);
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        lp.setMargins(0, 0, 0, 16);
        card.setLayoutParams(lp);
        parent.addView(card);
        return card;
    }

    private TextView row(LinearLayout parent, String label, String value) {
        LinearLayout row = new LinearLayout(this);
        row.setOrientation(LinearLayout.HORIZONTAL);
        row.setPadding(0, 8, 0, 8);

        TextView tvLabel = new TextView(this);
        tvLabel.setText(label);
        tvLabel.setTextSize(14f);
        tvLabel.setTextColor(Color.parseColor("#888888"));
        tvLabel.setLayoutParams(new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f));
        row.addView(tvLabel);

        TextView tvValue = new TextView(this);
        tvValue.setText(value);
        tvValue.setTextSize(16f);
        tvValue.setTypeface(null, Typeface.BOLD);
        tvValue.setTextColor(Color.parseColor("#333333"));
        tvValue.setGravity(Gravity.END);
        row.addView(tvValue);

        parent.addView(row);
        return tvValue;
    }

    private TextView hexText(LinearLayout parent) {
        TextView t = new TextView(this);
        t.setText("--");
        t.setTextSize(10f);
        t.setTypeface(Typeface.MONOSPACE);
        t.setTextColor(Color.parseColor("#888888"));
        t.setPadding(0, 4, 0, 4);
        parent.addView(t);
        return t;
    }

    private LinearLayout buttonRow(LinearLayout parent) {
        LinearLayout row = new LinearLayout(this);
        row.setOrientation(LinearLayout.HORIZONTAL);
        row.setPadding(0, 8, 0, 8);
        parent.addView(row);
        return row;
    }

    private Button btn(LinearLayout parent, String label, View.OnClickListener l) {
        Button b = new Button(this);
        b.setText(label);
        b.setOnClickListener(l);
        b.setAllCaps(false);
        LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
        lp.setMargins(8, 0, 8, 0);
        b.setLayoutParams(lp);
        parent.addView(b);
        return b;
    }

    private static String bytesToHex(byte[] bytes) {
        if (bytes == null || bytes.length == 0) return "(empty)";
        StringBuilder sb = new StringBuilder(bytes.length * 3);
        for (byte b : bytes) {
            sb.append(String.format("%02X ", b & 0xFF));
        }
        sb.setLength(sb.length() - 1);
        return sb.toString();
    }

    // ---------- Data update ----------

    private void updateVersion(McuVersion v) {
        if (v == null) return;
        tvHwVer.setText(v.hwVersion != null ? v.hwVersion : "--");
        tvSwVer.setText(v.swVersion != null ? v.swVersion : "--");
        tvBootVer.setText(v.bootVersion != null ? v.bootVersion : "--");
    }

    private void updateLine(LineDetectInfo info) {
        if (info == null) return;
        tvIg.setText(info.ig ? "ON" : "OFF");
        tvIg.setTextColor(info.ig ? Color.parseColor("#4CAF50") : Color.parseColor("#999999"));
        tvAcc.setText(info.acc ? "ON" : "OFF");
        tvAcc.setTextColor(info.acc ? Color.parseColor("#4CAF50") : Color.parseColor("#999999"));
        tvIll.setText(info.illumination ? "ON" : "OFF");
        tvIll.setTextColor(info.illumination ? Color.parseColor("#4CAF50") : Color.parseColor("#999999"));
        tvReverse.setText(info.reverse ? "ON" : "OFF");
        tvReverse.setTextColor(info.reverse ? Color.parseColor("#FF5722") : Color.parseColor("#999999"));
    }

    @Override
    protected void onDestroy() {
        McuClient.getInstance().vehicle().removeListener(listener);
        McuClient.getInstance().key().removeListener(keyListener);
        McuClient.getInstance().removeFrameListener(rawFrameListener);
        super.onDestroy();
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (!McuClient.getInstance().isReady()) {
            android.widget.Toast.makeText(this, "Service 未连接，5s后重试", android.widget.Toast.LENGTH_SHORT).show();
            new Handler(Looper.getMainLooper()).postDelayed(this::onResume, 5000);
        }
    }
}

- * Vehicle Demo — clean data panel + control grid.

+ * Vehicle Demo — MCU data monitor with key/vehicle/hex display.

+    private TextView tvKeyDomain, tvSpeed;

+    private TextView tvKeyHex, tvSpeedHex;

+    // ----- Pajero SWC key name mapping (MCU M2A direction) -----

+    private static String swcName(int keyValue) {

+            case 0x03: return "SPEECH";

+            case 0x18: return "ANSWER";

+            case 0x19: return "REJECT";

+            case 0x25: return "CAMERA";

+            default:   return String.format("0x%02X", keyValue);

+    private static String swcStatus(int status) {

+            case 0x01: return "TRIGGER";

+            default:   return String.format("0x%02X", status);

+        @Override public void onCarSpeed(int speedKmh) {

+            Log.i(TAG, "Speed: " + speedKmh + " km/h");

+                tvSpeed.setText("Speed: " + speedKmh + " km/h");

+                tvSpeed.setTextColor(speedKmh > 0 ? Color.parseColor("#1976D2") : Color.parseColor("#999999"));

-            ui.post(() -> tvKeyDomain.setText(String.format("value=0x%02X status=0x%02X", keyValue, keyStatus)));

+            Log.i(TAG, "SWC keyValue=0x" + Integer.toHexString(keyValue) + " status=" + keyStatus);

+                String name = swcName(keyValue);

+                String state = swcStatus(keyStatus);

+                tvKeyDomain.setText(String.format("%s %s  [0x%02X/0x%02X]", name, state, keyValue, keyStatus));

+                tvKeyDomain.setTextColor(keyStatus == 0x01 ? Color.parseColor("#FF5722") : Color.parseColor("#4CAF50"));

+    // ----- Raw frame listener for KEY + SPEED hex -----

+    private final IMcuFrameListener.Stub rawFrameListener = new IMcuFrameListener.Stub() {

+        public void onMcuFrame(int frameType, int msgType, int cmd, byte[] body) {

+            String hex = bytesToHex(body);

+                    case 0x01: // PUBLIC

+                        if (cmd == 0x0A) { // CAR_SPEED

+                            tvSpeedHex.setText("SPEED (" + body.length + "B): " + hex);

+                            tvKeyHex.setText("KEY (" + body.length + "B): " + hex);

+        @Override public void onVehicleStateChanged(VehicleSnapshot s) {}

+        @Override public void onRadioStateChanged(RadioState s) {}

+        @Override public void onUpgradeProgress(com.qsjnic.mcu.contract.UpgradeProgress p) {}

tvTime      = row(sysCard, "Time:", "--");

+        tvSpeed     = row(sysCard, "Speed:", "-- km/h");

-        tvKeyDomain = row(keyCard, "Last Key:", "--");

+        tvKeyDomain = row(keyCard, "Last SWC:", "--");

+        root.addView(sectionTitle("Frame Hex (KEY / SPEED)"));

+        LinearLayout hexCard = card(root);

+        McuClient.getInstance().addReadyListener(() -> ui.post(() -> {

+            McuClient.getInstance().addFrameListener(rawFrameListener);

+    private TextView hexText(LinearLayout parent) {

+        TextView t = new TextView(this);

+        t.setTypeface(Typeface.MONOSPACE);

+        t.setTextColor(Color.parseColor("#888888"));

+    private static String bytesToHex(byte[] bytes) {

+        if (bytes == null || bytes.length == 0) return "(empty)";

+        StringBuilder sb = new StringBuilder(bytes.length * 3);

+            sb.append(String.format("%02X ", b & 0xFF));

+        McuClient.getInstance().removeFrameListener(rawFrameListener);


Now let me build both demos and install them. Let me also fix the task status.


Now let me build both demos and install.


Bash

Build radio-demo


cd /Users/admin/workspace/jyq/qsjnic/demos/vehicle-demo && ./gradlew assembleDebug 2>&1

Build vehicle-demo


> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:mergeDebugNativeDebugMetadata NO-SOURCE
> Task :app:javaPreCompileDebug UP-TO-DATE
> Task :app:checkDebugAarMetadata UP-TO-DATE
> Task :app:generateDebugResValues UP-TO-DATE
> Task :app:mapDebugSourceSetPaths UP-TO-DATE
> Task :app:generateDebugResources UP-TO-DATE
> Task :app:mergeDebugResources UP-TO-DATE
> Task :app:packageDebugResources UP-TO-DATE
> Task :app:parseDebugLocalResources UP-TO-DATE
> Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
> Task :app:extractDeepLinksDebug UP-TO-DATE
> Task :app:processDebugMainManifest UP-TO-DATE
> Task :app:processDebugManifest UP-TO-DATE
> Task :app:processDebugManifestForPackage UP-TO-DATE
> Task :app:processDebugResources UP-TO-DATE
> Task :app:compileDebugJavaWithJavac
> Task :app:mergeDebugShaders UP-TO-DATE
> Task :app:compileDebugShaders NO-SOURCE
> Task :app:generateDebugAssets UP-TO-DATE
> Task :app:mergeDebugAssets UP-TO-DATE
> Task :app:compressDebugAssets UP-TO-DATE
> Task :app:desugarDebugFileDependencies UP-TO-DATE
> Task :app:dexBuilderDebug
> Task :app:mergeDebugGlobalSynthetics UP-TO-DATE
> Task :app:processDebugJavaRes NO-SOURCE
> Task :app:mergeDebugJavaResource UP-TO-DATE
> Task :app:checkDebugDuplicateClasses UP-TO-DATE
> Task :app:mergeExtDexDebug UP-TO-DATE
> Task :app:mergeLibDexDebug UP-TO-DATE
> Task :app:mergeDebugJniLibFolders UP-TO-DATE
> Task :app:mergeDebugNativeLibs NO-SOURCE
> Task :app:stripDebugDebugSymbols NO-SOURCE
> Task :app:validateSigningDebug UP-TO-DATE
> Task :app:writeDebugAppMetadata UP-TO-DATE
> Task :app:writeDebugSigningConfigVersions UP-TO-DATE
> Task :app:mergeProjectDexDebug
> Task :app:packageDebug
> Task :app:createDebugApkListingFileRedirect UP-TO-DATE
> Task :app:assembleDebug

BUILD SUCCESSFUL in 860ms
31 actionable tasks: 4 executed, 27 up-to-date


Warning: SDK processing. This version only understands SDK XML versions up to 3 but an SDK XML file of version 4 was encountered. This can happen if you use versions of Android Studio and the command-line tools that were released at different times.
> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:mergeDebugNativeDebugMetadata NO-SOURCE
> Task :app:javaPreCompileDebug UP-TO-DATE
> Task :app:checkDebugAarMetadata UP-TO-DATE
> Task :app:generateDebugResValues UP-TO-DATE
> Task :app:mapDebugSourceSetPaths UP-TO-DATE
> Task :app:generateDebugResources UP-TO-DATE
> Task :app:mergeDebugResources UP-TO-DATE
> Task :app:packageDebugResources UP-TO-DATE
> Task :app:parseDebugLocalResources UP-TO-DATE
> Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
> Task :app:extractDeepLinksDebug UP-TO-DATE
> Task :app:processDebugMainManifest UP-TO-DATE
> Task :app:processDebugManifest UP-TO-DATE
> Task :app:processDebugManifestForPackage UP-TO-DATE
> Task :app:processDebugResources UP-TO-DATE
> Task :app:compileDebugJavaWithJavac
> Task :app:mergeDebugShaders UP-TO-DATE
> Task :app:compileDebugShaders NO-SOURCE
> Task :app:generateDebugAssets UP-TO-DATE
> Task :app:mergeDebugAssets UP-TO-DATE
> Task :app:compressDebugAssets UP-TO-DATE
> Task :app:dexBuilderDebug
> Task :app:processDebugJavaRes NO-SOURCE
> Task :app:mergeDebugJniLibFolders UP-TO-DATE
> Task :app:mergeDebugNativeLibs NO-SOURCE
> Task :app:stripDebugDebugSymbols NO-SOURCE
> Task :app:validateSigningDebug UP-TO-DATE
> Task :app:writeDebugAppMetadata UP-TO-DATE
> Task :app:writeDebugSigningConfigVersions UP-TO-DATE
> Task :app:checkDebugDuplicateClasses
> Task :app:mergeLibDexDebug UP-TO-DATE
> Task :app:mergeProjectDexDebug
> Task :app:mergeDebugJavaResource
> Task :app:desugarDebugFileDependencies
> Task :app:mergeDebugGlobalSynthetics UP-TO-DATE
> Task :app:mergeExtDexDebug
> Task :app:packageDebug
> Task :app:createDebugApkListingFileRedirect UP-TO-DATE
> Task :app:assembleDebug

BUILD SUCCESSFUL in 2s
31 actionable tasks: 8 executed, 23 up-to-date


Both builds successful. Now let me install both APKs and also try the serial connection.


两个构建都成功了。安装并开始串口测试。


adb -s 192.168.0.66:5555 install -r /Users/admin/workspace/jyq/qsjnic/demos/radio-demo/app/build/outputs/apk/debug/app-debug.apk 2>&1 && adb -s 192.168.0.66:5555 install -r /Users/admin/workspace/jyq/qsjnic/demos/vehicle-demo/app/build/outputs/apk/debug/app-debug.apk 2>&1

Install both APKs on car device


Performing Streamed Install
Success
Performing Streamed Install
Success


Both APKs installed. Now let me try to use Python to connect to the serial port properly. Since pyserial isn't available, let me use Python's built-in `termios` module for serial communication.

Let me write a quick Python script that:
1. Opens the serial port /dev/cu.usbmodem21302 at 921600
2. Sends "KeyTest 1 0x19 0x01\r\n"
3. Reads the response for 3 seconds
4. Prints the output


两个 APK 已安装成功。现在用 Python 的 termios 来正确建立串口连接并发送测试命令。


python3 << 'PYEOF'
import serial  # try import first
print("pyserial available")
PYEOF

Check if pyserial is somehow available


Error: Exit code 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'serial'