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

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.os.RemoteException;
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 : "";
                // Truncate display if too long
                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) {
            // Only monitor RADIO module (0x05)
            if (msgType != 0x05) return;
            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) + " ..." // truncate display
                                : hex;
                        tvRtHex.setText("RT (" + body.length + "B): " + rtHex);
                        break;
                    case 0x04: // TA_PTY_TIPS
                        break;
                    case 0x05: // WORK_STATUS
                        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);
            // Register raw frame listener for hex dump
            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);
        // AUDIO_SOURCE_FM_TUNER = 1998 (from system/audio.h)
        final int AUDIO_SOURCE_FM_TUNER = 1998;

        try {
            // Use DEFAULT source + setPreferredDevice(tuner) approach
            audioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT,
                    sampleRate, channelConfig, audioFormat, bufferSize);

            // Try to set device on API 24+
            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);
                        // Log first non-zero sample every 3 seconds
                        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;
    }

    /** Simple text label for monitor rows. */
    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;
    }

    /** Monospace hex text line. */
    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;
    }

    /** Convert full byte array to space-separated hex. */
    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); // trim trailing space
        return sb.toString();
    }

    /** Convert first N bytes to hex. */
    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 ----
        // valid bit (p[4] bit7) — the key firmware issue indicator
        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"));
        }

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

        // Traffic flags
        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"));

        // Track strongest signal
        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);
        }
    }
}

import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
+import android.os.RemoteException;
import android.util.Log;
import android.view.Gravity;
import android.view.View;

- * Radio Demo with FM Tuner audio passthrough.

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

public class MainActivity extends Activity {

+    private TextView tvValid, tvPty, tvTa, tvTp, tvPs, tvRt, tvWorkStatus, tvTaPtyTips;

+    private TextView tvFreqHex, tvPsPtyHex, tvRtHex;

+                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) {

+                lastRt = (rt != null) ? rt : "";

+                // Truncate display if too long

+                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"));

+                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);

+                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() {

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

+            // Only monitor RADIO module (0x05)

+            if (msgType != 0x05) return;

+            String hex = bytesToHex(body);

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

+                    case 0x02: // PS_PTY

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

+                        String rtHex = body.length > 32

+                                ? bytesToHex(body, 32) + " ..." // truncate display

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

+                    case 0x04: // TA_PTY_TIPS

+                    case 0x05: // WORK_STATUS

+        @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) {}

tvStereo = chip(detailRow, "Stereo: --");

+        // ===== 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);

+        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);

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

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

+        tvPs = monText(monCard, "PS: --");

+        tvRt = monText(monCard, "RT: --");

+        divider.setBackgroundColor(Color.parseColor("#E0E0E0"));

+        LinearLayout.LayoutParams dlp = new LinearLayout.LayoutParams(

+                LinearLayout.LayoutParams.MATCH_PARENT, 1);

+        TextView hexLabel = new TextView(this);

+        hexLabel.setText("Raw Frame Hex");

+        hexLabel.setTypeface(null, Typeface.BOLD);

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

+        hexLabel.setPadding(0, 0, 0, 4);

+            // Register raw frame listener for hex dump

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

+    /** Simple text label for monitor rows. */

+    private TextView monText(LinearLayout parent, String text) {

+        TextView t = new TextView(this);

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

+    private TextView hexText(LinearLayout parent) {

+        t.setTypeface(Typeface.MONOSPACE);

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

LinearLayout row = new LinearLayout(this);

+    /** Convert full byte array to space-separated hex. */

+    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));

+        sb.setLength(sb.length() - 1); // trim trailing space

+    /** Convert first N bytes to hex. */

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

+        int n = Math.min(bytes.length, limit);

+        StringBuilder sb = new StringBuilder(n * 3);

+            sb.append(String.format("%02X ", bytes[i] & 0xFF));

+        if (bytes.length > limit) sb.append(" ...");

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

+        // valid bit (p[4] bit7) — the key firmware issue indicator

+            tvValid.setText("valid: LOCK");

+            tvValid.setTextColor(Color.parseColor("#4CAF50"));

+            tvValid.setText("valid: 0");

+            tvValid.setTextColor(Color.parseColor("#F44336"));

+            tvValid.setText("valid: --");

+            tvValid.setTextColor(Color.parseColor("#999999"));

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

+        tvTa.setTextColor(s.flagTa == 1 ? Color.parseColor("#4CAF50") : Color.parseColor("#999999"));

+        tvTp.setTextColor(s.flagTp == 1 ? Color.parseColor("#4CAF50") : Color.parseColor("#999999"));

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


Good, the file is written. Now let me mark Task #1 as complete and start Task #2 (building the APK).

But first, let me check that the project builds correctly. I need to find the right gradle command.

Let me find the build.gradle for the radio-demo module.


Good, code written. Now let me build the APK.


Updated task #1 status


Updated task #2 status


Let me find the gradle build files and build the APK.


Bash

ls /Users/admin/workspace/jyq/qsjnic/demos/radio-demo/app/build.gradle* 2>/dev/null; ls /Users/admin/workspace/jyq/qsjnic/demos/radio-demo/build.gradle* 2>/dev/null; ls /Users/admin/workspace/jyq/qsjnic/demos/radio-demo/settings.gradle* 2>/dev/null

Find gradle build files for radio-demo


List radio-demo directory contents


README.md
app
build.gradle.kts
gradle
gradle.properties
gradlew
settings.gradle.kts


The radio-demo has its own gradle setup. Let me build it using its own gradlew.


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

Build radio-demo APK


Starting a Gradle Daemon (subsequent builds will be faster)
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
> Task :app:processDebugManifest
> 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:processDebugJavaRes NO-SOURCE
> Task :app:mergeDebugJniLibFolders UP-TO-DATE
> Task :app:mergeDebugNativeLibs NO-SOURCE
> Task :app:stripDebugDebugSymbols NO-SOURCE
> Task :app:checkDebugDuplicateClasses
> Task :app:mergeLibDexDebug UP-TO-DATE
> Task :app:validateSigningDebug UP-TO-DATE
> Task :app:writeDebugAppMetadata UP-TO-DATE
> Task :app:writeDebugSigningConfigVersions UP-TO-DATE
> Task :app:processDebugManifestForPackage
> Task :app:mergeDebugJavaResource
> Task :app:processDebugResources
> Task :app:compileDebugJavaWithJavac
> Task :app:dexBuilderDebug
> Task :app:mergeProjectDexDebug
> 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 6s
31 actionable tasks: 12 executed, 19 up-to-date


Build successful. Now let me find the APK and copy it to the testapp folder, along with the source code.


Build successful. Now let me copy the APK and source to the testapp folder.


Updated task #3 status


find /Users/admin/workspace/jyq/qsjnic/demos/radio-demo -name "*.apk" -type f 2>/dev/null

Find built APK files


/Users/admin/workspace/jyq/qsjnic/demos/radio-demo/app/build/outputs/apk/release/app-release.apk
/Users/admin/workspace/jyq/qsjnic/demos/radio-demo/app/build/outputs/apk/debug/app-debug.apk


Let me copy the debug APK to the testapp folder, along with the source code. The debug APK is what we just built. Let me also copy the AndroidManifest.xml for reference.

Actually, since the user wants the code also placed there, let me copy:
1. APK → /Users/admin/workspace/jyq/imgs/testapp/qsmw-radio-demo.apk
2. MainActivity.java → /Users/admin/workspace/jyq/imgs/testapp/MainActivity.java
3. AndroidManifest.xml → /Users/admin/workspace/jyq/imgs/testapp/AndroidManifest.xml

I could also create a simple README explaining the changes.


cp /Users/admin/workspace/jyq/qsjnic/demos/radio-demo/app/build/outputs/apk/debug/app-debug.apk "/Users/admin/workspace/jyq/imgs/testapp/qsmw-radio-demo.apk" && cp /Users/admin/workspace/jyq/qsjnic/demos/radio-demo/app/src/main/java/com/qsjnic/demo/radio/MainActivity.java "/Users/admin/workspace/jyq/imgs/testapp/MainActivity.java" && cp /Users/admin/workspace/jyq/qsjnic/demos/radio-demo/app/src/main/AndroidManifest.xml "/Users/admin/workspace/jyq/imgs/testapp/AndroidManifest.xml" && echo "All files copied successfully" && ls -la "/Users/admin/workspace/jyq/imgs/testapp/"

Copy APK and source to testapp folder


All files copied successfully
total 1608
drwxr-xr-x@  5 admin  staff     160 May 29 11:06 .
drwxr-xr-x  12 admin  staff     384 May 29 10:05 ..
-rw-r--r--@  1 admin  staff     904 May 29 11:06 AndroidManifest.xml
-rw-r--r--@  1 admin  staff   26185 May 29 11:06 MainActivity.java
-rw-r--r--@  1 admin  staff  789270 May 29 11:06 qsmw-radio-demo.apk


All files are copied. Let me now verify the APK works by checking its contents briefly, and then mark task 3 as complete.

Let me also verify the source file is correct by reading a few lines.

Actually, let me just verify everything is in order and mark the task as done.


All tasks are complete. Let me now give the user a summary of what was done.


全部完成。以下是改动总结：

---

## 改动内容

### 新增 "Frame Monitor" 调试卡片

在频率卡片和音频控制之间插入了一个新的监控卡片，包含两部分：

**解析值展示（从 RadioListener 回调）：**

| 字段 | 含义 | 数据来源 | 颜色提示 |
|------|------|----------|----------|
| `valid` | 调谐锁定标志 (p[4] bit7) | `onFreqChanged` → `RadioState.valid` | 绿色 LOCK / 红色 0 |
| `PTY` | 节目类型码 | `onFreqChanged` → `RadioState.pty` | — |
| `TA` / `TP` | 交通广播标志 | `onFreqChanged` → `RadioState.flagTa/flagTp` | 绿色 ON / 灰色 OFF |
| `Work` | 调谐器工作状态 | `onWorkStatus` | 绿色 OK / 红色 ERR |
| `Tips` | TA/PTY 搜索提示 | `onTaPtyTips` | — |
| `PS` | 台名（RDS） | `onPsPty` → ps 字符串 | 绿色有值 / 红色 empty |
| `RT` | 广播文本（RDS） | `onRadioText` → rt 字符串 | 绿色有值 / 红色 empty |

**原始 HEX 展示（通过 IMcuFrameListener）：**
- `FREQ (7B): XX XX XX XX XX XX XX` — 搜台帧原始字节
- `PS/PTY (9B): XX XX ...` — RDS 台名帧原始字节
- `RT (64B): XX XX ...` — 广播文本帧原始字节（超过 32 字节截断显示）

### 文件位置

```
/Users/admin/workspace/jyq/imgs/testapp/
├── qsmw-radio-demo.apk   ← 安装包
├── MainActivity.java     ← 完整源码
└── AndroidManifest.xml   ← 清单文件
```

### 使用方式

安装 APK 后，搜台时直接观察 "Frame Monitor" 卡片：
- 如果 `valid` 始终红色显示 0，说明 MCU 固件未填充 valid 位
- 如果 FREQ hex 中第 5 字节（valid+flags）和第 7 字节（signal）全为 0，与反馈文档中的 `26 D8 00 40 00 00 00` 现象一致
- 如果 PS/PTY hex 和 RT hex 全为 `00`，说明 RDS 数据未被填充


帮我安装


The user wants me to install the APK on a connected Android device.


Check connected Android devices


List of devices attached
192.168.0.66:5555	device


There's a device connected. Let me install the APK.


设备已连接，直接安装。


adb -s 192.168.0.66:5555 install -r "/Users/admin/workspace/jyq/imgs/testapp/qsmw-radio-demo.apk" 2>&1

Install APK on connected device


Performing Streamed Install
Success


已安装成功。设备 `192.168.0.66:5555` 上的 radio-demo 已更新为新版本，打开后搜台即可在 "Frame Monitor" 卡片中看到 valid、PS、RT、PTY 等调试信息和原始 HEX 数据。


帮我打开radio对demo


The user wants me to launch the radio demo app on the device.


adb -s 192.168.0.66:5555 shell am start -n com.qsjnic.demo.radio/.MainActivity 2>&1

Launch radio demo app


Starting: Intent { cmp=com.qsjnic.demo.radio/.MainActivity }


The app has been launched. Let me also check if there's a way to verify the app is running and see logs. But the user just asked to open it, so that's done.

Actually, let me also check if the service is running since the radio demo depends on the MCU service.


已打开。如果主界面显示 "Service: connecting" 没有变成 connected，说明 MCU 服务未运行。需要我也检查下服务状态吗？


我们给 radio demo 新增了 Frame Monitor 调试面板（valid、PS、RT、hex），APK 已安装并打开。下一步——确认 MCU 服务是否已连接上，然后开始用搜台观察反馈中的字段值。 (disable recaps in /config)


看看目前mcu版本号多少


The user wants to check the MCU firmware version. Let me look at the logcat to find version information, or check if there's a way to query it via the service.

Let me check logcat for version-related logs, or look at how the MCU version is reported in the codebase.


adb -s 192.168.0.66:5555 logcat -d -s "McuClient:*" "McuRadioClient:*" "RadioDemo:*" "FrameRouter:*" "QsMcuSystemService:*" 2>&1 | tail -50

Check MCU-related logs for version info


05-27 14:13:04.968  3233  3250 I FrameRouter: RADIO_FREQ FM 9950(*100KHz) signal=0
05-27 14:13:04.968  3233  3250 I FrameRouter: FREQ_BODY[7]=26 DE 00 40 00 00 00 | Byte12=0x00 (valid=0) Byte14(signal)=0x00
05-27 14:13:05.468  3233  3250 I FrameRouter: RADIO_FREQ FM 9950(*100KHz) signal=0
05-27 14:13:05.468  3233  3250 I FrameRouter: FREQ_BODY[7]=26 DE 00 40 00 00 00 | Byte12=0x00 (valid=0) Byte14(signal)=0x00
05-27 14:13:05.469  3233  3250 I FrameRouter: PS_PTY[9]=00 00 00 00 00 00 00 00 00 | PTY=0x00 PS="(empty)"
05-27 14:13:05.469 27739 27753 I RadioDemo: PS/PTY pi=0 pty=0 ps=
05-27 14:13:05.471 27739 27753 I RadioDemo: RT=
05-27 14:13:05.471  3233  3250 I FrameRouter: RT[64]=00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | RT="(empty)"
05-27 14:13:05.968  3233  3250 I FrameRouter: RADIO_FREQ FM 9950(*100KHz) signal=0
05-27 14:13:05.968  3233  3250 I FrameRouter: FREQ_BODY[7]=26 DE 00 40 00 00 00 | Byte12=0x00 (valid=0) Byte14(signal)=0x00
05-27 14:13:06.468  3233  3250 I FrameRouter: RADIO_FREQ FM 9950(*100KHz) signal=0
05-27 14:13:06.468  3233  3250 I FrameRouter: FREQ_BODY[7]=26 DE 00 40 00 00 00 | Byte12=0x00 (valid=0) Byte14(signal)=0x00
05-27 14:13:06.469  3233  3250 I FrameRouter: PS_PTY[9]=00 00 00 00 00 00 00 00 00 | PTY=0x00 PS="(empty)"
05-27 14:13:06.469 27739 27876 I RadioDemo: PS/PTY pi=0 pty=0 ps=
05-27 14:13:06.470 27739 27876 I RadioDemo: RT=
05-27 14:13:06.471  3233  3250 I FrameRouter: RT[64]=00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | RT="(empty)"
05-27 14:13:06.968  3233  3250 I FrameRouter: RADIO_FREQ FM 9950(*100KHz) signal=0
05-27 14:13:06.968  3233  3250 I FrameRouter: FREQ_BODY[7]=26 DE 00 40 00 00 00 | Byte12=0x00 (valid=0) Byte14(signal)=0x00
05-27 14:13:07.468  3233  3250 I FrameRouter: RADIO_FREQ FM 9950(*100KHz) signal=0
05-27 14:13:07.469  3233  3250 I FrameRouter: FREQ_BODY[7]=26 DE 00 40 00 00 00 | Byte12=0x00 (valid=0) Byte14(signal)=0x00
05-27 14:13:07.470  3233  3250 I FrameRouter: PS_PTY[9]=00 00 00 00 00 00 00 00 00 | PTY=0x00 PS="(empty)"
05-27 14:13:07.470 27739 27753 I RadioDemo: PS/PTY pi=0 pty=0 ps=
05-27 14:13:07.470 27739 27753 I RadioDemo: RT=
05-27 14:13:07.470  3233  3250 I FrameRouter: RT[64]=00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | RT="(empty)"
05-27 14:13:07.968  3233  3250 I FrameRouter: RADIO_FREQ FM 9950(*100KHz) signal=0
05-27 14:13:07.968  3233  3250 I FrameRouter: FREQ_BODY[7]=26 DE 00 40 00 00 00 | Byte12=0x00 (valid=0) Byte14(signal)=0x00
05-27 14:13:08.468  3233  3250 I FrameRouter: RADIO_FREQ FM 9950(*100KHz) signal=0
05-27 14:13:08.468  3233  3250 I FrameRouter: FREQ_BODY[7]=26 DE 00 40 00 00 00 | Byte12=0x00 (valid=0) Byte14(signal)=0x00
05-27 14:13:08.469  3233  3250 I FrameRouter: PS_PTY[9]=00 00 00 00 00 00 00 00 00 | PTY=0x00 PS="(empty)"
05-27 14:13:08.469 27739 27752 I RadioDemo: PS/PTY pi=0 pty=0 ps=
05-27 14:13:08.470 27739 27876 I RadioDemo: RT=
05-27 14:13:08.470  3233  3250 I FrameRouter: RT[64]=00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | RT="(empty)"
05-27 14:13:08.969  3233  3250 I FrameRouter: RADIO_FREQ FM 9950(*100KHz) signal=0
05-27 14:13:08.969  3233  3250 I FrameRouter: FREQ_BODY[7]=26 DE 00 40 00 00 00 | Byte12=0x00 (valid=0) Byte14(signal)=0x00
05-27 14:13:09.468  3233  3250 I FrameRouter: RADIO_FREQ FM 9950(*100KHz) signal=0
05-27 14:13:09.468  3233  3250 I FrameRouter: FREQ_BODY[7]=26 DE 00 40 00 00 00 | Byte12=0x00 (valid=0) Byte14(signal)=0x00
05-27 14:13:09.471  3233  3250 I FrameRouter: PS_PTY[9]=00 00 00 00 00 00 00 00 00 | PTY=0x00 PS="(empty)"
05-27 14:13:09.471 27739 27752 I RadioDemo: PS/PTY pi=0 pty=0 ps=
05-27 14:13:09.471 27739 27752 I RadioDemo: RT=
05-27 14:13:09.471  3233  3250 I FrameRouter: RT[64]=00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | RT="(empty)"
05-27 14:13:09.968  3233  3250 I FrameRouter: RADIO_FREQ FM 9950(*100KHz) signal=0
05-27 14:13:09.968  3233  3250 I FrameRouter: FREQ_BODY[7]=26 DE 00 40 00 00 00 | Byte12=0x00 (valid=0) Byte14(signal)=0x00
05-27 14:13:10.468  3233  3250 I FrameRouter: RADIO_FREQ FM 9950(*100KHz) signal=0
05-27 14:13:10.468  3233  3250 I FrameRouter: FREQ_BODY[7]=26 DE 00 40 00 00 00 | Byte12=0x00 (valid=0) Byte14(signal)=0x00
05-27 14:13:10.469  3233  3250 I FrameRouter: PS_PTY[9]=00 00 00 00 00 00 00 00 00 | PTY=0x00 PS="(empty)"
05-27 14:13:10.469 27739 27876 I RadioDemo: PS/PTY pi=0 pty=0 ps=
05-27 14:13:10.470 27739 27752 I RadioDemo: RT=
05-27 14:13:10.470  3233  3250 I FrameRouter: RT[64]=00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | RT="(empty)"
05-27 14:13:10.968  3233  3250 I FrameRouter: RADIO_FREQ FM 9950(*100KHz) signal=0
05-27 14:13:10.969  3233  3250 I FrameRouter: FREQ_BODY[7]=26 DE 00 40 00 00 00 | Byte12=0x00 (valid=0) Byte14(signal)=0x00


The logs show the current state but not the MCU firmware version. Let me look for version-related logs. The version might be reported in a different tag. Let me search more broadly.

The feedback document mentions firmware version `0302_20260527`. Let me search for version in logcat.


Search logcat for MCU version info


05-27 14:13:21.878 26951 26965 I QsmwRepository: onMcuVersion: swVersion=0302_20260527
05-27 14:13:21.914  3233  3250 I FrameRouter: MCU Version: PAJERO69XZA0120018
05-27 14:13:21.915 26951 27931 I QsmwRepository: onMcuVersion: swVersion=0302_20260527
05-27 14:13:21.926  3233  3250 I FrameRouter: VERSION_STR type=0x00 "0302_20260527"
05-27 14:13:21.926 26951 26965 I QsmwRepository: onMcuVersion: swVersion=0302_20260527
05-27 14:13:21.942  3233  3250 I FrameRouter: VERSION_STR type=0x01 "03"
05-27 14:13:21.942 26951 27220 I QsmwRepository: onMcuVersion: swVersion=0302_20260527
05-27 14:13:21.955 26951 27051 I QsmwRepository: onMcuVersion: swVersion=0302_20260527
05-27 14:13:21.970 26951 27051 I QsmwRepository: onMcuVersion: swVersion=0302_20260527
05-27 14:13:21.983 26951 27051 I QsmwRepository: onMcuVersion: swVersion=0302_20260527
05-27 14:13:31.903 26951 26965 I QsmwRepository: onMcuVersion: swVersion=0302_20260527
05-27 14:13:31.918 26951 26965 I QsmwRepository: onMcuVersion: swVersion=0302_20260527
05-27 14:13:31.954  3233  3250 I FrameRouter: MCU Version: PAJERO69XZA0120018
05-27 14:13:31.954 26951 26965 I QsmwRepository: onMcuVersion: swVersion=0302_20260527
05-27 14:13:31.970 26951 26965 I QsmwRepository: onMcuVersion: swVersion=0302_20260527
05-27 14:13:31.970  3233  3250 I FrameRouter: VERSION_STR type=0x00 "0302_20260527"
05-27 14:13:31.982  3233  3250 I FrameRouter: VERSION_STR type=0x01 "03"
05-27 14:13:31.983 26951 27931 I QsmwRepository: onMcuVersion: swVersion=0302_20260527
05-27 14:13:31.994 26951 27931 I QsmwRepository: onMcuVersion: swVersion=0302_20260527
05-27 14:13:32.006 26951 27220 I QsmwRepository: onMcuVersion: swVersion=0302_20260527


The MCU version is `0302_20260527` — this matches the firmware version mentioned in the feedback document.

Let me summarize the findings from the logcat:
- MCU firmware version: `0302_20260527`
- Hardware version: `PAJERO69XZA0120018`
- The logs confirm the exact same issue as described in the feedback: valid=0, signal=0, PS empty, RT empty for all frames

The user asked about the version, so let me report this clearly.


当前 MCU 版本信息：

- **软件版本**: `0302_20260527`
- **硬件版本**: `PAJERO69XZA0120018`

这就是反馈文档中提到的那个固件版本。从日志中可以看到问题现象完全一致——valid 始终为 0、signal 为 0、PS/PTY 和 RT 全是 `00`。


We added a Frame Monitor card to the radio demo to display the MCU firmware issue values (valid, signal, PS, RT, raw hex). The APK is installed on the device and running — it confirms firmware `0302_20260527` with all fields stuck at zero. What would you like to do next? (disable recaps in /config)


能否帮我截一张图，就是当前车机屏幕的图


The user wants me to take a screenshot of the current car head unit screen. Since the device is connected via ADB, I can use `adb exec-out screencap` or `adb shell screencap` to capture the screen.


adb -s 192.168.0.66:5555 exec-out screencap -p > "/Users/admin/workspace/jyq/imgs/testapp/screenshot.png" 2>&1 && echo "Screenshot saved"

Take screenshot of car head unit screen


Screenshot saved


The image couldn't be displayed in the response, but it was saved to the file. Let me check the file size and provide the path to the user.

Wait, actually, the screenshot was taken. Let me verify it's a valid PNG and tell the user where to find it.


截图已保存到：

```
/Users/admin/workspace/jyq/imgs/testapp/screenshot.png
```

不过我无法在这个对话中直接渲染图片。你可以通过以下方式查看：

1. **Finder** — 直接打开上述路径
2. **终端** — `open /Users/admin/workspace/jyq/imgs/testapp/screenshot.png`

或者如果你需要我分析截图中的内容，可以描述你想确认的具体信息，我来通过其他方式帮你检查。


我们在 radio demo 中新增了 Frame Monitor 调试面板，显示 valid、PS、RT、PTY 和原始 hex 数据。已安装到车机并确认固件 0302_20260527 的问题现象——valid/PS/RT 全为零。下一步可以把这个截图或日志发给 MCU 固件团队作为证据。 (disable recaps in /config)


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


The user wants me to:
1. Read the test document about steering wheel controls and vehicle speed self-test
2. Use picocom or minicom to send test commands via serial port
3. Check if the Android demo can receive the data
4. Potentially optimize the demo to support this testing

Let me first read the document to understand what test commands are available.


Let me first read the test document.


1	MCU版本:PAJERO69XZA0120018
2	shell工具:https://www.xshell.com/zh/xshell/
3	1.方控自测(前提条件:Android与MCU必须建链成功)
4	[1].实物测试
5		接线: 将方控板KEY线与尾线SWC_L相接,另外一根线接GND
6		测试: 每次按下方控,会将对应key值与状态发给android端,详细MCU打印日志如下:
7
8		I(0000025683) KEYMGR,keymgr_key_trigger_handle:key_type[04] state[TRIGGER]
9		[ARM_CORE,write data:] ff,aa,00,0b,08,06,02,01,04,01,de
10		I(0000026043) KEYMGR,keymgr_key_trigger_handle:key_type[04] state[IDLE]
11		[ARM_CORE,write data:] ff,aa,00,0b,09,06,02,01,04,00,de
12
13		I(0000115553) KEYMGR,keymgr_key_trigger_handle:key_type[0E] state[TRIGGER]
14		[ARM_CORE,write data:] ff,aa,00,0b,0c,06,02,01,0e,01,d0
15		I(0000116233) KEYMGR,keymgr_key_trigger_handle:key_type[0E] state[IDLE]
16		[ARM_CORE,write data:] ff,aa,00,0b,0d,06,02,01,0e,00,d0
17
18		I(0000178883) KEYMGR,keymgr_key_trigger_handle:key_type[0F] state[TRIGGER]
19		[ARM_CORE,write data:] ff,aa,00,0b,0e,06,02,01,0f,01,cd
20		I(0000179033) KEYMGR,keymgr_key_trigger_handle:key_type[0F] state[IDLE]
21		[ARM_CORE,write data:] ff,aa,00,0b,0f,06,02,01,0f,00,cd
22
23
24		I(0000223153) KEYMGR,keymgr_key_trigger_handle:key_type[09] state[TRIGGER]
25		[ARM_CORE,write data:] ff,aa,00,0b,10,06,02,01,09,01,d1
26		I(0000223493) KEYMGR,keymgr_key_trigger_handle:key_type[09] state[IDLE]
27		[ARM_CORE,write data:] ff,aa,00,0b,11,06,02,01,09,00,d1
28
29
30		I(0000266683) KEYMGR,keymgr_key_trigger_handle:key_type[08] state[TRIGGER]
31		[ARM_CORE,write data:] ff,aa,00,0b,12,06,02,01,08,01,d0
32		I(0000266973) KEYMGR,keymgr_key_trigger_handle:key_type[08] state[IDLE]
33		[ARM_CORE,write data:] ff,aa,00,0b,13,06,02,01,08,00,d0
34
35
36		I(0000302853) KEYMGR,keymgr_key_trigger_handle:key_type[03] state[TRIGGER]
37		[ARM_CORE,write data:] ff,aa,00,0b,14,06,02,01,03,01,d3
38		I(0000303023) KEYMGR,keymgr_key_trigger_handle:key_type[03] state[IDLE]
39		[ARM_CORE,write data:] ff,aa,00,0b,15,06,02,01,03,00,d3
40
41
42		I(0000332953) KEYMGR,keymgr_key_trigger_handle:key_type[18] state[TRIGGER]
43		[ARM_CORE,write data:] ff,aa,00,0b,16,06,02,01,18,01,bc
44		I(0000333113) KEYMGR,keymgr_key_trigger_handle:key_type[18] state[IDLE]
45		[ARM_CORE,write data:] ff,aa,00,0b,17,06,02,01,18,00,bc
46
47
48		I(0000357333) KEYMGR,keymgr_key_trigger_handle:key_type[19] state[TRIGGER]
49		[ARM_CORE,write data:] ff,aa,00,0b,18,06,02,01,19,01,b9
50		I(0000357473) KEYMGR,keymgr_key_trigger_handle:key_type[19] state[IDLE]
51		[ARM_CORE,write data:] ff,aa,00,0b,19,06,02,01,19,00,b9
52
53
54		I(0000380363) KEYMGR,keymgr_key_trigger_handle:key_type[25] state[TRIGGER]
55		[ARM_CORE,write data:] ff,aa,00,0b,1a,06,02,01,25,01,ab
56		I(0000380523) KEYMGR,keymgr_key_trigger_handle:key_type[25] state[IDLE]
57		[ARM_CORE,write data:] ff,aa,00,0b,1b,06,02,01,25,00,ab
58
59
60	[2]. 模拟测试:
61		接上MCU打印，shell命令行输入 KeyTest + 测试项 + 按键值 + 按键状态
62		如下是模拟按键挂电话按下和弹起的过程:
63		KeyTest命令识别字符串,1表示第1个测试项,0x19表示按键值KEY_REJECT,0x01表示按键按下/0x00表示按键弹起
64
65		xiexw:/$ KeyTest 1 0x19 0x01
66		I(0000490647) KEYMGR,KeyTest:KeyTest mode:1 key:0x19 value:0x01
67		[ARM_CORE,write data:] ff,aa,00,0b,1c,06,02,01,19,01,b5
68
69		xiexw:/$ KeyTest 1 0x19 0x00
70		I(0000535107) KEYMGR,KeyTest:KeyTest mode:1 key:0x19 value:0x00
71		[ARM_CORE,write data:] ff,aa,00,0b,1d,06,02,01,19,00,b5
72
73	[3]. 帕杰罗方控按键值如下:
74	typedef enum
75	{
76	    EN_KEYMGR_SWC_TYPE_IDLE             = 0x00,//空闲
77	    EN_KEYMGR_SWC_TYPE_VOLUME_DOWN      = 0x08,//音量减
78	    EN_KEYMGR_SWC_TYPE_VOLUME_UP        = 0x09,//音量加
79	    EN_KEYMGR_SWC_TYPE_NEXT             = 0x0F,//下一曲
80	    EN_KEYMGR_SWC_TYPE_PREV             = 0x0E,//上一曲
81	    EN_KEYMGR_SWC_TYPE_MUSIC            = 0x04,//音乐
82	    EN_KEYMGR_SWC_TYPE_SPEECH           = 0x03,//语音
83	    EN_KEYMGR_SWC_TYPE_ANSWER           = 0x18,//接听
84	    EN_KEYMGR_SWC_TYPE_REJECT           = 0x19,//挂断
85	    EN_KEYMGR_SWC_TYPE_CAMERA           = 0x25,//摄像头
86	} EN_KEYMGR_SWC_TYPE;
87
88	[4]. KEY_A2M_KEY暂时不用实现
89
90
91
92	2. 车速上报模拟测试:
93		接上MCU打印，shell命令行输入 PublicDebug + 23 + 车速 + 百分比,如下是MCU端打印:
94		xiexw:/$ PublicDebug 23 60 100
95		I(0001000191) PUBLIC_MGR,PublicDebug:Set car speed: 60, ratio: 100
96		Return: 0, 0x00000000
97

MCU版本:PAJERO69XZA0120018
shell工具:https://www.xshell.com/zh/xshell/
1.方控自测(前提条件:Android与MCU必须建链成功)
[1].实物测试
	接线: 将方控板KEY线与尾线SWC_L相接,另外一根线接GND
	测试: 每次按下方控,会将对应key值与状态发给android端,详细MCU打印日志如下:

	I(0000025683) KEYMGR,keymgr_key_trigger_handle:key_type[04] state[TRIGGER]
	[ARM_CORE,write data:] ff,aa,00,0b,08,06,02,01,04,01,de
	I(0000026043) KEYMGR,keymgr_key_trigger_handle:key_type[04] state[IDLE]
	[ARM_CORE,write data:] ff,aa,00,0b,09,06,02,01,04,00,de

	I(0000115553) KEYMGR,keymgr_key_trigger_handle:key_type[0E] state[TRIGGER]
	[ARM_CORE,write data:] ff,aa,00,0b,0c,06,02,01,0e,01,d0
	I(0000116233) KEYMGR,keymgr_key_trigger_handle:key_type[0E] state[IDLE]
	[ARM_CORE,write data:] ff,aa,00,0b,0d,06,02,01,0e,00,d0

	I(0000178883) KEYMGR,keymgr_key_trigger_handle:key_type[0F] state[TRIGGER]
	[ARM_CORE,write data:] ff,aa,00,0b,0e,06,02,01,0f,01,cd
	I(0000179033) KEYMGR,keymgr_key_trigger_handle:key_type[0F] state[IDLE]
	[ARM_CORE,write data:] ff,aa,00,0b,0f,06,02,01,0f,00,cd

	I(0000223153) KEYMGR,keymgr_key_trigger_handle:key_type[09] state[TRIGGER]
	[ARM_CORE,write data:] ff,aa,00,0b,10,06,02,01,09,01,d1
	I(0000223493) KEYMGR,keymgr_key_trigger_handle:key_type[09] state[IDLE]
	[ARM_CORE,write data:] ff,aa,00,0b,11,06,02,01,09,00,d1

	I(0000266683) KEYMGR,keymgr_key_trigger_handle:key_type[08] state[TRIGGER]
	[ARM_CORE,write data:] ff,aa,00,0b,12,06,02,01,08,01,d0
	I(0000266973) KEYMGR,keymgr_key_trigger_handle:key_type[08] state[IDLE]
	[ARM_CORE,write data:] ff,aa,00,0b,13,06,02,01,08,00,d0

	I(0000302853) KEYMGR,keymgr_key_trigger_handle:key_type[03] state[TRIGGER]
	[ARM_CORE,write data:] ff,aa,00,0b,14,06,02,01,03,01,d3
	I(0000303023) KEYMGR,keymgr_key_trigger_handle:key_type[03] state[IDLE]
	[ARM_CORE,write data:] ff,aa,00,0b,15,06,02,01,03,00,d3

	I(0000332953) KEYMGR,keymgr_key_trigger_handle:key_type[18] state[TRIGGER]
	[ARM_CORE,write data:] ff,aa,00,0b,16,06,02,01,18,01,bc
	I(0000333113) KEYMGR,keymgr_key_trigger_handle:key_type[18] state[IDLE]
	[ARM_CORE,write data:] ff,aa,00,0b,17,06,02,01,18,00,bc

	I(0000357333) KEYMGR,keymgr_key_trigger_handle:key_type[19] state[TRIGGER]
	[ARM_CORE,write data:] ff,aa,00,0b,18,06,02,01,19,01,b9
	I(0000357473) KEYMGR,keymgr_key_trigger_handle:key_type[19] state[IDLE]
	[ARM_CORE,write data:] ff,aa,00,0b,19,06,02,01,19,00,b9

	I(0000380363) KEYMGR,keymgr_key_trigger_handle:key_type[25] state[TRIGGER]
	[ARM_CORE,write data:] ff,aa,00,0b,1a,06,02,01,25,01,ab
	I(0000380523) KEYMGR,keymgr_key_trigger_handle:key_type[25] state[IDLE]
	[ARM_CORE,write data:] ff,aa,00,0b,1b,06,02,01,25,00,ab

[2]. 模拟测试:
	接上MCU打印，shell命令行输入 KeyTest + 测试项 + 按键值 + 按键状态
	如下是模拟按键挂电话按下和弹起的过程:
	KeyTest命令识别字符串,1表示第1个测试项,0x19表示按键值KEY_REJECT,0x01表示按键按下/0x00表示按键弹起

	xiexw:/$ KeyTest 1 0x19 0x01
	I(0000490647) KEYMGR,KeyTest:KeyTest mode:1 key:0x19 value:0x01
	[ARM_CORE,write data:] ff,aa,00,0b,1c,06,02,01,19,01,b5

	xiexw:/$ KeyTest 1 0x19 0x00
	I(0000535107) KEYMGR,KeyTest:KeyTest mode:1 key:0x19 value:0x00
	[ARM_CORE,write data:] ff,aa,00,0b,1d,06,02,01,19,00,b5

[3]. 帕杰罗方控按键值如下:
typedef enum
{
    EN_KEYMGR_SWC_TYPE_IDLE             = 0x00,//空闲
    EN_KEYMGR_SWC_TYPE_VOLUME_DOWN      = 0x08,//音量减
    EN_KEYMGR_SWC_TYPE_VOLUME_UP        = 0x09,//音量加
    EN_KEYMGR_SWC_TYPE_NEXT             = 0x0F,//下一曲
    EN_KEYMGR_SWC_TYPE_PREV             = 0x0E,//上一曲
    EN_KEYMGR_SWC_TYPE_MUSIC            = 0x04,//音乐
    EN_KEYMGR_SWC_TYPE_SPEECH           = 0x03,//语音
    EN_KEYMGR_SWC_TYPE_ANSWER           = 0x18,//接听
    EN_KEYMGR_SWC_TYPE_REJECT           = 0x19,//挂断
    EN_KEYMGR_SWC_TYPE_CAMERA           = 0x25,//摄像头
} EN_KEYMGR_SWC_TYPE;

[4]. KEY_A2M_KEY暂时不用实现

2. 车速上报模拟测试:
	接上MCU打印，shell命令行输入 PublicDebug + 23 + 车速 + 百分比,如下是MCU端打印:
	xiexw:/$ PublicDebug 23 60 100
	I(0001000191) PUBLIC_MGR,PublicDebug:Set car speed: 60, ratio: 100
	Return: 0, 0x00000000


Now I understand. The document describes two types of tests:

1. **Key Test (方控自测)**:
   - Command: `KeyTest 1 <key_value> <state>`
   - Example: `KeyTest 1 0x19 0x01` (press reject key), `KeyTest 1 0x19 0x00` (release reject key)
   - This sends MCU frames over serial to Android

2. **Speed Test (车速上报模拟)**:
   - Command: `PublicDebug 23 <speed> <ratio>`
   - Example: `PublicDebug 23 60 100`

These commands are sent via the MCU serial console (via UART), not from the Android side. The user wants to:
- Use picocom/minicom to connect to the MCU serial port
- Send test commands
- See if the Android demo receives the data

But wait, the KeyTest and PublicDebug commands are shell commands on the MCU side, not raw serial data. They're commands you type into the MCU's shell (via serial console). The MCU then generates the actual protocol frames that get sent to Android.

So the flow would be:
1. Connect to MCU serial console via picocom/minicom
2. Type `KeyTest 1 0x19 0x01` in the MCU shell
3. MCU generates and sends the frame `ff,aa,00,0b,1c,06,02,01,19,01,b5` to Android via the protocol link
4. Android receives it and processes it

The Android side should already receive these frames if the MCU service is running and the link is established.

Now, the user also wants me to optimize the test demo. Currently the radio-demo only shows radio data. We need to also show key events and vehicle speed data in the demo app.

Let me first try to connect to the MCU serial port via picocom to send test commands. But I need to know:
- What serial port the MCU is connected to on the Android device
- The baud rate

From the exploration earlier: `/dev/ttyS7 @ 460800 baud`

But that's on the Android side. The MCU serial console might be a different port.

Actually, wait. Let me re-read the document. It says "接上MCU打印" (connect to MCU debug output). The KeyTest and PublicDebug are commands typed into the MCU's shell, which you access via serial connection to the MCU directly (not via Android).

But in the user's setup, the MCU is connected to the Android board via UART. The user wants to know if we can test this from the Android side, or if they need to connect to the MCU directly.

Actually, let me think about this differently. The user has a car head unit running Android. The MCU is connected to the Android board via serial (/dev/ttyS7). The MCU protocol frames flow over this serial connection.

The KeyTest and PublicDebug commands are MCU-side shell commands. To use them, you'd need to connect a PC to the MCU's debug serial port (not the MCU-Android link).

But the user says they have picocom and minicom installed. They probably want to:
1. Connect their Mac to the MCU debug serial port
2. Send test commands
3. Observe the frames on Android

OR... the user might want me to send raw serial data from the Android side to simulate MCU frames and see if the service processes them correctly.

Actually, I think the most practical approach is:
1. First, check what serial ports are available on the Android device
2. See if we can access the MCU serial from Android's shell directly
3. Or connect the Mac directly to the MCU's debug port

But since the Android device is connected via ADB over TCP (192.168.0.66:5555), and the MCU protocol link is on `/dev/ttyS7`, we could potentially:
- Send test frames from Android side by writing to the MCU serial port
- Or just verify that the Android service is receiving data when the MCU sends it

Let me first check what we can do. The simplest approach:
1. Check if we can see the MCU serial port on the Android device
2. Check the current logcat for key events to see if the MCU is sending any
3. Then figure out how to send test commands

Actually, let me re-read the user's request: "你能否优化测试demo，然后我们终端中输入测试命令，然后安卓车机的demo里面看看能否收到数据"

So the user wants to:
1. Optimize the test demo to display key events and vehicle speed data
2. Send test commands somehow (either via serial to MCU, or via Android shell)
3. See the results in the demo app

Let me think about what the demo currently shows: only radio data. We need to also show:
- SWC (Steering Wheel Control) key events
- Vehicle speed data

Let me first explore what key event and vehicle speed handling already exists in the codebase.


Interesting document. Let me first explore what key/vehicle handling already exists, then figure out the serial test approach.


Agent

Explore key/vehicle feature code

Prompt:
I need to understand how steering wheel key events and vehicle speed data are handled in this project at /Users/admin/workspace/jyq/qsjnic. Please search:

1. Find any Java/Kotlin files that handle KEY or key events (search for "KeyFeature", "keyFeature", "key_type", "SWC", "KEYMGR")
2. Find any Java/Kotlin files that handle vehicle speed or PublicDebug (search for "VehicleFeature", "vehicleFeature", "speed", "carSpeed")
3. Find the MsgType constants to understand what message types exist (KEY, VEHICLE, etc.)
4. Look for existing AIDL interfaces for key events and vehicle data
5. Check if there's an existing demo app for key/vehicle testing

Report file paths, class names, and the data flow from MCU frame → Java → listener callback. Be thorough.


Bash

adb -s 192.168.0.66:5555 shell "ls -la /dev/ttyS* 2>/dev/null; echo '---'; ls -la /dev/tty* 2>/dev/null | head -20"

Check available serial ports on Android device


I now have a complete picture. Here is the thorough report.

---

## Report: Steering Wheel Key Events and Vehicle Speed Data Handling

### Project Structure Overview

This is the `qsjnic` (Qisheng Jersey New Intelligent Cockpit) MCU communication framework. It follows a layered architecture:

```
Native (C/JNI)  -->  FrameRouter  -->  DomainBus  -->  Feature  -->  AIDL Binder  -->  SDK  -->  App
```

---

### 1. Message Type Constants (MsgType)

**File:** `/Users/admin/workspace/jyq/qsjnic/contract/src/main/java/com/qsjnic/mcu/domain/MsgType.java`

| Constant | Value | Domain |
|---|---|---|
| `PUBLIC` | `0x01` | Vehicle/system data, including speed |
| `KEY` | `0x02` | Steering wheel / front panel key events |
| `RADIO` | `0x05` | Radio tuner |
| `UPDATE` | `0x07` | Firmware OTA updates |

---

### 2. Key Event Handling (msgType = 0x02)

#### 2A. Command ID Constants

**File:** `/Users/admin/workspace/jyq/qsjnic/contract/src/main/java/com/qsjnic/mcu/domain/Cmds.java`

The `Key` inner class defines:
```java
public static final class Key {
    public static final int M2A_KEY = 0x01;   // MCU -> Android (steering wheel key press)
    public static final int A2M_KEY = 0x81;   // Android -> MCU (simulate key press)
}
```

#### 2B. Frame Decoder

**File:** `/Users/admin/workspace/jyq/qsjnic/contract/src/main/java/com/qsjnic/mcu/domain/Decoders.java`

```java
// Returns [keyValue, keyStatus] or null.
public static int[] parseKeyControl(byte[] body) {
    if (body == null || body.length < 2) return null;
    return new int[] { body[0] & 0xFF, body[1] & 0xFF };
}
```

- `body[0]` = **keyValue** (which button)
- `body[1]` = **keyStatus** (press/release/long-press/etc.)

#### 2C. Frame Encoder (A2M direction)

**File:** `/Users/admin/workspace/jyq/qsjnic/contract/src/main/java/com/qsjnic/mcu/domain/Commands.java`

```java
public static final class Key {
    public static byte[] key(int keyValue, int keyStatus) {
        require(keyValue >= 0 && keyValue <= 0x0B, "keyValue out of range");
        require(keyStatus >= 0 && keyStatus <= 0x04, "keyStatus out of range");
        return new byte[] { (byte) keyValue, (byte) keyStatus };
    }
}
```

#### 2D. Service-Side Feature: KeyFeature

**File:** `/Users/admin/workspace/jyq/qsjnic/service/src/main/java/com/qsjnic/mcu/service/feature/KeyFeature.java`

Class: `KeyFeature implements Feature`

- `accepts(int msgType)` -- returns `true` only for `MsgType.KEY`
- `onFrame(int frameType, int msgType, int cmd, byte[] body)` -- when `cmd == Cmds.Key.M2A_KEY (0x01)`, decodes via `Decoders.parseKeyControl(body)` and broadcasts the two ints (`keyValue`, `keyStatus`) to all registered `IMcuKeyListener` callbacks.
- Uses a `RemoteCallbackList<IMcuKeyListener>` for cross-process listener management.

#### 2E. AIDL Interface: IMcuKeyListener

**File:** `/Users/admin/workspace/jyq/qsjnic/contract/src/main/aidl/com/qsjnic/mcu/contract/IMcuKeyListener.aidl`

```aidl
interface IMcuKeyListener {
    void onKeyEvent(int keyValue, int keyStatus);
}
```

Note: this is a **two-way** (non-oneway) AIDL call. The `KeyFeature` broadcasts via `broadcastKeyEvent` which calls the `OneWayCall` helper, but the AIDL interface itself lacks the `oneway` keyword.

#### 2F. SDK Client: McuKeyClient

**File:** `/Users/admin/workspace/jyq/qsjnic/sdk/src/main/java/com/qsjnic/mcu/sdk/McuKeyClient.java`

Class: `McuKeyClient`

Key constants:
```java
public static final int KEY_VALUE_NONE            = 0x00;
public static final int KEY_VALUE_MODE            = 0x01;
public static final int KEY_VALUE_MUTE            = 0x02;
public static final int KEY_VALUE_WORKMODE        = 0x03;
public static final int KEY_VALUE_CALL            = 0x04;
public static final int KEY_VALUE_VOICE_ASSISTANT = 0x05;
public static final int KEY_VALUE_HOME            = 0x06;
public static final int KEY_VALUE_OK              = 0x07;
public static final int KEY_VALUE_CHANNEL_UP      = 0x08;
public static final int KEY_VALUE_CHANNEL_DOWN    = 0x09;
public static final int KEY_VALUE_VOLUME_UP       = 0x0A;
public static final int KEY_VALUE_VOLUME_DOWN     = 0x0B;

public static final int KEY_STATUS_RELEASE       = 0x00;
public static final int KEY_STATUS_SHORT_PRESS   = 0x01;
public static final int KEY_STATUS_STUCK         = 0x02;
public static final int KEY_STATUS_ENCODER_LEFT  = 0x03;
public static final int KEY_STATUS_ENCODER_RIGHT = 0x04;
```

It registers an `IMcuKeyListener.Stub` AIDL listener with the service, and fans out to a `CopyOnWriteArrayList<KeyListener>` (the Java `KeyListener` interface with `default` methods).

- `addListener(KeyListener l)` -- subscribes; auto-registers AIDL listener on service connect
- `removeListener(KeyListener l)` -- unsubscribes
- `sendKeyEvent(int keyValue, int keyStatus)` -- sends A2M key event to MCU via `IMcuService.sendKeyEvent()`
- `postKeyEvent(int keyValue, int keyStatus)` -- fire-and-forget via `postCommand()`

#### 2G. Java Listener Interface

**File:** `/Users/admin/workspace/jyq/qsjnic/sdk/src/main/java/com/qsjnic/mcu/sdk/KeyListener.java`

```java
public interface KeyListener {
    default void onKeyEvent(int keyValue, int keyStatus) {}
}
```

---

### 3. Vehicle Speed Data Handling (msgType = 0x01 / PUBLIC)

#### 3A. Command ID Constants for Speed

**File:** `/Users/admin/workspace/jyq/qsjnic/contract/src/main/java/com/qsjnic/mcu/domain/Cmds.java`

Within `Cmds.Public`:

| Direction | Constant | Value | Description |
|---|---|---|---|
| M2A | `M2A_CAR_SPEED` | `0x0A` | MCU reports car speed |
| M2A | `M2A_MILEAGE` | `0x0B` | MCU reports mileage |
| A2M | `SPEED_RATIO` | `0x8F` | Android sets speed ratio to MCU |
| A2M | `GPS_INFO` | `0x94` | Android sends GPS speed data to MCU |

#### 3B. Speed Decoder

**File:** `/Users/admin/workspace/jyq/qsjnic/contract/src/main/java/com/qsjnic/mcu/domain/Decoders.java`

```java
public static int parseCarSpeed(byte[] body) {
    if (body == null || body.length < 2) return -1;
    return ((body[0] & 0xFF) << 8) | (body[1] & 0xFF);
}
```

Speed is returned as an `int` in **km/h**, decoded from 2 bytes big-endian.

#### 3C. Speed Encoder (A2M)

**File:** `/Users/admin/workspace/jyq/qsjnic/contract/src/main/java/com/qsjnic/mcu/domain/Commands.java`

```java
public static byte[] speedRatio(int ratio) {
    require(ratio >= 0 && ratio <= 0xFF, "ratio out of range");
    return new byte[] { (byte) ratio };
}
```

#### 3D. Service-Side Feature: VehicleFeature

**File:** `/Users/admin/workspace/jyq/qsjnic/service/src/main/java/com/qsjnic/mcu/service/feature/VehicleFeature.java`

Class: `VehicleFeature implements Feature`

- `accepts(int msgType)` -- returns `true` only for `MsgType.PUBLIC (0x01)`
- Handles `Cmds.Public.M2A_CAR_SPEED (0x0A)` by calling `Decoders.parseCarSpeed(body)` and then `broadcastCarSpeed(kmh)`.
- The `broadcastCarSpeed` method fans out to all `IMcuVehicleListener` callbacks via `RemoteCallbackList`.
- Also maintains a `VehicleSnapshot` object that caches the latest known state (voltage, line detect, backlight, etc.), but notably **`VehicleSnapshot` does NOT contain a car speed field** -- speed is only broadcast as an ephemeral event, not cached.

#### 3E. AIDL Interface: IMcuVehicleListener

**File:** `/Users/admin/workspace/jyq/qsjnic/contract/src/main/aidl/com/qsjnic/mcu/contract/IMcuVehicleListener.aidl`

The speed-related callback:
```aidl
/** Public 0x0A CAR_SPEED (km/h) */
void onCarSpeed(int speedKmh);
```

This is a `oneway` interface (fire-and-forget, client should not block).

The full interface defines 25 callbacks covering line detect, voltage, backlight, key events (from the PUBLIC domain), battery info, car speed, mileage, fuel calibration, encryption status, vehicle config, factory commands, and more.

#### 3F. SDK Client: McuVehicleClient

**File:** `/Users/admin/workspace/jyq/qsjnic/sdk/src/main/java/com/qsjnic/mcu/sdk/McuVehicleClient.java`

- Registers an `IMcuVehicleListener.Stub` AIDL listener with the service.
- Fans out to `CopyOnWriteArrayList<VehicleListener>`.
- The AIDL stub's `onCarSpeed(int speedKmh)` iterates all Java `VehicleListener` instances and calls `l.onCarSpeed(speedKmh)`.

#### 3G. Java Listener Interface for Vehicle Data

**File:** `/Users/admin/workspace/jyq/qsjnic/sdk/src/main/java/com/qsjnic/mcu/sdk/VehicleListener.java`

```java
default void onCarSpeed(int speedKmh) {}
```

---

### 4. Full Data Flow: MCU Frame -> Java -> App Callback

There are two distinct key event paths:

#### Path A: Dedicated Key Domain (MsgType 0x02)

```
MCU (serial UART) -> native JNI (FrameRouter.onMcuFrame)
    -> FrameRouter.dispatch(msgType=0x02, cmd=0x01, body=[keyValue, keyStatus])
        -> DomainBus.onFrame() -> KeyFeature.onFrame() [if accepts(0x02)]
            -> Decoders.parseKeyControl(body) -> [keyValue, keyStatus]
            -> KeyFeature.broadcastKeyEvent(keyValue, keyStatus)
                -> IMcuKeyListener.onKeyEvent(keyValue, keyStatus) [AIDL to client process]
                    -> McuKeyClient.aidlListener.onKeyEvent()
                        -> KeyListener.onKeyEvent(keyValue, keyStatus) [app callback]
        -> FrameRouter.handleKey() [just logs the event]
```

#### Path B: Public Domain Key Event (MsgType 0x01, cmd 0x06)

```
MCU -> FrameRouter.onMcuFrame(msgType=0x01, cmd=0x06, body)
    -> VehicleFeature.onFrame() -> Cmds.Public.M2A_KEY_EVENT (0x06)
        -> Decoders.parseKeyEvent(body) -> raw byte (1 byte only)
        -> VehicleFeature.broadcastKeyEvent(rawByte)
            -> IMcuVehicleListener.onKeyEvent(int rawByte) [AIDL]
                -> McuVehicleClient.aidlListener.onKeyEvent(int raw)
                    -> VehicleListener.onKeyEvent(int rawByte) [app callback]
```

Path A carries both `keyValue` and `keyStatus` (2 bytes). Path B carries only a single raw byte and appears to be a legacy/alternate key event path.

#### Path C: Vehicle Speed (MsgType 0x01, cmd 0x0A)

```
MCU (serial UART) -> native JNI (FrameRouter.onMcuFrame)
    -> FrameRouter.dispatch(msgType=0x01, cmd=0x0A, body=[2 bytes speed])
        -> DomainBus.onFrame() -> VehicleFeature.onFrame()
            -> Stored in VehicleSnapshot: NOT cached for speed
            -> Decoders.parseCarSpeed(body) -> int speedKmh
            -> VehicleFeature.broadcastCarSpeed(speedKmh)
                -> IMcuVehicleListener.onCarSpeed(int speedKmh) [AIDL]
                    -> McuVehicleClient.aidlListener.onCarSpeed(int speedKmh)
                        -> VehicleListener.onCarSpeed(int speedKmh) [app callback]
```

Speed is streamed as a real-time event only -- it is **NOT** persisted in the `VehicleSnapshot` object (unlike voltage, backlight, line detect, etc.). The `VehicleSnapshot` class only holds: `lineDetect`, `voltageMv`, `firstPwrOn`, `tftDutyPercent`, `timeEpochMillis`, `timeIsSet`, and `mcuVersion`.

---

### 5. AIDL Interfaces (Complete List)

All at `/Users/admin/workspace/jyq/qsjnic/contract/src/main/aidl/com/qsjnic/mcu/contract/`:

| File | Role |
|---|---|
| `IMcuService.aidl` | Central service contract (register/unregister listeners, send commands) |
| `IMcuKeyListener.aidl` | Key domain callback (`onKeyEvent(keyValue, keyStatus)`) |
| `IMcuVehicleListener.aidl` | Public/vehicle domain callbacks (25 methods including `onCarSpeed`) |
| `IMcuRadioListener.aidl` | Radio tuner callbacks |
| `IMcuUpgradeListener.aidl` | OTA upgrade progress callbacks |
| `IMcuFrameListener.aidl` | Unified raw-frame listener (raw frames + decoded state snapshots) |
| `VehicleSnapshot.aidl` | Cached vehicle state parcelable |
| `RadioState.aidl` | Cached radio state parcelable |
| `UpgradeProgress.aidl` | Upgrade progress parcelable |
| `UpgradeRequest.aidl` | Upgrade request parcelable |
| `McuVersion.aidl` | MCU version parcelable |
| `LineDetectInfo.aidl` | Line detection state parcelable |

---

### 6. Existing Demo Apps for Key/Vehicle Testing

Three demo apps exist:

| Demo | Path | Focus |
|---|---|---|
| **vehicle-demo** | `/Users/admin/workspace/jyq/qsjnic/demos/vehicle-demo/app/src/main/java/com/qsjnic/demo/vehicle/MainActivity.java` | **Key + Vehicle** -- displays line detect, voltage, backlight, MCU version; has buttons to send key events (MODE, MUTE, WORKMODE, CALL, VOICE, HOME, OK, CH_UP/DOWN, VOL_UP/DOWN); sends mute, backlight, and refresh-init commands |
| **radio-demo** | `/Users/admin/workspace/jyq/qsjnic/demos/radio-demo/app/src/main/java/com/qsjnic/demo/radio/MainActivity.java` | Radio tuner with FM audio passthrough and raw frame hex monitor |
| **ota-demo** | `/Users/admin/workspace/jyq/qsjnic/demos/ota-demo/app/src/main/java/com/qsjnic/demo/ota/MainActivity.java` | Firmware upgrade (OTA) |

The **vehicle-demo** app is the one relevant to your needs. It:
- Registers both a `VehicleListener` and a `KeyListener`
- Receives `onKeyEvent(keyValue, keyStatus)` from the key domain
- Sends simulated key presses via `McuClient.getInstance().key().sendKeyEvent(value, status)`
- Does **not** currently display car speed (the UI shows no `onCarSpeed` handler, even though the `VehicleListener` stub could override it)

There is also a **sdk-smoketest** app at `/Users/admin/workspace/jyq/qsjnic/sdk-smoketest/src/main/java/com/qsjnic/mcu/sdksmoketest/MainActivity.java` which does a comprehensive 77-interface automated test and does handle `onCarSpeed` (logs it).

---

### 7. Key Service-Side Wiring (Orchestration)

**File:** `/Users/admin/workspace/jyq/qsjnic/service/src/main/java/com/qsjnic/mcu/service/QsMcuSystemService.java`

The service `onCreate()`:
1. Creates features: `new VehicleFeature(frameListeners)`, `new KeyFeature()`
2. Creates `DomainBus`, registers all features
3. Calls `FrameRouter.setBus(bus)`
4. Opens serial port, starts link state machine

**File:** `/Users/admin/workspace/jyq/qsjnic/service/src/main/java/com/qsjnic/mcu/service/McuBinder.java`

The AIDL binder implementation (`IMcuService.Stub`):
- `registerKeyListener(IMcuKeyListener)` / `unregisterKeyListener(...)` -- delegates to `KeyFeature`
- `registerVehicleListener(IMcuVehicleListener)` / `unregisterVehicleListener(...)` -- delegates to `VehicleFeature`
- `sendKeyEvent(int keyValue, int keyStatus)` -- builds payload via `Commands.Key.key()`, sends via `McuLink.sendAndWaitAck(MsgType.KEY, Cmds.Key.A2M_KEY, ...)`
- `getLatestVehicleSnapshot()` -- returns `VehicleFeature.snapshot()`

**File:** `/Users/admin/workspace/jyq/qsjnic/service/src/main/java/com/qsjnic/mcu/service/FrameRouter.java`

Static entry point called from native JNI (`jmethodID` cached in `jni_bridge.cpp`):
```java
@Keep
public static void onMcuFrame(int frameType, int msgType, int cmd, byte[] payload) {
    final byte[] copy = (payload == null) ? null : payload.clone();
    HANDLER.post(() -> dispatch(frameType, msgType, cmd, copy));
}
```

The native code runs on the RX thread and marshalls to a dedicated `HandlerThread` ("FrameRouter") for dispatch.

**File:** `/Users/admin/workspace/jyq/qsjnic/service/src/main/java/com/qsjnic/mcu/service/McuLink.java`

Singleton that wraps `libmcujni.so`. Manages UART open/close, SETUP handshake, heartbeat, send/ack, upgrade, and self-test. The native RX thread calls back into `FrameRouter.onMcuFrame()`.

---

### 8. Test Coverage

**File:** `/Users/admin/workspace/jyq/qsjnic/service/src/test/java/com/qsjnic/mcu/service/feature/KeyFeatureTest.java`
- Tests that `KeyFeature` accepts only `MsgType.KEY`
- Tests that a key frame does not crash

**File:** `/Users/admin/workspace/jyq/qsjnic/service/src/test/java/com/qsjnic/mcu/service/feature/VehicleFeatureTest.java`
- Tests voltage parsing (`12800 mV`), line detect decoding, sys status, backlight
- No explicit test for car speed parsing yet

---

### Summary of Key Architectural Observations

1. **Speed data is not cached** -- Unlike voltage/backlight/line-detect which go into `VehicleSnapshot`, speed is broadcast as a pure event. Apps must listen for `onCarSpeed` callbacks and maintain their own state.

2. **Two key event paths** -- The dedicated `KeyFeature` (MsgType 0x02) carries both `keyValue` and `keyStatus` (2 bytes). The `VehicleFeature` also handles a legacy `M2A_KEY_EVENT` (cmd 0x06) which carries only 1 raw byte. Apps should prefer the `KeyListener` / `McuKeyClient` API for steering wheel events.

3. **The vehicle-demo** app is the closest template for a key+vehicle test app. It already includes full key-sending capability but does not yet display car speed.

4. **All A2M commands** go through `McuBinder` -> `McuLink.sendAndWaitAck()` which constructs a `[msgType, cmd, body...]` payload and sends it as a DATA frame (FT=0x06) over UART, then blocks waiting for an ACK.

Prompt:
I need to understand how steering wheel key events and vehicle speed data are handled in this project at /Users/admin/workspace/jyq/qsjnic. Please search:

1. Find any Java/Kotlin files that handle KEY or key events (search for "KeyFeature", "keyFeature", "key_type", "SWC", "KEYMGR")
2. Find any Java/Kotlin files that handle vehicle speed or PublicDebug (search for "VehicleFeature", "vehicleFeature", "speed", "carSpeed")
3. Find the MsgType constants to understand what message types exist (KEY, VEHICLE, etc.)
4. Look for existing AIDL interfaces for key events and vehicle data
5. Check if there's an existing demo app for key/vehicle testing

Report file paths, class names, and the data flow from MCU frame → Java → listener callback. Be thorough.


crw------- 1 root      root         4,  64 1970-01-01 08:00 /dev/ttyS0
crw-rw---- 1 bluetooth net_bt_admin 4,  65 2026-05-26 18:33 /dev/ttyS1
crw------- 1 root      root         4,  74 1970-01-01 08:00 /dev/ttyS10
crw------- 1 root      root         4,  66 1970-01-01 08:00 /dev/ttyS2
crw------- 1 root      root         4,  67 1970-01-01 08:00 /dev/ttyS3
crw------- 1 root      root         4,  68 1970-01-01 08:00 /dev/ttyS4
crw------- 1 root      root         4,  69 2026-05-26 18:02 /dev/ttyS5
crw------- 1 root      root         4,  70 1970-01-01 08:00 /dev/ttyS6
crw-rw-rw- 1 root      system       4,  71 2026-05-26 18:33 /dev/ttyS7
crw------- 1 root      root         4,  72 1970-01-01 08:00 /dev/ttyS8
crw------- 1 root      root         4,  73 1970-01-01 08:00 /dev/ttyS9
---
crw-rw-rw- 1 root      root           5,   0 1970-01-01 08:00 /dev/tty
crw------- 1 root      root           4,  64 1970-01-01 08:00 /dev/ttyS0
crw-rw---- 1 bluetooth net_bt_admin   4,  65 2026-05-26 18:33 /dev/ttyS1
crw------- 1 root      root           4,  74 1970-01-01 08:00 /dev/ttyS10
crw------- 1 root      root           4,  66 1970-01-01 08:00 /dev/ttyS2
crw------- 1 root      root           4,  67 1970-01-01 08:00 /dev/ttyS3
crw------- 1 root      root           4,  68 1970-01-01 08:00 /dev/ttyS4
crw------- 1 root      root           4,  69 2026-05-26 18:02 /dev/ttyS5
crw------- 1 root      root           4,  70 1970-01-01 08:00 /dev/ttyS6
crw-rw-rw- 1 root      system         4,  71 2026-05-26 18:33 /dev/ttyS7
crw------- 1 root      root           4,  72 1970-01-01 08:00 /dev/ttyS8
crw------- 1 root      root           4,  73 1970-01-01 08:00 /dev/ttyS9
crw------- 1 root      root         236,   0 1970-01-01 08:00 /dev/ttynull


Now I have a comprehensive understanding. Let me analyze the situation:

1. **Key events** are handled by `KeyFeature` (msgType=0x02) → `IMcuKeyListener.onKeyEvent(keyValue, keyStatus)` → SDK `KeyListener`
2. **Vehicle speed** is handled by `VehicleFeature` (msgType=0x01, cmd=0x0A) → `IMcuVehicleListener.onCarSpeed(speedKmh)` → SDK `VehicleListener`
3. The existing **vehicle-demo** already has key sending capabilities but doesn't display speed

Now, about the serial test:
- `/dev/ttyS7` is the MCU-Android communication port (460800 baud)
- The KeyTest and PublicDebug commands from the document are MCU-side shell commands
- To send those commands, you need to connect to the MCU's debug serial port directly

However, the user's Mac is connected to the Android device via ADB (TCP). The Android device has a serial connection to the MCU via `/dev/ttyS7`. But the MCU's debug shell is likely on a different UART (perhaps `/dev/ttyS5` or another port).

Actually, wait. Let me reconsider. The document says "接上MCU打印" (connect to MCU debug output). The KeyTest and PublicDebug are MCU-side commands. When you type `KeyTest 1 0x19 0x01` on the MCU shell, it:
1. Prints a log message
2. Sends the corresponding frame over the MCU-Android UART link

So the MCU has a debug serial port that accepts shell commands. This is separate from the MCU-Android protocol link.

To use picocom/minicom, the user would:
1. Connect a USB-to-serial adapter to their Mac
2. Connect the adapter to the MCU's debug UART pins
3. Use picocom/minicom to access the MCU shell
4. Type KeyTest/PublicDebug commands

But the user is asking me to do this from the terminal. Since we're connected via ADB, let me think about alternatives:

**Alternative 1**: Write raw data to `/dev/ttyS7` from Android shell to simulate MCU frames. This won't work because `/dev/ttyS7` is already opened by the MCU service.

**Alternative 2**: Use the Android-to-MCU direction. The `KeyFeature` has `A2M_KEY = 0x81` which sends key events FROM Android TO MCU. But that's the wrong direction - we want to simulate MCU sending to Android.

**Alternative 3**: Write test frames directly to the MCU link. But the serial port is already opened exclusively by the MCU service.

**Alternative 4**: Instead of going through serial, inject test data at a higher level in Android. For example:
- Write a shell script that calls `am broadcast` or `service call` to trigger the MCU service
- Or create a test mode in the demo app that generates fake frame data

Actually, the simplest approach is to add a test input panel to the demo app itself. This way:
- We can simulate key events by calling the SDK's `sendKeyEvent` / `postKeyEvent` methods (which sends A2M commands)
- But wait, that's Android→MCU direction. We want MCU→Android.

Let me reconsider. The user wants to:
1. Send test commands (KeyTest/PublicDebug) from terminal
2. See if Android receives the corresponding frames

For this to work, they need to access the MCU's debug shell. Let me check if there's a way to do this via ADB.

Actually, looking at the serial ports on the device:
- `/dev/ttyS5` - also has a timestamp (2026-05-26 18:02), might be the MCU debug port
- `/dev/ttyS7` - the MCU-Android protocol link

Maybe we can use picocom from within Android (if installed) or from the Mac by forwarding the serial port over ADB.

Actually, the best approach for the user's scenario is:
1. **Optimize the demo app** to also show key events and vehicle speed (combine radio-demo + vehicle-demo features)
2. **For the serial test**: Since we can't easily connect to the MCU debug port remotely, we should add a "test injection" feature to the demo app that can simulate receiving these frames

But wait, let me re-read the user's message: "你能否优化测试demo，然后我们终端中输入测试命令，然后安卓车机的demo里面看看能否收到数据，或者你先用串口工具结合文档的测试，先看看返回值是什么"

The user wants TWO things:
a) Optimize the test demo to display key/vehicle data
b) Try using serial tools (picocom/minicom) to send test commands and see the return values

For (b), we need to figure out which serial port to connect to. The MCU debug console is likely on a different UART. Let me check if we can determine which port.

Actually, looking at this more practically:
- The Android device is at 192.168.0.66 (network-connected)
- The MCU debug port would be a physical UART on the board
- To access it from the Mac, we'd need a physical serial connection, OR we could use ADB to proxy

Let me check if we can read from any of the serial ports via ADB to find the MCU debug console.

Actually, the most practical approach given the constraints:
1. First, let me create a combined demo app that shows radio + key events + vehicle speed
2. For the serial testing, let me check if we can open a serial connection to the MCU from the Android side

Wait, I just realized something. The document mentions "接上MCU打印，shell命令行输入" - this means connecting to the MCU's debug UART and typing commands in the MCU's shell. The MCU is a separate microcontroller. We can't access its shell from Android.

But we CAN verify the Android side is working by:
1. Checking if the MCU is already sending key events (check logcat for KeyFeature logs)
2. If the MCU service is running and frames are flowing, when someone presses a steering wheel button, we should see the events

Let me check if there are any key events in the current logcat.

For now, let me focus on the practical next steps:
1. Check if key events are being received in logcat
2. Optimize the radio-demo to also show key events and vehicle speed (add KeyListener and VehicleListener)
3. For serial testing - explain to the user how to connect

Let me also think about whether the demo should be extended to include a "Simulate" feature where you can type hex frames directly in the app to simulate MCU data. That would be very useful for testing without needing the MCU.

Actually, looking at the SDK architecture, there's no way to inject raw frames from the app side. The frames come from the MCU service which gets them from JNI/serial. But we could potentially use `postCommand` or `sendCommand` to send A2M commands.

Wait, but that's the wrong direction. We want to simulate M2A (MCU to Android) frames.

Let me think of another approach. I could add a test mode to the service that allows injecting frames. But that's a bigger change.

For now, the most useful thing I can do:
1. Add KeyListener and VehicleListener to the radio-demo (making it a comprehensive test tool)
2. Check if we can access the MCU debug shell via any Android serial port
3. Try to use picocom from the Mac to connect to the MCU if there's a USB-serial adapter

Let me first check if there's any existing key event traffic in the logcat, and also check what ports are available.

Actually, I think the best plan is:
1. Read the vehicle-demo MainActivity to understand the key sending code
2. Extend our radio-demo to also include key and vehicle data display (and key sending)
3. Try to determine the MCU debug serial port
4. If possible, connect picocom to send test commands

Let me also look at the FrameRouter to see the handleKey() logging.


Good findings. Now let me check what's happening in the current logcat for key/vehicle events, and read the vehicle-demo to understand the existing key UI pattern.


adb -s 192.168.0.66:5555 logcat -d 2>&1 | grep -iE "KeyFeature|onKeyEvent|CAR_SPEED|onCarSpeed|key_type|VehicleFeature|handleKey" | tail -20

Check logcat for key events and vehicle speed


串口波特兰是921600


05-26 18:03:07.673  1126  1210 I CAR.INPUT: onKeyEvent:aready on