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


Prompt: 串口波特兰是921600


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


The user specified the serial port baud rate is 921600. This is critical info.

Now let me plan the approach. The current situation:

1. The MCU debug serial port is separate from the MCU-Android protocol link (/dev/ttyS7)
2. The MCU debug port accepts shell commands like `KeyTest 1 0x19 0x01` and `PublicDebug 23 60 100`
3. To access it, we need to figure out which serial port on the Android device connects to the MCU debug console, OR we need a physical serial connection

Wait, actually, let me reconsider. Maybe the MCU debug shell is accessible through the same serial port (/dev/ttyS7 at 921600). The MCU might multiplex both the protocol frames AND the debug shell on the same UART. Or maybe there's a separate debug port.

Actually, looking at this more carefully:
- `/dev/ttyS7` is used by the MCU service at 460800 baud for the protocol link
- The user says the serial baud rate is 921600

So maybe the MCU has a separate debug UART at 921600 baud. Let me check if there are other tty devices or USB-serial adapters.

Actually, wait. The user is on a Mac and has picocom/minicom installed. For them to use picocom, they need a physical serial connection. Maybe they have a USB-to-serial adapter connected to their Mac, and the other end is connected to the MCU debug port on the car board.

Let me check if there are any USB serial devices on the user's Mac.

Actually, let me take a step back. The user said "串口波特兰是921600" (serial baud rate is 921600). This is likely the baud rate for the MCU debug console.

For the practical approach, let me:
1. Check what USB serial devices exist on the Mac
2. Try to connect to the MCU debug console via picocom
3. Meanwhile, extend the demo to include key + speed display

Let me check for USB serial devices first.


串口波特率 921600，收到。让我先检查 Mac 上的串口设备，然后同时优化 demo。


Bash

ls -la /dev/cu.* /dev/tty.* 2>/dev/null | grep -ivE "bluetooth|wlan|airport|brain\.|debug|iphon|pcm|mfi|ur"> /dev/null; ls /dev/cu.* 2>/dev/null; echo "---"; ls /dev/tty.usb* 2>/dev/null; echo "---USB check---"; system_profiler SPUSBDataType 2>/dev/null | grep -A5 "Serial\|UART\|CP210\|CH340\|FTDI\|PL2303" | head -30

Check for USB serial devices on Mac


adb -s 192.168.0.66:5555 logcat -d 2>&1 | grep -iE "speed|Speed|kmh|Kmh|car.speed|SPEED|车速" | tail -10

Check if any speed data is flowing in logcat


/dev/cu.Bluetooth-Incoming-Port
/dev/cu.debug-console
/dev/cu.usbmodem21302
---
/dev/tty.usbmodem21302
---USB check---


05-26 18:03:01.755  1095  1433 W HvacController: registerHvacPropertyEventListeners - propertyId: + HVAC_FAN_SPEED is not implemented. Skipping registering callback.
05-26 18:03:01.760  1095  1433 W HvacController: registerHvacPropertyEventListeners - propertyId: + HVAC_ACTUAL_FAN_SPEED_RPM is not implemented. Skipping registering callback.
05-26 18:06:18.126   882  1015 D ConnectivityService: registerNetworkAgent NetworkAgentInfo{network{100}  handle{432902426637}  ni{WIFI CONNECTING extra: } created=2026-05-26T10:06:18.125Z Score(Policies : IS_UNMETERED&EVER_USER_SELECTED ; KeepConnected : 0)  explicitlySelected  lp{{LinkAddresses: [ ] DnsAddresses: [ ] Domains: null MTU: 0 Routes: [ ]}}  nc{[ Transports: WIFI Capabilities: NOT_METERED&INTERNET&NOT_RESTRICTED&TRUSTED&NOT_VPN&NOT_ROAMING&NOT_CONGESTED&NOT_SUSPENDED&NOT_VCN_MANAGED LinkUpBandwidth>=433000Kbps LinkDnBandwidth>=433000Kbps Specifier: <WifiNetworkAgentSpecifier [WifiConfiguration=, SSID="QS_5G", BSSID=74:45:2d:21:12:34, band=2, mMatchLocalOnlySpecifiers=false]> TransportInfo: <SSID: "QS_5G", BSSID: 74:45:2d:21:12:34, MAC: dc:0d:30:06:3f:a3, IP: null, Security type: 2, Supplicant state: GROUP_HANDSHAKE, Wi-Fi standard: 5, RSSI: -127, Link speed: -1Mbps, Tx Link speed: -1Mbps, Max Supported Tx Link speed: 433Mbps, Rx Link speed: -1Mbps, Max Supported Rx Link speed: 433Mbps, Frequency: 5745MHz, Net ID: 0, Metered hint: false, score: 0, isUsable: true, CarrierMerged: false, SubscriptionId: -1, IsPrimary: 1, Trusted: true, Restricted: false, Ephemeral: false, OEM paid: false, OEM private: false, OSU AP: false, FQDN: <none>, Provider friendly name: <none>, Requesting package name: <none>"QS_5G"wpa2-pskMLO Information: , Is TID-To-Link negotiation supported by the AP: false, AP MLD Address: <none>, AP MLO Link Id: <none>, AP MLO Affiliated links: <none>> AllowedUids: <{1001000}> OwnerUid: 1001000 AdminUids: [1001000] SSID: "QS_5G" UnderlyingNetworks: Null]}  factorySerialNumber=7}
05-26 18:06:18.359   882  1015 W WifiScoreReport: Cannot start external scoring netId=100 currentNetId=100 currentNetCapabilities=[ Transports: WIFI Capabilities: NOT_METERED&INTERNET&NOT_RESTRICTED&TRUSTED&NOT_VPN&NOT_ROAMING&NOT_CONGESTED&NOT_SUSPENDED&NOT_VCN_MANAGED LinkUpBandwidth>=12000Kbps LinkDnBandwidth>=30000Kbps Specifier: <WifiNetworkAgentSpecifier [WifiConfiguration=, SSID="QS_5G", BSSID=74:45:2d:21:12:34, band=2, mMatchLocalOnlySpecifiers=false]> TransportInfo: <SSID: "QS_5G", BSSID: 74:45:2d:21:12:34, MAC: dc:0d:30:06:3f:a3, IP: /192.168.0.66, Security type: 2, Supplicant state: COMPLETED, Wi-Fi standard: 5, RSSI: -72, Link speed: 1Mbps, Tx Link speed: 1Mbps, Max Supported Tx Link speed: 433Mbps, Rx Link speed: 58Mbps, Max Supported Rx Link speed: 433Mbps, Frequency: 5745MHz, Net ID: 0, Metered hint: false, score: 58, isUsable: true, CarrierMerged: false, SubscriptionId: -1, IsPrimary: 1, Trusted: true, Restricted: false, Ephemeral: false, OEM paid: false, OEM private: false, OSU AP: false, FQDN: <none>, Provider friendly name: <none>, Requesting package name: <none>"QS_5G"wpa2-pskMLO Information: , Is TID-To-Link negotiation supported by the AP: false, AP MLD Address: <none>, AP MLO Link Id: <none>, AP MLO Affiliated links: <none>> SignalStrength: -72 AllowedUids: <{1001000}> OwnerUid: 1001000 AdminUids: [1001000] SSID: "QS_5G" UnderlyingNetworks: Null] sessionId=1008
05-26 18:06:18.388   882  1023 D ConnectivityService: Switching to new default network for: uid/pid:1000/882 activeRequest: 1 callbackRequest: 1 [NetworkRequest [ REQUEST id=1, [ Capabilities: INTERNET&NOT_RESTRICTED&TRUSTED&NOT_VPN&NOT_VCN_MANAGED RequestorUid: 1000 RequestorPkg: android UnderlyingNetworks: Null] ]] callback flags: 1 order: 2147483647 using NetworkAgentInfo{network{100}  handle{432902426637}  ni{WIFI CONNECTED extra: } created=2026-05-26T10:06:18.125Z Score(Policies : TRANSPORT_PRIMARY&IS_UNMETERED&EVER_USER_SELECTED ; KeepConnected : 0)  created 216842 explicitlySelected  lp{{InterfaceName: wlan0 LinkAddresses: [ 192.168.0.66/24 ] DnsAddresses: [ /192.168.0.1 ] Domains: null MTU: 0 ServerAddress: /192.168.0.1 TcpBufferSizes: 524288,1048576,2097152,262144,524288,1048576 Routes: [ fe80::/64 -> :: wlan0 mtu 0,192.168.0.0/24 -> 0.0.0.0 wlan0 mtu 0,0.0.0.0/0 -> 192.168.0.1 wlan0 mtu 0 ]}}  nc{[ Transports: WIFI Capabilities: NOT_METERED&INTERNET&NOT_RESTRICTED&TRUSTED&NOT_VPN&NOT_ROAMING&FOREGROUND&NOT_CONGESTED&NOT_SUSPENDED&NOT_VCN_MANAGED LinkUpBandwidth>=12000Kbps LinkDnBandwidth>=30000Kbps Specifier: <WifiNetworkAgentSpecifier [WifiConfiguration=, SSID="QS_5G", BSSID=74:45:2d:21:12:34, band=2, mMatchLocalOnlySpecifiers=false]> TransportInfo: <SSID: "QS_5G", BSSID: 74:45:2d:21:12:34, MAC: dc:0d:30:06:3f:a3, IP: /192.168.0.66, Security type: 2, Supplicant state: COMPLETED, Wi-Fi standard: 5, RSSI: -72, Link speed: 1Mbps, Tx Link speed: 1Mbps, Max Supported Tx Link speed: 433Mbps, Rx Link speed: 58Mbps, Max Supported Rx Link speed: 433Mbps, Frequency: 5745MHz, Net ID: 0, Metered hint: false, score: 58, isUsable: true, CarrierMerged: false, SubscriptionId: -1, IsPrimary: 1, Trusted: true, Restricted: false, Ephemeral: false, OEM paid: false, OEM private: false, OSU AP: false, FQDN: <none>, Provider friendly name: <none>, Requesting package name: <none>"QS_5G"wpa2-pskMLO Information: , Is TID-To-Link negotiation supported by the AP: false, AP MLD Address: <none>, AP MLO Link Id: <none>, AP MLO Affiliated links: <none>> SignalStrength: -72 OwnerUid: 1001000 AdminUids: [1001000] SSID: "QS_5G" UnderlyingNetworks: Null]}  factorySerialNumber=7}
05-26 18:06:18.444   882  1049 D InstantWifi: Receive Wifi is connected, freq =  5745 and currentNetworkId : 0, wifiinfo = SSID: <unknown ssid>, BSSID: 02:00:00:00:00:00, MAC: 02:00:00:00:00:00, IP: /192.168.0.66, Security type: 2, Supplicant state: COMPLETED, Wi-Fi standard: 5, RSSI: -72, Link speed: 1Mbps, Tx Link speed: 1Mbps, Max Supported Tx Link speed: 433Mbps, Rx Link speed: 58Mbps, Max Supported Rx Link speed: 433Mbps, Frequency: 5745MHz, Net ID: -1, Metered hint: false, score: 58, isUsable: true, CarrierMerged: false, SubscriptionId: -1, IsPrimary: 1, Trusted: true, Restricted: false, Ephemeral: false, OEM paid: false, OEM private: false, OSU AP: false, FQDN: <none>, Provider friendly name: <none>, Requesting package name: <none><none>MLO Information: , Is TID-To-Link negotiation supported by the AP: false, AP MLD Address: <none>, AP MLO Link Id: <none>, AP MLO Affiliated links: <none>
05-26 18:06:21.194   882  1049 D InstantWifi: Receive Wifi is connected, freq =  5745 and currentNetworkId : 0, wifiinfo = SSID: <unknown ssid>, BSSID: 02:00:00:00:00:00, MAC: 02:00:00:00:00:00, IP: /192.168.0.66, Security type: 2, Supplicant state: COMPLETED, Wi-Fi standard: 5, RSSI: -73, Link speed: 175Mbps, Tx Link speed: 175Mbps, Max Supported Tx Link speed: 433Mbps, Rx Link speed: 117Mbps, Max Supported Rx Link speed: 433Mbps, Frequency: 5745MHz, Net ID: -1, Metered hint: false, score: 58, isUsable: true, CarrierMerged: false, SubscriptionId: -1, IsPrimary: 1, Trusted: true, Restricted: false, Ephemeral: false, OEM paid: false, OEM private: false, OSU AP: false, FQDN: <none>, Provider friendly name: <none>, Requesting package name: <none><none>MLO Information: , Is TID-To-Link negotiation supported by the AP: false, AP MLD Address: <none>, AP MLO Link Id: <none>, AP MLO Affiliated links: <none>
05-26 18:06:31.414   882  1049 D InstantWifi: Receive Wifi is connected, freq =  5745 and currentNetworkId : 0, wifiinfo = SSID: <unknown ssid>, BSSID: 02:00:00:00:00:00, MAC: 02:00:00:00:00:00, IP: /192.168.0.66, Security type: 2, Supplicant state: COMPLETED, Wi-Fi standard: 5, RSSI: -73, Link speed: 175Mbps, Tx Link speed: 175Mbps, Max Supported Tx Link speed: 433Mbps, Rx Link speed: 117Mbps, Max Supported Rx Link speed: 433Mbps, Frequency: 5745MHz, Net ID: -1, Metered hint: false, score: 58, isUsable: true, CarrierMerged: false, SubscriptionId: -1, IsPrimary: 1, Trusted: true, Restricted: false, Ephemeral: false, OEM paid: false, OEM private: false, OSU AP: false, FQDN: <none>, Provider friendly name: <none>, Requesting package name: <none><none>MLO Information: , Is TID-To-Link negotiation supported by the AP: false, AP MLD Address: <none>, AP MLO Link Id: <none>, AP MLO Affiliated links: <none>
05-26 18:13:52.543   882  1049 D InstantWifi: Receive Wifi is connected, freq =  5745 and currentNetworkId : 0, wifiinfo = SSID: <unknown ssid>, BSSID: 02:00:00:00:00:00, MAC: 02:00:00:00:00:00, IP: /192.168.0.66, Security type: 2, Supplicant state: COMPLETED, Wi-Fi standard: 5, RSSI: -78, Link speed: 87Mbps, Tx Link speed: 87Mbps, Max Supported Tx Link speed: 433Mbps, Rx Link speed: 175Mbps, Max Supported Rx Link speed: 433Mbps, Frequency: 5745MHz, Net ID: -1, Metered hint: false, score: 51, isUsable: true, CarrierMerged: false, SubscriptionId: -1, IsPrimary: 1, Trusted: true, Restricted: false, Ephemeral: false, OEM paid: false, OEM private: false, OSU AP: false, FQDN: <none>, Provider friendly name: <none>, Requesting package name: <none><none>MLO Information: , Is TID-To-Link negotiation supported by the AP: false, AP MLD Address: <none>, AP MLO Link Id: <none>, AP MLO Affiliated links: <none>
05-26 18:14:37.684   882  1049 D InstantWifi: Receive Wifi is connected, freq =  5745 and currentNetworkId : 0, wifiinfo = SSID: <unknown ssid>, BSSID: 02:00:00:00:00:00, MAC: 02:00:00:00:00:00, IP: /192.168.0.66, Security type: 2, Supplicant state: COMPLETED, Wi-Fi standard: 5, RSSI: -77, Link speed: 117Mbps, Tx Link speed: 117Mbps, Max Supported Tx Link speed: 433Mbps, Rx Link speed: 175Mbps, Max Supported Rx Link speed: 433Mbps, Frequency: 5745MHz, Net ID: -1, Metered hint: false, score: 51, isUsable: true, CarrierMerged: false, SubscriptionId: -1, IsPrimary: 1, Trusted: true, Restricted: false, Ephemeral: false, OEM paid: false, OEM private: false, OSU AP: false, FQDN: <none>, Provider friendly name: <none>, Requesting package name: <none><none>MLO Information: , Is TID-To-Link negotiation supported by the AP: false, AP MLD Address: <none>, AP MLO Link Id: <none>, AP MLO Affiliated links: <none>


There's a USB serial device `/dev/cu.usbmodem21302` on the Mac. This is likely the connection to the MCU debug port. The user says the baud rate is 921600.

Now I have a plan:
1. Use picocom to connect to `/dev/cu.usbmodem21302` at 921600 baud
2. Send test commands (KeyTest, PublicDebug)
3. Watch logcat for the results on Android

But first, let me also:
- Extend the radio-demo to include key + vehicle speed display (so we can see results in the app)

Let me first try connecting to the MCU with picocom and sending a test command, while simultaneously checking logcat.

Actually, let me plan the demo extension:

Current radio-demo shows: Radio data + raw frame hex
Need to add:
1. **Key Events** section - show last key event (from KeyListener)
2. **Vehicle Speed** section - show car speed (from VehicleListener.onCarSpeed)
3. **Raw frame hex for KEY and PUBLIC msgTypes** - already partially covered by the existing frame monitor (but currently only filters msgType=0x05)

Let me extend the existing frame monitor to also show KEY (0x02) and PUBLIC (0x01) frames.

For the raw frame listener, I need to update it to handle msgType 0x01 (PUBLIC) and 0x02 (KEY) in addition to 0x05 (RADIO).

Let me also add:
- KeyListener to capture key events
- VehicleListener to capture speed data

Actually, let me first focus on the serial test. Let me try to connect via picocom to the MCU debug port and send a command. Then I'll extend the demo.

Wait, I should do both in parallel:
1. Extend the demo (code change)
2. Try picocom connection (terminal)

Let me write the extended demo code first, then try picocom.

The changes needed in MainActivity.java:
1. Import KeyListener and VehicleListener
2. Add key event and speed display fields
3. Register key and vehicle listeners
4. Update frame monitor to show KEY and PUBLIC frames too
5. Add speed and key display sections to UI

Let me plan the exact UI additions:

After the Frame Monitor card, add:

```
┌─ Key & Speed ──────────────────────────┐
│  Last Key: 0x19 status=0x01 (PRESS)    │
│  Speed: 60 km/h                        │
└────────────────────────────────────────┘
```

Actually, I can merge this into the existing Frame Monitor card since it's all debug data. But for clarity, let me keep it separate.

Wait, I should be careful not to make the demo too cluttered. Let me think about a clean layout:

Current layout:
1. Title + Status
2. Frequency Card
3. **Frame Monitor** (valid, PTY, TA, TP, PS, RT, hex)
4. Tuner Audio
5. Band / Frequency / Seek / Other controls

I'll add:
5. (before Band) → **Key & Speed Monitor** card with key events and speed
6. Also update raw frame hex to show KEY and PUBLIC frames

Actually, let me put the key and speed info in the Frame Monitor card since it's all debugging data. Let me add rows for key event and speed.

Or better yet, since the Frame Monitor is getting long, let me rename it to "Data Monitor" and add key/speed sections. Or keep "Frame Monitor" and add a separate compact card for key/speed.

I think a separate compact card is cleaner. Let me add:

```
┌─ SWC & Speed ──────────────────────────┐
│  Key: 0x19 REJECT PRESS                │
│  Speed: 60 km/h                        │
│  KEY hex: ...                          │
│  PUBLIC hex: ...                       │
└────────────────────────────────────────┘
```

Actually, let me just add key and speed as simple rows within the Frame Monitor card to keep it all in one place. That's more practical for quick debugging.

Let me redesign Frame Monitor to include:
- Row 1: valid, PTY, TA, TP (existing)
- Row 2: Work status, Tips (existing)
- Row 3: PS (existing)
- Row 4: RT (existing)
- Row 5: Key: -- | Speed: -- (NEW)
- Divider
- Hex: FREQ, PS/PTY, RT, KEY, SPEED (extend existing)

This is the cleanest approach. Let me implement it.

For the raw frame listener, I need to extend beyond just RADIO (0x05):
- msgType 0x01 (PUBLIC): cmd 0x0A = CAR_SPEED → show hex
- msgType 0x02 (KEY): cmd 0x01 = M2A_KEY → show hex

Let me now code the changes.

Actually, I think it's more urgent to first try the picocom connection to see if we can communicate with the MCU. The demo extension can happen in parallel. Let me:

1. Start picocom on /dev/cu.usbmodem21302 at 921600
2. Try to send a command
3. Meanwhile prepare the demo code

But actually, the user asked me to first check the serial tool. Let me do the picocom test first, then extend the demo.

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

They want:
Option 1: Optimize demo → send test commands from terminal → see if Android receives data
Option 2: First use serial tool with the test doc → see return values

Let me do both. First try the serial tool approach to verify MCU communication, then optimize the demo.

Let me try to connect with picocom.


Mac 上检测到 USB 串口设备 `/dev/cu.usbmodem21302`。让我先用 picocom 连接 MCU 调试串口，同时扩展 demo。


which picocom && picocom --version 2>&1 || echo "picocom not found"

Check picocom availability


which minicom && minicom --version 2>&1 || echo "minicom not found"

Check minicom availability


/opt/homebrew/bin/picocom
Unrecognized option(s)
Run with '--help'.
picocom not found


/opt/homebrew/bin/minicom
minicom version 2.10 (compiled Feb 22 2025)
Copyright (C) Miquel van Smoorenburg.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version
2 of the License, or (at your option) any later version.


Good, both are available. picocom has a help flag issue, let me try with --help. Actually, let me just use picocom.

Let me try connecting with picocom to the MCU debug port at 921600 baud. But picocom is an interactive tool - I can't easily script it. Let me use a different approach.

Actually, I can use a script to send commands via the serial port and capture output. Let me try:

1. Open the serial port
2. Send a command
3. Read the response

Or I can use `screen` or a scripted approach.

Actually, let me think about this differently. The user wants to:
1. Connect to MCU debug serial
2. Type `KeyTest 1 0x19 0x01`
3. See the MCU's response on the serial console
4. Then check if Android received the frame

For this, I need an interactive session. But I'm in a non-interactive environment. Let me try a different approach:

1. Use a Python script to open the serial port, send a command, and read the response
2. Or use `echo` to write to the serial device and `cat` to read

Actually, the best approach for non-interactive use is to write a quick script. But let me first try a simple approach:

```bash
# Set up serial port
stty -f /dev/cu.usbmodem21302 921600 cs8 -cstopb -parenb

# Send a command and read response in background
cat /dev/cu.usbmodem21302 &
PID=$!
echo -e "KeyTest 1 0x19 0x01\r" > /dev/cu.usbmodem21302
sleep 2
kill $PID
```

Wait, this is risky because:
1. The MCU might be continuously outputting data
2. The `cat` might capture a lot of data
3. We need to send the right line ending (\r or \n or \r\n)

Let me try a more controlled approach. First, let me check if the serial port is responsive by trying to read some data from it.

Actually, let me use a Python script since it gives us more control. But before that, let me just check if the port is alive.

Actually, let me think about what approach would work best. The MCU debug console probably:
1. Outputs a prompt (like "xiexw:/$")
2. Accepts commands terminated by \r or \n
3. Echoes back the command and then outputs the result

Let me write a quick Python script to interact with the serial port.

Wait, I should also consider: does the user have Python available? Let me check.

Actually, let me try a simpler approach first. Let me use `stty` and shell redirection.

But actually, the most reliable approach for this kind of testing is to use a small Python script. Let me check if Python is available and write a script.

But wait, maybe I should first try a simpler test - just see if we can read any data from the serial port.

Let me try:
```
timeout 5 cat /dev/cu.usbmodem21302
```

Or with `dd`:
```
dd if=/dev/cu.usbmodem21302 bs=1 count=100 2>/dev/null
```

Actually, let me just go ahead and write a simple script to interact with the serial port. Let me use a Bash approach with background processes.

Actually, the cleanest approach is to write a small Python script that:
1. Opens the serial port at 921600
2. Sends "KeyTest 1 0x19 0x01\r\n"
3. Reads response for a few seconds
4. Prints output

Let me check if Python is available.


python3 --version 2>&1; pip3 list 2>/dev/null | grep -i pyserial

Check Python and pyserial availability


Python 3.13.2


Python 3 is available but pyserial is NOT installed. Let me install it or use a different approach.

I can:
1. Install pyserial: `pip3 install pyserial`
2. Or use `screen` command
3. Or use raw shell with `stty`

Let me try the `stty` approach first since it doesn't require installing anything. Or let me just install pyserial since it's the cleanest approach.

Actually, let me try using `screen` first since it's built-in on macOS:

```
screen /dev/cu.usbmodem21302 921600
```

But that's interactive. I need to script it.

Let me try a Python script without pyserial, using the `termios` module (built-in on macOS). Actually, pyserial would be much cleaner. Let me install it.

Wait, I shouldn't install packages without asking the user. Let me try with built-in tools first.

Actually, let me just try with shell redirection and `stty`. Here's the approach:

```bash
# Configure serial port
stty -f /dev/cu.usbmodem21302 921600 cs8 -cstopb -parenb -icanon -echo

# Send command
printf "KeyTest 1 0x19 0x01\r\n" > /dev/cu.usbmodem21302

# Read response (timeout after 3 seconds)
timeout 3 cat /dev/cu.usbmodem21302
```

But `timeout` might not be available on macOS. Let me try `gtimeout` from coreutils or use a different approach.

Actually, macOS doesn't have `timeout`. Let me use `perl -e 'alarm 3; ...'` or just use a background process.

Let me try this approach:
```bash
# Configure
stty -f /dev/cu.usbmodem21302 921600 cs8 -cstopb -parenb

# Start reading in background
cat /dev/cu.usbmodem21302 > /tmp/mcu_output.txt 2>&1 &
CATPID=$!

# Send command
printf "KeyTest 1 0x19 0x01\r\n" > /dev/cu.usbmodem21302

# Wait a bit
sleep 3

# Kill the reader
kill $CATPID 2>/dev/null

# Show output
cat /tmp/mcu_output.txt
```

This should work. Let me try it. But actually, there's a risk. The serial port might already be in use (by the MCU service on Android? No, this is a USB serial connected to the Mac, so it's a different physical connection).

Wait, actually `/dev/cu.usbmodem21302` is on the Mac. This USB-serial adapter is connected to what? It should be connected to the MCU's debug UART on the car board. So this is the right port to use.

Let me try the shell approach. But I realize there might be an issue with permissions. Let me check.

Actually, let me just do it.


Check serial port permissions


crw-rw-rw-  1 root  wheel  0x9000003 May 29 15:51 /dev/cu.usbmodem21302


Good, permissions are open (rw-rw-rw-). Let me try to communicate with the MCU.

Let me set up the serial port, send a command, and read the response.

The baud rate is 921600 as the user specified.

Let me try sending "KeyTest 1 0x19 0x01" and see the response.


权限没问题。让我尝试通过串口与 MCU 通信。


stty -f /dev/cu.usbmodem21302 921600 cs8 -cstopb -parenb -icanon min 0 time 5 2>&1; echo "port configured"; cat /dev/cu.usbmodem21302 > /tmp/mcu_read.txt 2>&1 & CATPID=$!; sleep 0.5; printf "KeyTest 1 0x19 0x01\r\n" > /dev/cu.usbmodem21302; echo "command sent"; sleep 3; kill $CATPID 2>/dev/null; wait $CATPID 2>/dev/null; echo "---MCU OUTPUT---"; cat /tmp/mcu_read.txt; echo "---END---"

Try to communicate with MCU via serial port


port configured
command sent
---MCU OUTPUT---
---END---


No output from the MCU. This could mean:
1. The MCU debug port is not on this USB serial device
2. The baud rate is wrong
3. The connection isn't properly established
4. The MCU uses hardware flow control

Let me try different approaches:
1. Try different line endings (\r vs \n vs \r\n)
2. Try reading without sending anything first (maybe the MCU is outputting logs continuously)
3. Try different baud rates
4. Check if there's any output at all on the port

Let me first check if there's any continuous output from the MCU (like boot logs or prompt).


MCU 没有返回数据。让我检查是否有持续的日志输出，并尝试不同的参数。


# First check if the MCU is outputting anything at all
stty -f /dev/cu.usbmodem21302 921600 cs8 -cstopb -parenb -icanon min 0 time 5 2>&1
cat /dev/cu.usbmodem21302 > /tmp/mcu_raw.txt 2>&1 &
CATPID=$!
sleep 5
kill $CATPID 2>/dev/null; wait $CATPID 2>/dev/null
echo "---raw bytes---"
wc -c /tmp/mcu_raw.txt
xxd /tmp/mcu_raw.txt | head -20
echo "---text---"
cat -v /tmp/mcu_raw.txt | head -20

Read raw data from serial port to check for any MCU output


---raw bytes---
       0 /tmp/mcu_raw.txt
---text---


Zero bytes received in 5 seconds. This means either:
1. The MCU debug port is not this device
2. The baud rate doesn't match
3. Hardware flow control is needed (RTS/CTS)
4. The MCU only responds after receiving a valid command with proper handshake

Let me check a few things:
1. What USB devices are connected to the Mac
2. Try different serial settings (hardware flow control, different line endings)

Let me also check if the MCU on the car board might use a different serial port. The `/dev/cu.usbmodem21302` might be something else.

Let me try with hardware flow control enabled, or try different baud rates.

Actually wait - the user said the baud rate is 921600. Let me check by trying to send with different settings.

Actually, maybe the issue is that we need to send the command first and then read. Some MCUs only respond to input. Let me try sending a command and then immediately reading.


# Try with different approach: send then read, with hardware flow control off
stty -f /dev/cu.usbmodem21302 921600 cs8 -cstopb -parenb -icanon -echo -crtscts min 0 time 2 2>&1

# Try sending a few different commands
for cmd in "\r\n" "help\r\n" "?\r\n" "\r\nKeyTest\r\n"; do
  printf "%s" "$cmd" > /dev/cu.usbmodem21302
  sleep 0.3
done

# Now read
cat /dev/cu.usbmodem21302 > /tmp/mcu_raw2.txt 2>&1 &
CATPID=$!
sleep 3
kill $CATPID 2>/dev/null; wait $CATPID 2>/dev/null
echo "bytes: $(wc -c < /tmp/mcu_raw2.txt)"
cat -v /tmp/mcu_raw2.txt | head -30

Try sending commands then reading response


bytes:        0


Still no response. Let me check:
1. What USB device is this? Let me check the USB info
2. Maybe the port needs different settings
3. Maybe it's not connected to the MCU at all

Let me check the USB device info to understand what we're connected to.


system_profiler SPUSBDataType 2>/dev/null | grep -B2 -A15 "usbmodem21302\|CP210\|CH340\|FTDI\|PL230\|Serial\|UART\|ACM\|modem" | head -40

Check USB device details for the serial adapter


(Bash completed with no output)


The grep returned no output. Let me try a broader search to find what USB device `/dev/cu.usbmodem21302` is.


system_profiler SPUSBDataType 2>/dev/null | head -80

Get full USB device info


No USB output at all? That's strange. Maybe `system_profiler` needs more time or permissions. Let me try another approach.


ioreg -p IOUSB -l -w0 2>/dev/null | grep -iE "USB|Serial|CP210|CH340|FTDI|PL230|modem|ACM|Product|Vendor" | head -20; echo "---"; ls -la /dev/cu.* /dev/tty.usb* 2>/dev/null

Query IORegistry for USB serial device info


The output is too large. Let me search specifically for the USB serial device.


ioreg -p IOUSB -l -w0 2>/dev/null | grep -iE "usbmodem|CP210|CH340|FTDI|PL230|ACM|usbmodem21302|Product Name|Vendor Name" | head -20

Find USB serial device details


The ioreg output is too large. I can see "AppleUSBSerial" = 1 and "IOSerialBSDClient" = 3 which indicates there are serial devices. Let me try a different approach.

Actually, let me take a step back. The serial port `/dev/cu.usbmodem21302` exists. Let me just try different approaches:

1. Maybe the MCU is connected to the Android device's serial port, not a USB port on the Mac
2. Maybe I need to use ADB to forward the Android device's serial port to the Mac

Wait, actually `/dev/cu.usbmodem21302` is a local serial device on the Mac. It appears to be a USB-to-serial adapter. But maybe it's NOT connected to the MCU. Maybe it's just an unconnected adapter.

Let me think about this differently. The MCU debug port would be physically on the car board. The user would need a USB-to-serial adapter connected between their Mac and the MCU debug pins. The fact that `/dev/cu.usbmodem21302` exists just means a USB-serial chip is plugged into the Mac, but it might not be connected to anything on the other end.

Alternatively, maybe the user expects us to communicate with the MCU through the Android device. The Android device has `/dev/ttyS7` which is connected to the MCU. But that port is being used by the MCU service.

Wait, there's another approach. The user document mentions that the MCU shell commands like `KeyTest` and `PublicDebug` are typed ON the MCU. The MCU prints debug output on its debug UART. This debug UART might be accessible from the Android side through a different serial port.

Let me check if there's a way to access the MCU debug console from the Android device. Maybe we can use ADB to open a serial connection to one of the ttyS ports on the Android device.

Actually, the most practical approach now is:
1. Try to use ADB to access one of the serial ports on the Android device that might be the MCU debug console
2. Or, check if the MCU debug UART is accessible from the Android device

Let me check if there are other serial ports on the Android device that might be the MCU debug console.

Actually, look at the Android serial ports:
- `/dev/ttyS5` was also accessed on 2026-05-26 18:02 (same time as ttyS7)
- Maybe `/dev/ttyS5` is the MCU debug port?

Let me check the permissions and try to read from it.

But wait - the MCU service only opens `/dev/ttyS7`. If `/dev/ttyS5` is the MCU debug console, it might be accessible.

However, there's also a more straightforward approach. The document shows these commands run on the MCU's own shell (the MCU is running an RTOS or similar). The KeyTest command directly triggers the MCU to send the corresponding frame over the MCU-Android UART link. So:
1. KeyTest on MCU → MCU generates frame → frame goes to Android via ttyS7
2. The result is visible on the Android side via the MCU service

So maybe instead of trying to connect to the MCU debug port, I should:
1. Check if there's already key event traffic coming from the MCU (indicating the link is working)
2. If the link is working but there are no key events (because nobody is pressing steering wheel buttons), I can't test key events through the existing flow

Actually, wait. Let me re-read the document once more. It says:

"[2]. 模拟测试: 接上MCU打印，shell命令行输入 KeyTest + 测试项 + 按键值 + 按键状态"

"接上MCU打印" means "connect to MCU debug output". This is about connecting a PC to the MCU's debug UART (separate from the Android-MCU link). Through this debug console, you can type commands like `KeyTest` which instructs the MCU to:
1. Print a log message on the debug console
2. Send the corresponding protocol frame over the MCU-Android UART

So the flow is:
- PC (Mac) → USB-to-UART adapter → MCU debug UART → type "KeyTest 1 0x19 0x01"
- MCU receives command, sends frame over Android UART (ttyS7)
- Android receives frame, MCU service processes it, sends to app

The user has `/dev/cu.usbmodem21302` on their Mac. They want me to connect to it at 921600 and send test commands.

But I got no response. Let me try with different settings. Maybe the issue is:
1. RTS/CTS flow control is needed
2. The MCU uses a different line ending
3. The MCU needs a specific init sequence

Let me also try a different approach - see if we can access the MCU debug console from the Android device directly. Some designs route the MCU debug UART to a secondary serial port on the Android SoC.

Actually, let me try a few more things with the serial port:
1. Try with RTS/CTS
2. Try sending just a newline to wake up the console
3. Try different line endings

Also, let me try running a script that reads continuously while sending periodic commands.

Wait, I just realized something. Instead of spending more time debugging the serial connection, let me take the user's second approach: optimize the demo app to show key events and vehicle speed, then figure out the serial testing.

For the demo, the key changes are:
1. Add KeyListener → display key events in the demo
2. Add VehicleListener → display speed in the demo
3. Extend raw frame hex to also show KEY and PUBLIC frames

And for the serial test, I should try:
1. Reading from the serial port with various settings
2. Also try writing raw hex frames directly to the Android's MCU serial port (bypassing the normal protocol)

Actually, wait. I just had another idea. What if the USB-serial device on the Mac is actually the same physical UART that the Android device uses to talk to the MCU? In some test setups, the Mac is connected between the Android board and the MCU to monitor the traffic (like a bus monitor). In that case:
- The Mac would see all the traffic between Android and MCU
- But the Mac can't inject commands into the MCU debug shell (that's a separate UART)

The document specifically shows a separate debug console (printing "xiexw:/$" prompt). This is the MCU's own shell.

OK, let me take a pragmatic approach now:

1. Extend the demo to show key + speed data
2. Try to use the serial port more aggressively (maybe we need to send a specific wake-up sequence)
3. Also try the alternative: ADB forward from Android's serial port

Let me try one more thing with the serial port - maybe the MCU is waiting for a specific character or break signal.


让我换个思路，也许 MCU 的调试串口是通过 Android 设备端的另一个 UART 暴露的，同时我来扩展 demo。


adb -s 192.168.0.66:5555 shell "ls -la /dev/ttyS* 2>/dev/null; echo '---'; cat /proc/tty/driver/serial 2>/dev/null || cat /proc/tty/drivers 2>/dev/null"

Check Android serial ports status


Exit code 1
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:40 /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:40 /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
---


Let me now focus on extending the demo app. We can sort out the serial connection issue separately. The user's core goal is:
1. Extend the demo to display key events and speed data
2. Be able to send test commands from the terminal

For #2, since we can't easily access the MCU debug console, let me think of alternatives:
- We CAN access the Android device's shell via ADB
- The Android device has the MCU service running
- The MCU service listens on ttyS7 for protocol frames

What if we write raw test frames to ttyS7 from the Android side? But that port is already opened by the MCU service.

Another approach: what if the MCU service has a test/debug interface we can use?

Actually, looking at McuLink.java, there's a `sendAndWaitAck` method. The service sends frames TO the MCU. But we want to simulate frames FROM the MCU.

Let me take yet another approach. What if we modify the FrameRouter to have a test injection mode? We could add an ADB shell command or a broadcast receiver that allows injecting test frames directly into the FrameRouter.

But that would require modifying the service code, not just the demo.

Actually, the simplest approach for testing is:
1. Use the Android-to-MCU direction: send key events FROM Android TO MCU via the SDK's `sendKeyEvent` method
2. The MCU should echo or acknowledge these
3. But this doesn't test the M2A direction...

OK, I think the most practical approach right now is:
1. Extend the demo to show key + speed data (so when real MCU data arrives, we can see it)
2. The serial test via picocom can be done by the user manually (or I can provide the exact commands)

For the demo extension, let me add:
- KeyListener to show key events
- VehicleListener to show speed
- Extended raw frame hex for KEY and PUBLIC msgTypes
- Key test buttons (sending A2M key events)

Let me now implement the extended demo. I'll add:
1. KeyListener and VehicleListener imports
2. New display fields for key and speed
3. A "SWC & Speed" card
4. Key test buttons matching the SWC key values from the document (帕杰罗方控按键值)
5. Update the raw frame listener to show KEY (0x02) and PUBLIC (0x01) frames too

This makes the radio-demo a comprehensive MCU debug tool.


串口通信暂时无响应。让我先把重心放在扩展 demo 上——加入方控按键和车速显示，同时扩展原始帧 HEX 覆盖 KEY 和 PUBLIC 消息类型。


TaskCreate

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.


Task #4 created successfully: Add SWC key events and vehicle speed display to radio-demo


Let me now write the extended MainActivity.java. I need to add:

1. KeyListener - to receive and display key events
2. VehicleListener - to receive and display speed data
3. Extend raw frame listener for msgType 0x01 (PUBLIC) and 0x02 (KEY)
4. SWC key test buttons with Pajero key values
5. Speed display

Key mappings from the document:
```
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 // 摄像头
```

Key status:
- 0x00 = IDLE/RELEASE
- 0x01 = TRIGGER/SHORT_PRESS

For the raw frame hex display, I need to update the rawFrameListener to handle:
- msgType 0x01 (PUBLIC): cmd 0x0A = CAR_SPEED
- msgType 0x02 (KEY): cmd 0x01 = M2A_KEY

Let me write the complete updated file now. I'll add sections:
1. After Frame Monitor, add "SWC & Speed" card
2. SWC test buttons with Pajero key values
3. Speed display + test input

Actually, to keep it manageable, let me plan the exact UI layout:

After existing Frame Monitor card, add:

```
sectionTitle("SWC Keys")
card:
  Last Key: 0x19 REJECT PRESS
  [Vol-] [Vol+] [Prev] [Next] [Voice] [Music]
  [Answer] [Reject] [Camera]

sectionTitle("Speed Monitor")
card:
  Speed: -- km/h
  KEY hex: --
  SPEED hex: --
```

Wait, actually the KEY and SPEED hex should go in the existing Frame Monitor hex section. Let me update the raw frame listener instead.

Let me write the complete updated file. Given the extent of changes, I'll write the whole file.

For the KEY frame raw hex, I need to handle msgType=0x02 in the raw frame listener.
For the SPEED frame raw hex, I need to handle msgType=0x01, cmd=0x0A in the raw frame listener.

Let me restructure the Frame Monitor hex section to also show KEY and SPEED frames. And add new cards for SWC keys and speed.

Let me also think about the Pajero key values vs the McuKeyClient constants. The document shows different key values than what McuKeyClient defines:

Document (Pajero):
- 0x03 = SPEECH
- 0x04 = MUSIC
- 0x08 = VOLUME_DOWN
- 0x09 = VOLUME_UP
- 0x0E = PREV
- 0x0F = NEXT
- 0x18 = ANSWER
- 0x19 = REJECT
- 0x25 = CAMERA

McuKeyClient constants:
- 0x01 = MODE
- 0x02 = MUTE
- 0x03 = WORKMODE
- 0x04 = CALL
- 0x05 = VOICE_ASSISTANT
- 0x06 = HOME
- 0x07 = OK
- 0x08 = CHANNEL_UP
- 0x09 = CHANNEL_DOWN
- 0x0A = VOLUME_UP
- 0x0B = VOLUME_DOWN

These are different mappings! The McuKeyClient seems to use a different key mapping system. The Pajero mapping is from the MCU side (MCU → Android), while the McuKeyClient constants might be for a different purpose (Android → MCU).

Actually, looking at Commands.Key.key():
```java
public static byte[] key(int keyValue, int keyStatus) {
    require(keyValue >= 0 && keyValue <= 0x0B, "keyValue out of range");
    ...
}
```

The A2M direction limits key values to 0x00-0x0B. But the M2A direction (what the MCU sends) can have values like 0x19 (REJECT) and 0x25 (CAMERA). So the Pajero values are for M2A (MCU → Android) and the McuKeyClient values are for A2M (Android → MCU).

This makes sense: the MCU sends Pajero-specific key codes when buttons are pressed, but Android sends a different set of codes when it wants to simulate key presses back to the MCU.

So for the demo:
- KeyListener.onKeyEvent(keyValue, keyStatus) receives the Pajero key values
- I should map those to human-readable names
- For test buttons, I should use the Pajero key values via sendKey/postKey

But wait, the Commands.Key.key() restricts keyValue to 0x00-0x0B. So we can't send Pajero key values through the normal A2M path. The A2M key is for a different purpose.

The document says "[4]. KEY_A2M_KEY暂时不用实现" which means "KEY_A2M_KEY doesn't need to be implemented for now".

So the approach for the demo should be:
1. Display received M2A key events (Pajero key codes)
2. But for testing, we can't easily simulate M2A key events without the MCU

For the test buttons, I'll send the A2M key codes (0x01-0x0B) which go TO the MCU. This is a different test but still useful.

Actually wait, let me reconsider. The user wants to see data coming FROM the MCU. The key events come from the MCU when someone presses steering wheel buttons. We need real MCU data to test this.

For now, let me:
1. Add KeyListener to display received key events
2. Add VehicleListener to display speed
3. Extend raw hex monitor
4. Don't add test buttons for keys (since we can't trigger them without the MCU)

But we can try to connect to the MCU debug serial to trigger test events. Let me try one more approach with the serial port.

Actually, let me also check: maybe I can try `adb shell` to access the MCU debug console via the Android device. Some Android BSPs expose the MCU UART through a virtual serial port or a sysfs interface.

Let me try:
```
adb shell "cat /dev/ttyS5"
```
in the background to see if there's any MCU debug output.

Actually, the permission for ttyS5 is `crw------- 1 root root` - only root can access it. And ttyS7 is `crw-rw-rw- 1 root system` which the service uses.

OK, there's another possibility. The user said the USB-serial device is connected to the Mac. Maybe the device end is connected to the MCU debug port, but we need to set the right parameters. Let me try with hardware flow control (RTS/CTS) enabled, as many MCU debug consoles require this.

Let me also try sending a Ctrl+C or Break to wake up the console.

Actually, let me focus on extending the demo first and come back to the serial test.


Updated task #4 status


串口要先建立连接，不知道你有没有考虑这个问题


Let me now write the extended MainActivity.java with:
1. KeyListener + key name mapping for Pajero SWC keys
2. VehicleListener for speed
3. Extended raw frame hex for KEY (0x02) and PUBLIC (0x01) msgTypes
4. New "SWC & Speed" monitoring card

Let me write the complete file.


Write

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.KeyListener;
import com.qsjnic.mcu.sdk.McuClient;
import com.qsjnic.mcu.sdk.McuRadioClient;
import com.qsjnic.mcu.sdk.RadioListener;
import com.qsjnic.mcu.sdk.VehicleListener;

/**
 * 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.
 */
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 — radio parsed
    private TextView tvValid, tvPty, tvTa, tvTp, tvPs, tvRt, tvWorkStatus, tvTaPtyTips;
    private TextView tvFreqHex, tvPsPtyHex, tvRtHex, tvKeyHex, tvSpeedHex;
    private String lastPs = "";
    private String lastRt = "";

    // SWC & Speed
    private TextView tvLastKey, tvSpeed;

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

    // ----- Pajero SWC key name mapping (MCU → Android) -----
    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";
            case 0x02: return "STUCK";
            case 0x03: return "ENC_L";
            case 0x04: return "ENC_R";
            default:   return String.format("0x%02X", status);
        }
    }

    // ----- Radio Listener -----
    private final RadioListener radioListener = 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);
            });
        }
    };

    // ----- 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);
            ui.post(() -> {
                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");
            ui.post(() -> {
                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) -----
    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 — speed, etc.
                        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;
                    case 0x05: // RADIO
                        switch (cmd) {
                            case 0x01:
                                tvFreqHex.setText("FREQ (" + body.length + "B): " + hex);
                                break;
                            case 0x02:
                                tvPsPtyHex.setText("PS/PTY (" + body.length + "B): " + hex);
                                break;
                            case 0x03:
                                String rtHex = body.length > 32
                                        ? bytesToHex(body, 32) + " ..."
                                        : hex;
                                tvRtHex.setText("RT (" + body.length + "B): " + rtHex);
                                break;
                        }
                        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("MCU Monitor");
        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: --");

        // ===== SWC & Speed Card =====
        root.addView(sectionTitle("SWC Keys & Speed"));
        LinearLayout swcCard = card(root);

        tvLastKey = monText(swcCard, "Key: --");
        tvLastKey.setTextSize(16f);
        tvLastKey.setTypeface(null, Typeface.BOLD);

        tvSpeed = monText(swcCard, "Speed: -- km/h");
        tvSpeed.setTextSize(16f);
        tvSpeed.setTypeface(null, Typeface.BOLD);

        // Key hex
        tvKeyHex = hexText(swcCard);
        tvSpeedHex = hexText(swcCard);

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

        // Row 1: valid + pty + TA + TP
        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 section
        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);

        tvFreqHex = hexText(monCard);
        tvPsPtyHex = hexText(monCard);
        tvRtHex = hexText(monCard);

        // ===== Tuner Audio =====
        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(radioListener);
        McuClient.getInstance().key().addListener(keyListener);
        McuClient.getInstance().vehicle().addListener(vehicleListener);

        // 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(radioListener);
        McuClient.getInstance().key().removeListener(keyListener);
        McuClient.getInstance().vehicle().removeListener(vehicleListener);
        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);
        }
    }
}