commit 5addc8fb42be2a04709623ec58c6d3e657e98218
Author: Eric Peng <epeng@atmosic.com>
Date:   Mon Mar 16 17:20:37 2026 +0800

    [SOFTWARE-8884][OpenAirVsCodeExt] Make WEST FLASH macOS detection venv-aware and dedupe FTDI ports
    
    - run macOS detect through the extension-managed venv activation flow
    - replace text parsing with Python comports() structured JSON output
    - detect J-Link and FTDI interfaces on macOS
    - dedupe FTDI interface-index suffix ports using serial_number
    - preserve fallback parsing when FTDI serial_number is unavailable
    - log command path, raw port fields, and parse summary behind debug mode
    - update WEST FLASH command/UI to use interface-aware serial selection
    - add regression tests for detection, config migration, and flash arguments
    
    Change-Id: Ibf4645403c7db3d92959e566a4efce9dbb8c8170

diff --git a/atmosic/openairvscodeext/src/extension.ts b/atmosic/openairvscodeext/src/extension.ts
index f02ccba2..d3d1ecd2 100644
--- a/atmosic/openairvscodeext/src/extension.ts
+++ b/atmosic/openairvscodeext/src/extension.ts
@@ -22,17 +22,38 @@ import {
 	WestFlashViewProvider,
 	ERASE_OPTIONS,
 	parseDomainsFromBuildDir,
+	detectInterfaceSerials,
 	detectJlinkSerials,
+	WestFlashInterfaceKind,
+	WestFlashInterfaceSerial,
 } from './views/westFlashViewProvider';
 import { boardManager } from './utils/boardManager';
 import { projectManager, ProjectItem } from './utils/projectManager';
 import { testManager, TestItem } from './utils/testManager';
 import { executeCommandWithProgress, executeCommandInTerminal } from './utils/terminal';
-import { getWestPath, getVenvActivatePath, isWindows } from './utils/platform';
+import { getWestPath, getVenvActivatePath, isMacOS, isWindows } from './utils/platform';
 
 // Output channel for logging
 let outputChannel: vscode.OutputChannel;
 
+interface WestFlashInterfaceQuickPickItem extends vscode.QuickPickItem {
+	interfaceKind: WestFlashInterfaceKind;
+}
+
+interface WestFlashDetectedInterfaceQuickPickItem extends vscode.QuickPickItem {
+	interfaceSerial: WestFlashInterfaceSerial;
+}
+
+function getWestFlashInterfaceLabel(kind: WestFlashInterfaceKind): string {
+	return kind === 'jlink' ? 'J-Link' : 'FTDI';
+}
+
+export function buildWestFlashDeviceArgs(interfaceSerial: WestFlashInterfaceSerial): string {
+	return interfaceSerial.kind === 'jlink'
+		? `--device ${interfaceSerial.serial} --jlink`
+		: `--device ${interfaceSerial.serial}`;
+}
+
 export function activate(context: vscode.ExtensionContext) {
 	console.log('Zephyr Environment Setup extension is now active!');
 
@@ -358,6 +379,7 @@ export function activate(context: vscode.ExtensionContext) {
 
 	const westFlashCmd = vscode.commands.registerCommand('zephyr-env-setup.westFlash', async () => {
 		const config = flashViewProvider.getConfig();
+		const interfaceSerial = flashViewProvider.getInterfaceSerial();
 		const workspacePath = getWorkspacePath();
 		if (!workspacePath) {
 			vscode.window.showErrorMessage('Workspace path not set. Please run Step 1 first.');
@@ -375,24 +397,25 @@ export function activate(context: vscode.ExtensionContext) {
 			return;
 		}
 
-		// Check if J-Link serial is set
-		if (!config.jlinkSerial) {
-			vscode.window.showErrorMessage('J-Link serial number not set. Please configure it first.');
-			outputChannel.appendLine('❌ Error: J-Link serial number not set');
+		// Check if interface serial is set
+		if (!interfaceSerial) {
+			vscode.window.showErrorMessage('Interface serial not set. Please configure it first.');
+			outputChannel.appendLine('❌ Error: Interface serial not set');
 			return;
 		}
 
 		// Build the west flash command
 		const eraseFlag = config.eraseOption === 'None' ? '' : config.eraseOption;
 		const domainFlag = config.domain === 'None (flash all)' ? '' : `--domain ${config.domain}`;
-		const westFlash = `west flash -d ${config.buildDir} --no-rebuild --fast_load --device=${config.jlinkSerial} --jlink --verify ${eraseFlag} ${domainFlag}`.replace(/\s+/g, ' ').trim();
+		const deviceArgs = buildWestFlashDeviceArgs(interfaceSerial);
+		const westFlash = `west flash -d ${config.buildDir} --no-rebuild --fast_load ${deviceArgs} --verify ${eraseFlag} ${domainFlag}`.replace(/\s+/g, ' ').trim();
 
 		// Construct the full command with venv activation
 		const activateCommand = isWindows() ? `"${venvActivate}"` : `source "${venvActivate}"`;
 		const command = `${activateCommand} && ${westFlash}`;
 
 		outputChannel.appendLine(`Build dir: ${config.buildDir}`);
-		outputChannel.appendLine(`J-Link serial: ${config.jlinkSerial}`);
+		outputChannel.appendLine(`Interface serial (${getWestFlashInterfaceLabel(interfaceSerial.kind)}): ${interfaceSerial.serial}`);
 		outputChannel.appendLine('');
 		outputChannel.appendLine(`Activating venv and executing flash...`);
 		outputChannel.appendLine('');
@@ -410,44 +433,103 @@ export function activate(context: vscode.ExtensionContext) {
 		}
 	});
 
-	// ── West Flash: Set J-Link Serial ───────────────────────────────────
+	// ── West Flash: Set Interface Serial ────────────────────────────────
 	const westFlashSetJlinkSerialCmd = vscode.commands.registerCommand('zephyr-env-setup.westFlashSetJlinkSerial', async () => {
 		const options = ['$(search) Auto-detect', '$(edit) Enter manually'];
 		const selected = await vscode.window.showQuickPick(options, {
-			placeHolder: 'Select J-Link serial number option',
+			placeHolder: 'Select interface serial option',
 		});
 		if (!selected) { return; }
 
 		if (selected.includes('Auto-detect')) {
-			vscode.window.showInformationMessage('Detecting J-Link devices...');
-			const serials = await detectJlinkSerials();
-			if (serials.length === 0) {
-				vscode.window.showErrorMessage('No J-Link detected. Please connect J-Link and try again.');
-				return;
-			}
-			if (serials.length === 1) {
-				await flashViewProvider.setJlinkSerial(serials[0]);
-				vscode.window.showInformationMessage(`✓ J-Link detected: ${serials[0]}`);
+			if (isMacOS()) {
+				vscode.window.showInformationMessage('Detecting J-Link and FTDI devices...');
+				const detected = await detectInterfaceSerials();
+				if (detected.length === 0) {
+					vscode.window.showErrorMessage('No J-Link or FTDI interface detected. Please connect a device and try again.');
+					return;
+				}
+				if (detected.length === 1) {
+					await flashViewProvider.setInterfaceSerial(detected[0]);
+					vscode.window.showInformationMessage(`✓ ${getWestFlashInterfaceLabel(detected[0].kind)} detected: ${detected[0].serial}`);
+				} else {
+					const quickPickItems: WestFlashDetectedInterfaceQuickPickItem[] = detected.map(interfaceSerial => ({
+						label: `${getWestFlashInterfaceLabel(interfaceSerial.kind)}: ${interfaceSerial.serial}`,
+						description: interfaceSerial.kind === 'jlink'
+							? 'Uses --jlink'
+							: 'Uses default west flash interface',
+						interfaceSerial,
+					}));
+					const picked = await vscode.window.showQuickPick(quickPickItems, {
+						placeHolder: 'Detected multiple interfaces — select one',
+					});
+					if (picked) {
+						await flashViewProvider.setInterfaceSerial(picked.interfaceSerial);
+						vscode.window.showInformationMessage(`✓ ${getWestFlashInterfaceLabel(picked.interfaceSerial.kind)} selected: ${picked.interfaceSerial.serial}`);
+					}
+				}
 			} else {
-				// Multiple J-Links — let user pick
-				const picked = await vscode.window.showQuickPick(serials, {
-					placeHolder: 'Multiple J-Links detected — select one',
-				});
-				if (picked) {
-					await flashViewProvider.setJlinkSerial(picked);
-					vscode.window.showInformationMessage(`✓ J-Link selected: ${picked}`);
+				vscode.window.showInformationMessage('Detecting J-Link devices...');
+				const serials = await detectJlinkSerials();
+				if (serials.length === 0) {
+					vscode.window.showErrorMessage('No J-Link detected. Please connect J-Link and try again.');
+					return;
+				}
+				if (serials.length === 1) {
+					await flashViewProvider.setJlinkSerial(serials[0]);
+					vscode.window.showInformationMessage(`✓ J-Link detected: ${serials[0]}`);
+				} else {
+					const picked = await vscode.window.showQuickPick(serials, {
+						placeHolder: 'Multiple J-Links detected — select one',
+					});
+					if (picked) {
+						await flashViewProvider.setJlinkSerial(picked);
+						vscode.window.showInformationMessage(`✓ J-Link selected: ${picked}`);
+					}
 				}
 			}
 		} else {
-			const current = flashViewProvider.getConfig().jlinkSerial;
-			const input = await vscode.window.showInputBox({
-				prompt: 'Enter J-Link serial number',
-				value: current,
-				placeHolder: 'e.g., 900039257',
-			});
-			if (input !== undefined && input.trim()) {
-				const serial = input.trim().replace(/^0+/, '');
-				await flashViewProvider.setJlinkSerial(serial);
+			if (isMacOS()) {
+				const current = flashViewProvider.getInterfaceSerial();
+				const interfaceOptions: WestFlashInterfaceQuickPickItem[] = [
+					{
+						label: 'J-Link',
+						description: 'Flash with --jlink',
+						interfaceKind: 'jlink',
+					},
+					{
+						label: 'FTDI',
+						description: 'Flash without --jlink',
+						interfaceKind: 'ftdi',
+					},
+				];
+				const selectedInterface = await vscode.window.showQuickPick(interfaceOptions, {
+					placeHolder: 'Select interface type',
+				});
+				if (!selectedInterface) { return; }
+
+				const kind = selectedInterface.interfaceKind;
+				const input = await vscode.window.showInputBox({
+					prompt: `Enter ${getWestFlashInterfaceLabel(kind)} interface serial`,
+					value: current?.kind === kind ? current.serial : '',
+					placeHolder: kind === 'jlink' ? 'e.g., 900039257' : 'e.g., AQ01L2KP',
+				});
+				if (input !== undefined && input.trim()) {
+					await flashViewProvider.setInterfaceSerial({
+						kind,
+						serial: input.trim(),
+					});
+				}
+			} else {
+				const current = flashViewProvider.getInterfaceSerial();
+				const input = await vscode.window.showInputBox({
+					prompt: 'Enter J-Link serial number',
+					value: current?.kind === 'jlink' ? current.serial : '',
+					placeHolder: 'e.g., 900039257',
+				});
+				if (input !== undefined && input.trim()) {
+					await flashViewProvider.setJlinkSerial(input.trim());
+				}
 			}
 		}
 	});
diff --git a/atmosic/openairvscodeext/src/test/extension.test.ts b/atmosic/openairvscodeext/src/test/extension.test.ts
index 4ca0ab41..b6b1f19f 100644
--- a/atmosic/openairvscodeext/src/test/extension.test.ts
+++ b/atmosic/openairvscodeext/src/test/extension.test.ts
@@ -3,6 +3,12 @@ import * as assert from 'assert';
 // You can import and use all API from the 'vscode' module
 // as well as import your extension to test it
 import * as vscode from 'vscode';
+import { buildWestFlashDeviceArgs } from '../extension';
+import {
+	buildMacOSInterfaceDetectionPlan,
+	normalizeWestFlashConfig,
+	parseMacOSInterfaceSerials,
+} from '../views/westFlashViewProvider';
 // import * as myExtension from '../../extension';
 
 suite('Extension Test Suite', () => {
@@ -12,4 +18,131 @@ suite('Extension Test Suite', () => {
 		assert.strictEqual(-1, [1, 2, 3].indexOf(5));
 		assert.strictEqual(-1, [1, 2, 3].indexOf(0));
 	});
+
+	test('parses macOS J-Link and FTDI interface serials from Python JSON output', () => {
+		const output = JSON.stringify([
+			{
+				device: '/dev/cu.usbmodem0009000392281',
+				vid: 4966,
+				pid: 4117,
+				serial_number: null,
+			},
+			{
+				device: '/dev/cu.usbmodem0009000392283',
+				vid: 4966,
+				pid: 4117,
+				serial_number: null,
+			},
+			{
+				device: '/dev/cu.usbserial-ATMEVK108000360',
+				vid: 1027,
+				pid: 24593,
+				serial_number: 'ATMEVK10800036',
+				interface: 'FT4232H',
+			},
+			{
+				device: '/dev/cu.usbserial-ATMEVK108000361',
+				vid: 1027,
+				pid: 24593,
+				serial_number: 'ATMEVK10800036',
+				interface: 'FT4232H',
+			},
+			{
+				device: '/dev/cu.Bluetooth-Incoming-Port',
+				vid: null,
+				pid: null,
+				serial_number: null,
+			},
+		]);
+
+		assert.deepStrictEqual(parseMacOSInterfaceSerials(output), [
+			{ kind: 'jlink', serial: '900039228' },
+			{ kind: 'ftdi', serial: 'ATMEVK10800036' },
+		]);
+	});
+
+	test('falls back to FTDI device suffix when Python metadata lacks serial_number', () => {
+		const output = JSON.stringify([
+			{
+				device: '/dev/cu.usbserial-AQ01L2KP',
+				vid: 1027,
+				pid: 24593,
+				serial_number: null,
+			},
+		]);
+
+		assert.deepStrictEqual(parseMacOSInterfaceSerials(output), [
+			{ kind: 'ftdi', serial: 'AQ01L2KP' },
+		]);
+	});
+
+	test('uses venv activation for macOS interface detection when available', () => {
+		const plan = buildMacOSInterfaceDetectionPlan('/tmp/workspace', '/tmp/workspace/.venv/bin/activate', true);
+		assert.strictEqual(plan.cwd, '/tmp/workspace');
+		assert.strictEqual(plan.strategy, 'activated-venv');
+		assert.ok(plan.command.startsWith('source "/tmp/workspace/.venv/bin/activate" && python - <<\'PY\''));
+		assert.ok(plan.command.includes('serial.tools.list_ports.comports()'));
+	});
+
+	test('falls back to bare python3 detection when macOS venv activation is unavailable', () => {
+		const workspacePlan = buildMacOSInterfaceDetectionPlan('/tmp/workspace', '/tmp/workspace/.venv/bin/activate', false);
+		assert.strictEqual(workspacePlan.cwd, '/tmp/workspace');
+		assert.strictEqual(workspacePlan.strategy, 'path-python-fallback');
+		assert.ok(workspacePlan.command.startsWith('python3 - <<\'PY\''));
+		assert.ok(workspacePlan.command.includes('serial.tools.list_ports.comports()'));
+
+		const defaultPlan = buildMacOSInterfaceDetectionPlan(undefined, undefined, false);
+		assert.strictEqual(defaultPlan.cwd, undefined);
+		assert.strictEqual(defaultPlan.strategy, 'path-python-fallback');
+		assert.ok(defaultPlan.command.startsWith('python3 - <<\'PY\''));
+	});
+
+	test('migrates legacy west flash config to interface-aware serial shape', () => {
+		assert.deepStrictEqual(
+			normalizeWestFlashConfig({
+				jlinkSerial: '000900039228',
+				interfaceKind: 'jlink',
+				eraseOption: '--erase_all',
+				domain: 'APP',
+				buildDir: 'build/app',
+			}),
+			{
+				serial: '900039228',
+				interfaceKind: 'jlink',
+				eraseOption: '--erase_all',
+				domain: 'APP',
+				buildDir: 'build/app',
+			},
+		);
+	});
+
+	test('keeps FTDI serials in the new persisted config shape', () => {
+		assert.deepStrictEqual(
+			normalizeWestFlashConfig({
+				serial: 'AQ01L2KP',
+				interfaceKind: 'ftdi',
+			}),
+			{
+				serial: 'AQ01L2KP',
+				interfaceKind: 'ftdi',
+				eraseOption: 'None',
+				domain: 'None (flash all)',
+				buildDir: 'build/test',
+			},
+		);
+	});
+
+	test('builds west flash device args for J-Link', () => {
+		assert.strictEqual(
+			buildWestFlashDeviceArgs({ kind: 'jlink', serial: '900039228' }),
+			'--device 900039228 --jlink',
+		);
+	});
+
+	test('builds west flash device args for FTDI without --jlink', () => {
+		assert.strictEqual(
+			buildWestFlashDeviceArgs({ kind: 'ftdi', serial: 'AQ01L2KP' }),
+			'--device AQ01L2KP',
+		);
+	});
 });
diff --git a/atmosic/openairvscodeext/src/views/westFlashViewProvider.ts b/atmosic/openairvscodeext/src/views/westFlashViewProvider.ts
index 3633602d..aa13be1f 100644
--- a/atmosic/openairvscodeext/src/views/westFlashViewProvider.ts
+++ b/atmosic/openairvscodeext/src/views/westFlashViewProvider.ts
@@ -2,24 +2,36 @@ import * as vscode from 'vscode';
 import * as path from 'path';
 import * as fs from 'fs';
 import { getWorkspacePath } from '../commands/setupWorkspace';
+import { debugLog } from '../utils/debugLogger';
 import { executeCommand } from '../utils/terminal';
-import { isWindows } from '../utils/platform';
-
-const MACOS_USBMODEM_PATTERN = /^cu\.usbmodem0*([0-9]{9})\d*$/;
+import { getVenvActivatePath, isMacOS, isWindows } from '../utils/platform';
 
 // ─── Configuration Interface ────────────────────────────────────────────────
 
+export type WestFlashInterfaceKind = 'jlink' | 'ftdi';
+
+export interface WestFlashInterfaceSerial {
+    kind: WestFlashInterfaceKind;
+    serial: string;
+}
+
 export interface WestFlashConfig {
-    jlinkSerial: string;
+    serial: string;
+    interfaceKind: WestFlashInterfaceKind;
     eraseOption: string;
     domain: string;
     buildDir: string;
 }
 
+interface StoredWestFlashConfig extends Partial<WestFlashConfig> {
+    jlinkSerial?: string;
+}
+
 // ─── Constants ──────────────────────────────────────────────────────────────
 
 const DEFAULT_CONFIG: WestFlashConfig = {
-    jlinkSerial: '',
+    serial: '',
+    interfaceKind: 'jlink',
     eraseOption: 'None',
     domain: 'None (flash all)',
     buildDir: 'build/test',
@@ -32,6 +44,174 @@ export const ERASE_OPTIONS = [
     '--erase_all',
 ];
 
+const FTDI_VENDOR_ID = 0x0403;
+const FTDI_MULTI_INTERFACE_PRODUCT_ID = 0x6011;
+const MACOS_INTERFACE_DETECTION_SCRIPT = String.raw`import json
+import serial.tools.list_ports
+
+ports = []
+for port in serial.tools.list_ports.comports():
+    ports.append({
+        "device": port.device,
+        "vid": port.vid,
+        "pid": port.pid,
+        "serial_number": port.serial_number,
+        "location": port.location,
+        "interface": getattr(port, "interface", None),
+        "description": port.description,
+    })
+
+print(json.dumps(ports))`;
+
+export interface MacOSInterfaceDetectionPlan {
+    command: string;
+    cwd?: string;
+    strategy: 'activated-venv' | 'path-python-fallback';
+}
+
+interface MacOSDetectedPort {
+    device?: string;
+    vid?: number | null;
+    pid?: number | null;
+    serial_number?: string | null;
+    location?: string | null;
+    interface?: string | null;
+    description?: string | null;
+}
+
+function normalizeJlinkSerial(serial: string): string {
+    return serial.replace(/^0+/, '');
+}
+
+export function normalizeWestFlashConfig(savedConfig?: StoredWestFlashConfig): WestFlashConfig {
+    const interfaceKind = savedConfig?.interfaceKind ?? DEFAULT_CONFIG.interfaceKind;
+    const rawSerial = savedConfig?.serial ?? savedConfig?.jlinkSerial ?? DEFAULT_CONFIG.serial;
+    const serial = interfaceKind === 'jlink'
+        ? normalizeJlinkSerial(rawSerial.trim())
+        : rawSerial.trim();
+
+    return {
+        serial,
+        interfaceKind,
+        eraseOption: savedConfig?.eraseOption ?? DEFAULT_CONFIG.eraseOption,
+        domain: savedConfig?.domain ?? DEFAULT_CONFIG.domain,
+        buildDir: savedConfig?.buildDir ?? DEFAULT_CONFIG.buildDir,
+    };
+}
+
+export function parseMacOSInterfaceSerials(output: string): WestFlashInterfaceSerial[] {
+    const serials: WestFlashInterfaceSerial[] = [];
+    const seen = new Set<string>();
+    let rawPorts: unknown;
+
+    try {
+        rawPorts = JSON.parse(output);
+    } catch (error) {
+        debugLog(`macOS interface detection JSON parse failed: ${error instanceof Error ? error.message : String(error)}`);
+        return [];
+    }
+
+    if (!Array.isArray(rawPorts)) {
+        debugLog('macOS interface detection JSON parse failed: root value was not an array');
+        return [];
+    }
+
+    let jlinkMatches = 0;
+    let ftdiMatches = 0;
+
+    for (const rawPort of rawPorts as MacOSDetectedPort[]) {
+        const device = typeof rawPort.device === 'string' ? rawPort.device.trim() : '';
+        const serialNumber = typeof rawPort.serial_number === 'string' ? rawPort.serial_number.trim() : '';
+        const interfaceName = typeof rawPort.interface === 'string' ? rawPort.interface.trim() : '';
+        const location = typeof rawPort.location === 'string' ? rawPort.location.trim() : '';
+        const description = typeof rawPort.description === 'string' ? rawPort.description.trim() : '';
+        const vid = typeof rawPort.vid === 'number' ? rawPort.vid : undefined;
+        const pid = typeof rawPort.pid === 'number' ? rawPort.pid : undefined;
+
+        debugLog(
+            `macOS interface detection raw port device=${device || '<none>'} vid=${vid ?? '<none>'} pid=${pid ?? '<none>'} ` +
+            `serial_number=${serialNumber || '<none>'} interface=${interfaceName || '<none>'} location=${location || '<none>'} ` +
+            `description=${description || '<none>'}`,
+        );
+
+        if (!device) {
+            continue;
+        }
+
+        const jlinkMatch = device.match(/\/dev\/cu\.usbmodem0*([0-9]{9})\d*(?:\s|$)/);
+        if (jlinkMatch) {
+            const serial = normalizeJlinkSerial(jlinkMatch[1]);
+            const key = `jlink:${serial}`;
+            if (!seen.has(key)) {
+                seen.add(key);
+                serials.push({ kind: 'jlink', serial });
+            }
+            jlinkMatches += 1;
+            continue;
+        }
+
+        if (vid === FTDI_VENDOR_ID && pid === FTDI_MULTI_INTERFACE_PRODUCT_ID) {
+            const fallbackMatch = device.match(/\/dev\/cu\.usbserial-([^\s]+)/);
+            const serial = serialNumber || fallbackMatch?.[1]?.trim() || '';
+            if (!serialNumber && serial) {
+                debugLog(`macOS FTDI detection fell back to device suffix for ${device}`);
+            }
+
+            if (!serial) {
+                continue;
+            }
+
+            const key = `ftdi:${serial}`;
+            if (!seen.has(key)) {
+                seen.add(key);
+                serials.push({ kind: 'ftdi', serial });
+            }
+            ftdiMatches += 1;
+        }
+    }
+
+    debugLog(
+        `macOS interface detection parse summary rawPorts=${rawPorts.length} ` +
+        `jlinkMatches=${jlinkMatches} ftdiMatches=${ftdiMatches} uniqueResults=${serials.length}`,
+    );
+
+    return serials;
+}
+
+function summarizeCommandOutput(output: string): string {
+    const trimmed = output.trim();
+    if (!trimmed) {
+        return 'empty';
+    }
+
+    const normalized = trimmed.replace(/\s+/g, ' ');
+    const preview = normalized.length > 200
+        ? `${normalized.slice(0, 200)}…`
+        : normalized;
+
+    return `${trimmed.split(/\r?\n/).length} line(s), ${trimmed.length} char(s): ${preview}`;
+}
+
+export function buildMacOSInterfaceDetectionPlan(
+    workspacePath?: string,
+    venvActivatePath?: string,
+    venvActivateExists = false,
+): MacOSInterfaceDetectionPlan {
+    if (workspacePath && venvActivatePath && venvActivateExists) {
+        return {
+            command: `source "${venvActivatePath}" && python - <<'PY'\n${MACOS_INTERFACE_DETECTION_SCRIPT}\nPY`,
+            cwd: workspacePath,
+            strategy: 'activated-venv',
+        };
+    }
+
+    return {
+        command: `python3 - <<'PY'\n${MACOS_INTERFACE_DETECTION_SCRIPT}\nPY`,
+        cwd: workspacePath,
+        strategy: 'path-python-fallback',
+    };
+}
+
 // ─── Domain Parsing ─────────────────────────────────────────────────────────
 
 /**
@@ -75,42 +255,49 @@ export function parseDomainsFromBuildDir(buildDir: string): string[] {
     return domains;
 }
 
-function normalizeJlinkSerial(serial: string): string {
-    return serial.replace(/^0+/, '');
-}
-
-export function extractMacOSJlinkSerial(deviceName: string): string | undefined {
-    const match = deviceName.match(MACOS_USBMODEM_PATTERN);
-    if (!match) {
-        return undefined;
-    }
+async function detectMacOSInterfaceSerials(): Promise<WestFlashInterfaceSerial[]> {
+    const workspacePath = getWorkspacePath();
+    const venvActivatePath = workspacePath ? getVenvActivatePath(workspacePath) : undefined;
+    const detectionPlan = buildMacOSInterfaceDetectionPlan(
+        workspacePath,
+        venvActivatePath,
+        Boolean(venvActivatePath && fs.existsSync(venvActivatePath)),
+    );
 
-    const serial = normalizeJlinkSerial(match[1]);
-    return serial || match[1];
-}
+    debugLog(`macOS interface detection workspacePath=${workspacePath ?? '<unresolved>'} venvActivatePath=${venvActivatePath ?? '<unavailable>'}`);
+    debugLog(`macOS interface detection strategy=${detectionPlan.strategy} cwd=${detectionPlan.cwd ?? '<default>'} command=${detectionPlan.command}`);
 
-function detectMacOSJlinkSerials(): string[] {
     try {
-        const serials = fs.readdirSync('/dev')
-            .map(extractMacOSJlinkSerial)
-            .filter((serial): serial is string => Boolean(serial));
+        const result = await executeCommand(detectionPlan.command, detectionPlan.cwd);
 
-        return [...new Set(serials)];
+        debugLog(
+            `macOS interface detection result success=${result.success} exitCode=${result.exitCode} ` +
+            `stdout=${summarizeCommandOutput(result.stdout)} stderr=${summarizeCommandOutput(result.stderr)}`,
+        );
+
+        if (result.success && result.stdout.trim()) {
+            const parsed = parseMacOSInterfaceSerials(result.stdout);
+            debugLog(
+                `macOS interface detection parsed ${parsed.length} interface(s): ` +
+                `${parsed.map(serial => `${serial.kind}:${serial.serial}`).join(', ') || 'none'}`,
+            );
+            return parsed;
+        }
     } catch (error) {
-        console.log('J-Link macOS auto-detection failed', error);
-        return [];
+        debugLog(`macOS interface auto-detection threw: ${error instanceof Error ? error.message : String(error)}`);
     }
+
+    debugLog('macOS interface detection parsed 0 interface(s): none');
+
+    return [];
 }
 
-// ─── J-Link Auto-Detection ───────────────────────────────────────────────────
+// ─── Interface Auto-Detection ───────────────────────────────────────────────
 
 /**
- * Detect connected J-Link serial numbers.
+ * Detect connected J-Link serial numbers using Windows tools.
  *
- * On macOS, enumerates /dev/cu.usbmodem* device nodes and extracts the
- * normalized 9-digit J-Link serial from the device name.
- *
- * On Windows, tries multiple strategies:
+ * Tries multiple strategies:
  * 1. PowerShell WMI query for SEGGER J-Link USB devices
  * 2. PowerShell Get-PnpDevice fallback
  *
@@ -118,15 +305,7 @@ function detectMacOSJlinkSerials(): string[] {
  *
  * @param context - Optional extension context for RDI driver checking
  */
-export async function detectJlinkSerials(context?: vscode.ExtensionContext): Promise<string[]> {
-    if (process.platform === 'darwin') {
-        return detectMacOSJlinkSerials();
-    }
-
-    if (!isWindows()) {
-        return [];
-    }
-
+async function detectNonMacOSJlinkSerials(context?: vscode.ExtensionContext): Promise<string[]> {
     const serials: string[] = [];
 
     try {
@@ -174,6 +353,26 @@ export async function detectJlinkSerials(context?: vscode.ExtensionContext): Pro
     return serials;
 }
 
+export async function detectInterfaceSerials(context?: vscode.ExtensionContext): Promise<WestFlashInterfaceSerial[]> {
+    if (isMacOS()) {
+        return detectMacOSInterfaceSerials();
+    }
+
+    const serials = await detectNonMacOSJlinkSerials(context);
+    return serials.map(serial => ({ kind: 'jlink', serial }));
+}
+
+export async function detectJlinkSerials(context?: vscode.ExtensionContext): Promise<string[]> {
+    if (isMacOS()) {
+        const serials = await detectInterfaceSerials(context);
+        return serials
+            .filter(serial => serial.kind === 'jlink')
+            .map(serial => serial.serial);
+    }
+
+    return detectNonMacOSJlinkSerials(context);
+}
+
 // ─── RDI Driver Check (Windows) ─────────────────────────────────────────────
 
 /**
@@ -330,23 +529,28 @@ export class WestFlashViewProvider implements vscode.TreeDataProvider<WestFlashI
     private config: WestFlashConfig;
 
     constructor(private context: vscode.ExtensionContext) {
-        this.config = context.globalState.get<WestFlashConfig>('westFlashConfig') || { ...DEFAULT_CONFIG };
+        const savedConfig = context.globalState.get<StoredWestFlashConfig>('westFlashConfig');
+        this.config = normalizeWestFlashConfig(savedConfig);
+        if (savedConfig?.jlinkSerial !== undefined && savedConfig.serial === undefined) {
+            void this.saveConfig();
+        }
 
         // Auto-detect J-Link on startup only when a serial is not already configured
-        if (!this.config.jlinkSerial) {
+        if (!this.config.serial) {
             this.autoDetectOnStartup();
         }
     }
 
     private async autoDetectOnStartup(): Promise<void> {
-        const serials = await detectJlinkSerials(this.context);
-        if (this.config.jlinkSerial || serials.length === 0) {
+        const detected = await detectInterfaceSerials(this.context);
+        if (this.config.serial) {
             return;
         }
 
-        this.config.jlinkSerial = serials[0];
-        await this.saveConfig();
-        this.refresh();
+        const jlink = detected.find(serial => serial.kind === 'jlink');
+        if (jlink) {
+            await this.setInterfaceSerial(jlink);
+        }
     }
 
     // ── TreeDataProvider interface ──────────────────────────────────────
@@ -360,10 +564,10 @@ export class WestFlashViewProvider implements vscode.TreeDataProvider<WestFlashI
     }
 
     getChildren(): Thenable<WestFlashItem[]> {
-        const serialDisplay = this.config.jlinkSerial || '(not detected)';
+        const serialDisplay = this.config.serial || '(not detected)';
 
         return Promise.resolve([
-            new WestFlashItem('J-Link Serial  ', serialDisplay,             'jlinkSerial', 'plug'),
+            new WestFlashItem('Interface Serial', serialDisplay,            'jlinkSerial', 'plug'),
             new WestFlashItem('Erase          ', this.config.eraseOption,   'eraseOption',  'trash'),
             new WestFlashItem('Domain         ', this.config.domain,        'domain',       'symbol-namespace'),
             new WestFlashItem('Build Dir      ', this.config.buildDir,      'buildDir',     'file-code'),
@@ -374,12 +578,30 @@ export class WestFlashViewProvider implements vscode.TreeDataProvider<WestFlashI
 
     getConfig(): WestFlashConfig { return this.config; }
 
-    async setJlinkSerial(value: string): Promise<void> {
-        this.config.jlinkSerial = value;
+    getInterfaceSerial(): WestFlashInterfaceSerial | undefined {
+        if (!this.config.serial) {
+            return undefined;
+        }
+
+        return {
+            kind: this.config.interfaceKind,
+            serial: this.config.serial,
+        };
+    }
+
+    async setInterfaceSerial(value: WestFlashInterfaceSerial): Promise<void> {
+        this.config.serial = value.kind === 'jlink'
+            ? normalizeJlinkSerial(value.serial.trim())
+            : value.serial.trim();
+        this.config.interfaceKind = value.kind;
         await this.saveConfig();
         this.refresh();
     }
 
+    async setJlinkSerial(value: string): Promise<void> {
+        await this.setInterfaceSerial({ kind: 'jlink', serial: value });
+    }
+
     async setEraseOption(value: string): Promise<void> {
         this.config.eraseOption = value;
         await this.saveConfig();
