// script.js

const VIDEO = document.getElementById('webcam');
const CONNECT_SERIAL_BTN = document.getElementById('connectSerialBtn');
const DISCONNECT_SERIAL_BTN = document.getElementById('disconnectSerialBtn');
const LOAD_MODEL_BTN = document.getElementById('loadModelBtn');
const START_WEBCAM_BTN = document.getElementById('startWebcamBtn');
const STOP_WEBCAM_BTN = document.getElementById('stopWebcamBtn');
const MODEL_STATUS = document.getElementById('modelStatus');
const SERIAL_STATUS = document.getElementById('serialStatus');
const PREDICTION_OUTPUT = document.getElementById('prediction');
const WEBCAM_STATUS_DISPLAY = document.getElementById('webcam-status-display'); // !!! ADDED !!!

let mobilenet; // 这个变量将存储加载后的 MobileNet 模型实例
let knnClassifier; // 这个变量将存储 KNN 分类器实例
let classNames = []; // 类别名称将从加载的模型中获取
let webcamStream = null;
let isPredicting = false;

// Web Serial API 变量
let serialPort = null;
let serialWriter = null;
const SERIAL_BAUD_RATE = 9600;
const SERIAL_SEND_MIN_INTERVAL = 500; // 最短发送间隔，500ms，用于内部节流
let lastSerialCommand = ''; // 用于sendToSerialPort内部的节流（避免重复命令+短间隔）
let lastSerialSendTime = 0;

// ===================================
// 新增变量：追踪串口连接状态，并记录上一次发送到串口的类别命令
let isSerialConnectedState = false; // 跟踪串口的逻辑连接状态
let lastSentClassCommand = null;    // 上一次"成功通过确认机制并实际发送"的命令
// ===================================

// ===================================
// 新增变量：用于实现“确认式”发送逻辑
let pendingCommandToSend = null;    // 正在等待确认的命令
let pendingCommandTimerId = null;   // 确认定时器ID
const CONFIRMATION_DELAY_MS = 100; // 等待 100 毫秒确认 class 是否稳定
// ===================================


// ===================================
// Helper Functions (UI Status)
// ===================================
function showStatus(element, type, message) {
    element.className = `status-message status-${type}`;
    element.textContent = message;
}

function updateSerialUI(isConnected) {
    CONNECT_SERIAL_BTN.disabled = isConnected;
    DISCONNECT_SERIAL_BTN.disabled = !isConnected;
    isSerialConnectedState = isConnected; // 更新我们维护的串口连接状态
    if (!isConnected) {
        showStatus(SERIAL_STATUS, 'info', '串口未连接。点击 "连接串口" 开始。');
    }
}

// !!! MODIFIED: Adjusted updateWebcamUI to use the new WEBCAM_STATUS_DISPLAY !!!
function updateWebcamUI(isRunning) {
    START_WEBCAM_BTN.disabled = isRunning;
    STOP_WEBCAM_BTN.disabled = !isRunning;
    if (isRunning) {
        showStatus(WEBCAM_STATUS_DISPLAY, 'info', '摄像头已启动，等待模型预测...');
        PREDICTION_OUTPUT.classList.remove('idle', 'error'); // !!! ADDED !!!
    } else {
        showStatus(WEBCAM_STATUS_DISPLAY, 'info', '摄像头未启动');
        PREDICTION_OUTPUT.classList.add('idle'); // !!! ADDED !!!
        PREDICTION_OUTPUT.textContent = '等待识别...'; // !!! ADDED !!!
    }
}

function updateModelUI(isLoaded) {
    LOAD_MODEL_BTN.disabled = false; // 总是允许重新加载模型
    START_WEBCAM_BTN.disabled = !isLoaded; // 模型加载后才能启动摄像头
}

// ===================================
// Core Logic: Model & Webcam
// ===================================
async function initModel() {
    showStatus(MODEL_STATUS, 'info', '正在加载 MobileNet 模型...');
    showStatus(WEBCAM_STATUS_DISPLAY, 'info', '系统初始化中...'); // !!! ADDED !!!
    try {
        // 确保 window.mobilenet 和 window.knnClassifier 库已加载
        if (!window.mobilenet || !window.knnClassifier) {
            showStatus(MODEL_STATUS, 'error', 'TensorFlow.js 库或 KNN 分类器库未加载。请检查 HTML 引入。');
            console.error('TensorFlow.js 库或 KNN 分类器库未加载。');
            return;
        }

        mobilenet = await window.mobilenet.load({ version: 2, alpha: 1.0 });
        knnClassifier = window.knnClassifier.create();
        showStatus(MODEL_STATUS, 'success', 'MobileNet 模型和 KNN 分类器已加载。');
        updateModelUI(false); // MobileNet 准备好，但KNN数据还未加载

        // ===== 新增：尝试自动从 CDN 加载 KNN 模型 =====
        // 请替换为你的实际 CDN 模型路径
        const cdnModelBaseUrl = 'https://goood-space-assets.oss-cn-beijing.aliyuncs.com/public/models/'; // 例如：'https://example.com/models/'
        const cdnModelJsonFileName = 'knn-model.json'; // 你的模型json文件名
        const cdnModelBinFileName = 'knn-model.bin'; // 你的模型bin文件名

        const cdnJsonUrl = `${cdnModelBaseUrl}${cdnModelJsonFileName}`;
        const cdnBinUrl = `${cdnModelBaseUrl}${cdnModelBinFileName}`;

        console.log(`尝试从 CDN 加载模型: ${cdnJsonUrl}, ${cdnBinUrl}`);
        showStatus(MODEL_STATUS, 'info', '正在尝试从 CDN 自动加载 KNN 模型...');
        
        try {
            await loadKNNModel(cdnJsonUrl, cdnBinUrl);
            console.log('CDN 模型自动加载成功。');
            // 如果成功，loadKNNModel 会更新状态
        } catch (cdnError) {
            showStatus(MODEL_STATUS, 'warning', `从 CDN 加载 KNN 模型失败: ${cdnError.message}。您可以尝试手动加载。`);
            console.warn('CDN KNN 模型加载失败:', cdnError);
            updateModelUI(false); // 即使 CDN 失败，用户仍然可以通过按钮加载
        }
        // ==============================================

    } catch (error) {
        showStatus(MODEL_STATUS, 'error', `模型加载失败: ${error.message}`);
        showStatus(WEBCAM_STATUS_DISPLAY, 'error', '模型加载失败'); // !!! ADDED !!!
        console.error('MobileNet/KNN加载失败:', error);
    }
}

// 提取 MobileNet 特征
async function getFeatures(img) {
    if (!mobilenet) {
        throw new Error("MobileNet model is not loaded.");
    }
    return tf.tidy(() => {
        const embeddings = mobilenet.infer(img, true);
        const norm = tf.norm(embeddings);
        const normalized = tf.div(embeddings, norm);
        return normalized;
    });
}

/**
 * 加载 KNN 模型文件，支持 '.json' 和 '.bin' 两部分文件。
 * 支持从 URL 或用户选择的文件加载。
 * @param {string} [jsonUrl] - 可选，KNN 模型 .json 文件的 URL。
 * @param {string} [binUrl] - 可选，KNN 模型 .bin 文件的 URL。
 */
async function loadKNNModel(jsonUrl = null, binUrl = null) {
    if (!knnClassifier) {
        showStatus(MODEL_STATUS, 'error', 'KNN 分类器未初始化。请先加载 MobileNet 模型。');
        return;
    }

    let modelData = null;
    let binData = null;
    let modelName = '未知模型';

    try {
        if (jsonUrl && binUrl) {
            // 从 CDN URL 加载
            showStatus(MODEL_STATUS, 'info', `正在从 CDN 加载模型配置文件 (${jsonUrl})...`);
            const jsonResponse = await fetch(jsonUrl);
            if (!jsonResponse.ok) {
                throw new Error(`无法从 ${jsonUrl} 加载.json文件: ${jsonResponse.statusText}`);
            }
            modelData = await jsonResponse.json();
            modelName = jsonUrl.split('/').pop();

            showStatus(MODEL_STATUS, 'info', `正在从 CDN 加载模型权重 (${binUrl})...`);
            const binResponse = await fetch(binUrl);
            if (!binResponse.ok) {
                throw new Error(`无法从 ${binUrl} 加载.bin文件: ${binResponse.statusText}`);
            }
            const arrayBuffer = await binResponse.arrayBuffer();
            binData = new Float32Array(arrayBuffer);

            // 验证 bin 文件名是否匹配（如果 json 中有定义）
            if (modelData.dataFile && !binUrl.endsWith(modelData.dataFile)) {
                console.warn(`CDN 加载警告：.bin URL (${binUrl}) 与 .json 中定义的 dataFile (${modelData.dataFile}) 不匹配。继续加载。`);
            }

        } else {
            // 从用户本地文件加载 (原逻辑不变)
            const inputJson = document.createElement('input');
            inputJson.type = 'file';
            inputJson.accept = '.json';
            inputJson.multiple = false;
        
            showStatus(MODEL_STATUS, 'info', '请先选择 KNN 模型配置文件 (.json)...');
        
            await new Promise((resolve, reject) => {
                inputJson.onchange = async (e) => {
                    const jsonFile = e.target.files[0];
                    if (!jsonFile) {
                        showStatus(MODEL_STATUS, 'info', '未选择 .json 文件。');
                        updateModelUI(false);
                        return reject(new Error('No JSON file selected.'));
                    }
        
                    showStatus(MODEL_STATUS, 'info', `正在解析 ${jsonFile.name}...`);
                    modelName = jsonFile.name;

                    try {
                        const reader = new FileReader();
                        const jsonText = await new Promise((res, rej) => {
                            reader.onload = () => res(reader.result);
                            reader.onerror = () => rej(reader.error);
                            reader.readAsText(jsonFile);
                        });
                        modelData = JSON.parse(jsonText);
                        
                        if (!modelData.dataFile) {
                            console.warn('模型JSON文件不包含 "dataFile" 字段，尝试以旧的单文件JSON格式加载。');
                            // 对于旧的单文件模型，直接加载并结束
                            await loadSingleJsonModel(modelData);
                            return resolve(); // 成功加载旧模型，返回
                        }
        
                    } catch (error) {
                        showStatus(MODEL_STATUS, 'error', `解析 .json 文件失败: ${error.message}`);
                        console.error('解析 .json 失败:', error);
                        updateModelUI(false);
                        return reject(error);
                    }
        
                    const inputBin = document.createElement('input');
                    inputBin.type = 'file';
                    inputBin.accept = '.bin';
                    inputBin.multiple = false;
        
                    showStatus(MODEL_STATUS, 'info', `已加载 .json 文件。请选择对应的权重文件 "${modelData.dataFile}" (.bin)...`);
        
                    inputBin.onchange = async (eBin) => {
                        const binFile = eBin.target.files[0];
                        if (!binFile) {
                            showStatus(MODEL_STATUS, 'info', '未选择 .bin 文件。');
                            updateModelUI(false);
                            return reject(new Error('No BIN file selected.'));
                        }
        
                        if (binFile.name !== modelData.dataFile) {
                            showStatus(MODEL_STATUS, 'error', `选择的 .bin 文件名 "${binFile.name}" 与 .json 中定义的 "${modelData.dataFile}" 不匹配！请选择正确的文件。`);
                            updateModelUI(false);
                            return reject(new Error('BIN file name mismatch.'));
                        }
        
                        showStatus(MODEL_STATUS, 'info', `正在读取 ${binFile.name} (二进制权重文件)...`);
                        try {
                            const reader = new FileReader();
                            const arrayBuffer = await new Promise((res, rej) => {
                                reader.onload = () => res(reader.result);
                                reader.onerror = () => rej(reader.error);
                                reader.readAsArrayBuffer(binFile);
                            });
                            binData = new Float32Array(arrayBuffer);
                            resolve(); // 成功获取到 binData，解析流程继续
                        } catch (error) {
                            showStatus(MODEL_STATUS, 'error', `读取 .bin 文件失败: ${error.message}`);
                            console.error('读取 .bin 失败:', error);
                            updateModelUI(false);
                            return reject(error);
                        }
                    };
                    inputBin.click();
                };
                inputJson.click();
            }); // 结束 Promise 包装的回调
        }

        // 如果 modelData 为 null (意味着旧的单文件JSON模型已在上面被处理并返回)，则停止后续处理
        if (!modelData) {
            return;
        }

        // 执行加载 KNN 分类器的核心逻辑
        if (modelData && binData) { // 仅当同时有 modelData 和 binData 时才尝试加载
            knnClassifier.clearAllClasses();

            Object.keys(modelData.dataset).forEach(label => {
                const classDataMeta = modelData.dataset[label];
                const startFloat32ElementIndex = classDataMeta.start;
                const numFloat32Elements = classDataMeta.length;
                
                const featureDim = modelData.featureDim || 1280;

                // 检查 binData 是否足够大以包含所需的数据
                if (startFloat32ElementIndex + numFloat32Elements > binData.length) {
                    throw new Error(`模型数据错误: 类别 ${label} 的数据超出 .bin 文件范围。`);
                }

                const classFeatures = binData.subarray(startFloat32ElementIndex, startFloat32ElementIndex + numFloat32Elements);

                if (classFeatures.length === 0) {
                    console.warn(`类别 ${label} 没有找到特征数据，跳过。`);
                    return;
                }

                if (classFeatures.length % featureDim !== 0) {
                    const actualSamples = classFeatures.length / featureDim;
                    console.error(
                        `--- 类别: ${label} ---`,
                        `起始 Float32 元素索引: ${startFloat32ElementIndex}`,
                        `该类别 Float32 元素数量: ${numFloat32Elements}`,
                        `ERROR: 特征数据长度 (${classFeatures.length} 个 Float32 元素) 与特征维度 (${featureDim}) 不匹配！` +
                        `实际样本数计算为 ${actualSamples} (预期为整数)。`,
                        `请检查您的模型导出逻辑和训练数据的完整性。`
                    );
                    throw new Error("模型数据完整性错误：特征数据长度与维度不匹配。");
                }
                
                const numSamples = classFeatures.length / featureDim;

                for (let i = 0; i < numSamples; i++) {
                    const startIndex = i * featureDim;
                    const endIndex = (i + 1) * featureDim;
                    const sampleFeatures = classFeatures.subarray(startIndex, endIndex);

                    const sampleTensor = tf.tensor(sampleFeatures, [1, featureDim]);
                    
                    knnClassifier.addExample(sampleTensor, parseInt(label));
                    tf.dispose(sampleTensor); // 及时释放 Tensor 内存
                }
            });

            if (modelData.classList && Array.isArray(modelData.classList)) {
                classNames = modelData.classList.map(c => c.name);
            } else {
                console.warn('模型JSON中未找到 classList 字段或格式不正确，使用默认类别名称。');
                // 如果没有 classList，尝试从 dataset 的键值来生成
                classNames = Object.keys(modelData.dataset).map(key => `Class ${parseInt(key) + 1}`);
            }

            showStatus(MODEL_STATUS, 'success', `KNN 模型 "${modelName}" 加载成功！类别: ${classNames.join(', ')}。`);
            updateModelUI(true); // 模型已加载，可以启动摄像头

        } else if (modelData && !binData && !jsonUrl) {
            // 如果只有 modelData 且不是从 CDN 加载，说明可能是单文件旧格式，但之前的逻辑没成功处理
            // 应该是由 loadSingleJsonModel 捕获，这里作为 fallback
            showStatus(MODEL_STATUS, 'error', '未知模型加载状态：仅有 JSON 数据，没有 BIN 数据。');
            updateModelUI(false);
        }

    } catch (error) {
        showStatus(MODEL_STATUS, 'error', `加载 KNN 模型失败: ${error.message}`);
        showStatus(WEBCAM_STATUS_DISPLAY, 'error', '模型加载失败'); // !!! ADDED !!!
        console.error('加载 KNN 模型总失败:', error);
        updateModelUI(false);
        // 重新抛出错误，以便 initModel 可以捕获 CDN 加载失败的情况
        throw error; 
    }
}

/**
 * 辅助函数：处理旧的单文件JSON模型格式（ dataset 字段直接包含数据而不是偏移量）
 * @param {object} modelData - 已解析的 JSON 模型数据
 * @returns {Promise<void>}
 */
async function loadSingleJsonModel(modelData) {
    try {
        knnClassifier.clearAllClasses();
        Object.keys(modelData.dataset).forEach(key => {
            const data = modelData.dataset[key];
            const featureDim = modelData.featureDim || 1280;
            if (data.length % featureDim !== 0) {
                throw new Error(`类别 ${key} 的特征数据长度 ${data.length} 与特征维度 ${featureDim} 不匹配！`);
            }
            const numSamples = data.length / featureDim;
            const tensor = tf.tensor(data, [numSamples, featureDim]);
            knnClassifier.addExample(tensor, parseInt(key));
            tf.dispose(tensor); // 及时释放 Tensor 内存
        });
 
        if (modelData.classList && Array.isArray(modelData.classList)) {
            classNames = modelData.classList.map(c => c.name);
        } else if (modelData.classNames && Array.isArray(modelData.classNames)) {
            classNames = modelData.classNames;
        } else {
            console.warn('模型JSON中未找到 classList/classNames 字段，使用默认类别名称。');
            classNames = Object.keys(modelData.dataset).map(key => `Class ${parseInt(key) + 1}`);
        }
        
        showStatus(MODEL_STATUS, 'success', `模型 (单文件JSON格式) 加载成功！类别: ${classNames.join(', ')}。`);
        updateModelUI(true);
    } catch (error) {
        showStatus(MODEL_STATUS, 'error', `加载单文件JSON模型失败: ${error.message}`);
        showStatus(WEBCAM_STATUS_DISPLAY, 'error', '模型加载失败'); // !!! ADDED !!!
        console.error('加载单文件JSON模型失败:', error);
        updateModelUI(false);
        throw error; // 重新抛出错误
    }
}


async function startWebcam() {
    if (webcamStream) return;

    if (!knnClassifier || knnClassifier.getNumClasses() === 0) {
        showStatus(MODEL_STATUS, 'error', '请先加载训练好的模型！');
        return;
    }

    try {
        const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user' }, audio: false });
        VIDEO.srcObject = stream;
        webcamStream = stream;
        updateWebcamUI(true);

        VIDEO.onloadeddata = () => {
             // ===================================
            // 启动摄像头时，重置所有发送状态，确保第一次识别结果可发送
            if (pendingCommandTimerId) {
                clearTimeout(pendingCommandTimerId);
                pendingCommandTimerId = null;
            }
            pendingCommandToSend = null;
            lastSentClassCommand = null; 
            // ===================================
            isPredicting = true;
            predictLoop();
            showStatus(WEBCAM_STATUS_DISPLAY, 'success', `摄像头已运行，识别中...`); // !!! MODIFIED !!!
            PREDICTION_OUTPUT.classList.remove('idle', 'error'); // !!! ADDED !!!
        };

    } catch (error) {
        showStatus(MODEL_STATUS, 'error', `无法访问摄像头: ${error.message}`);
        showStatus(WEBCAM_STATUS_DISPLAY, 'error', '无法启动摄像头'); // !!! MODIFIED !!!
        console.error('启动摄像头失败:', error);
        updateWebcamUI(false);
    }
}

function stopWebcam() {
    if (webcamStream) {
        webcamStream.getTracks().forEach(track => track.stop());
        webcamStream = null;
    }
    isPredicting = false;
    VIDEO.srcObject = null;
    updateWebcamUI(false); // !!! MODIFIED !!!
    showStatus(WEBCAM_STATUS_DISPLAY, 'info', '摄像头已停止'); // !!! MODIFIED !!!

    // ===================================
    // 停止摄像头时，清除任何待确认的命令，并发送“停止”或“复位”命令
    if (pendingCommandTimerId) {
        clearTimeout(pendingCommandTimerId);
        pendingCommandTimerId = null;
    }
    pendingCommandToSend = null;

    // 假设 '0' 是停止/复位命令，并且只有当上次发送的不是 '0' 时才发送
    if (lastSentClassCommand !== '0') { 
        sendToSerialPort('0'); 
    }
    lastSentClassCommand = null; // 重置，确保下次启动摄像头时能发送初始状态
    // ===================================
}

// ============== 重要的修改区域 start ==============
// 定义一个全局变量，用于存储当前帧识别到的命令，供 setTimeout 回调使用
let currentCommandInFrame = '0'; // 初始值默认是 '0' (复位/无动作)

async function predictLoop() {
    if (!isPredicting) return;

    if (VIDEO.readyState === 4 && VIDEO.videoWidth > 0 && VIDEO.videoHeight > 0) {
        let commandCandidate = '0'; // 每次循环开始时，将当前帧的候选命令初始化为'0'

        try {
            const features = await getFeatures(VIDEO);
            const k = 3;

            if (!knnClassifier || knnClassifier.getNumClasses() === 0) {
                features.dispose();
                PREDICTION_OUTPUT.textContent = 'KNN 分类器未就绪或无数据。';
                PREDICTION_OUTPUT.classList.add('error'); // !!! ADDED !!!
                commandCandidate = '0';
            } else {
                const prediction = await knnClassifier.predictClass(features, k);
                features.dispose();

                if (prediction && prediction.confidences) {
                    let maxConfidence = 0;
                    let predictedClassIndex = -1;
                    
                    const confidencesArray = Object.entries(prediction.confidences).map(([key, value]) => ({ index: parseInt(key), confidence: value }));
                    
                    confidencesArray.forEach(({ index, confidence }) => {
                        if (confidence > maxConfidence) {
                            maxConfidence = confidence;
                            predictedClassIndex = index;
                        }
                    });

                    const confidenceThreshold = 0.75;
                    if (predictedClassIndex !== -1 && maxConfidence > confidenceThreshold) {
                        const className = classNames[predictedClassIndex] || `Class ${predictedClassIndex + 1}`;
                        const percentage = (maxConfidence * 100).toFixed(1);
                        PREDICTION_OUTPUT.textContent = `识别为: ${className} (${percentage}%)`;
                        PREDICTION_OUTPUT.classList.remove('idle', 'error'); // !!! MODIFIED !!!

                        if (predictedClassIndex === 0) {
                            commandCandidate = '1';
                        } else if (predictedClassIndex === 1) {
                            commandCandidate = '2';
                        } else {
                            commandCandidate = '0';
                        }
                    } else {
                        PREDICTION_OUTPUT.textContent = `未知或不确定... (最高置信度: ${(maxConfidence * 100).toFixed(1)}%)`;
                        PREDICTION_OUTPUT.classList.add('idle'); // !!! MODIFIED !!!
                        commandCandidate = '0';
                    }
                } else {
                    PREDICTION_OUTPUT.textContent = '无法识别。';
                    PREDICTION_OUTPUT.classList.add('error'); // !!! ADDED !!!
                    commandCandidate = '0';
                }
            }
        } catch (error) {
            console.error('预测错误:', error);
            PREDICTION_OUTPUT.textContent = `预测错误: ${error.message}`;
            PREDICTION_OUTPUT.classList.add('error'); // !!! ADDED !!!
            commandCandidate = '0';
        }

        // =========================================================
        // 核心逻辑：确认式发送机制 (Persistence / Confirmation)
        // =========================================================

        // 更新全局变量 currentCommandInFrame，指示当前帧的预测结果
        currentCommandInFrame = commandCandidate;

        // 只有当当前帧的候选命令与上一次实际发送的命令不同时，才进入确认流程
        if (currentCommandInFrame !== lastSentClassCommand) {
            // 如果目前没有待确认的命令，或者待确认的命令与当前不同
            if (pendingCommandToSend === null || pendingCommandToSend !== currentCommandInFrame) {
                if (pendingCommandTimerId) {
                    clearTimeout(pendingCommandTimerId); // 清除旧的等待定时器
                    pendingCommandTimerId = null;
                    // console.log(`[${new Date().toLocaleTimeString()}] 命令在确认前再次变化，取消旧的确认定时器.`);
                }
                
                pendingCommandToSend = currentCommandInFrame; // 设置新的待确认命令
                
                // 设置一个定时器，在 CONFIRMATION_DELAY_MS 后进行确认
                pendingCommandTimerId = setTimeout(async () => {
                    // 当定时器触发时，再次检查全局的 currentCommandInFrame
                    // 如果它仍然与我们开始等待的命令相同，则确认并发送
                    if (currentCommandInFrame === pendingCommandToSend) {
                        console.log(`[${new Date().toLocaleTimeString()}] 确认命令 "${pendingCommandToSend}" 稳定，正在发送。`);
                        await sendToSerialPort(pendingCommandToSend);
                        lastSentClassCommand = pendingCommandToSend; // 更新上一次实际发送的命令
                    } else {
                        console.log(`[${new Date().toLocaleTimeString()}] 命令在确认期内再次变化 (${pendingCommandToSend} -> ${currentCommandInFrame})，不发送。`);
                    }
                    pendingCommandToSend = null;    // 清理待确认状态
                    pendingCommandTimerId = null;   // 清理定时器ID
                }, CONFIRMATION_DELAY_MS);

                // console.log(`[${new Date().toLocaleTimeString()}] 检测到新候选命令 "${currentCommandInFrame}"，启动 ${CONFIRMATION_DELAY_MS}ms 确认计时器。`);
            }
            // 如果 currentCommandInFrame 1. 仍然与 pendingCommandToSend 相同
            //    并且 2. 不同于 lastSentClassCommand
            // 说明它处在“正在等待确认”的状态，不需要做任何事情，让当前定时器继续运行
        } else { // 当前帧的命令与上次实际发送的命令相同
            // 如果已经发送了相同的命令，或者回到了已发送的命令，那么清除待确认定时器
            if (pendingCommandTimerId) {
                clearTimeout(pendingCommandTimerId);
                pendingCommandTimerId = null;
                pendingCommandToSend = null; // 清理待确认状态
                // console.log(`[${new Date().toLocaleTimeString()}] 当前命令 "${currentCommandInFrame}" 与上次发送命令相同，或回摆，取消所有待确认发送。`);
            }
        }
        // =========================================================
    }
    requestAnimationFrame(predictLoop); // 继续下一帧预测
}
// ============== 重要的修改区域 end ==============


// ===================================
// Web Serial API Logic
// ===================================
async function checkWebSerialCompatibility() {
    if ('serial' in navigator) {
        showStatus(SERIAL_STATUS, 'info', 'Web Serial API 可用！');
        CONNECT_SERIAL_BTN.disabled = false;
    } else {
        showStatus(SERIAL_STATUS, 'error', 'Web Serial API 在此浏览器中不可用。请使用最新版 Chrome！');
        CONNECT_SERIAL_BTN.disabled = true;
    }
}

async function connectSerial() {
    showStatus(SERIAL_STATUS, 'info', '正在请求连接串口...');
    try {
        serialPort = await navigator.serial.requestPort();
        
        serialPort.addEventListener('disconnect', () => {
             const disconnectTime = new Date().toLocaleString();
             console.warn(`[${disconnectTime}] ❗️❗️❗️ 串口已断开连接事件触发！ `); // 强调日志
            showStatus(SERIAL_STATUS, 'error', '串口连接已丢失！');
            disconnectSerial();
        });
 
        await serialPort.open({ baudRate: SERIAL_BAUD_RATE });
        console.log('串口已成功打开。尝试获取写入器...');
        serialWriter = serialPort.writable.getWriter();
        console.log('写入器已获取。');
        
        console.log('串口已连接并打开。');
        showStatus(SERIAL_STATUS, 'success', `串口已连接 (Baud: ${SERIAL_BAUD_RATE})`);
        updateSerialUI(true); // 更新状态为已连接
    } catch (error) {
        console.error('连接串口失败:', error);
        if (error.name === 'NotFoundError') {
            showStatus(SERIAL_STATUS, 'warning', '连接串口请求已取消或未选择端口。请选择一个设备。');
        } else if (error.name === 'SecurityError') {
            showStatus(SERIAL_STATUS, 'error', `连接串口失败: ${error.message}。请确保您在安全上下文 (HTTPS 或 localhost) 中运行。`);
        } else {
            showStatus(SERIAL_STATUS, 'error', `连接串口失败: ${error.message}`);
        }
        updateSerialUI(false); // 更新状态为未连接
    }
}

async function disconnectSerial() {
    console.trace(`[${new Date().toLocaleString()}] disconnectSerial() called`);
        
    if (!isSerialConnectedState && !serialPort && !serialWriter) {
        console.log('Already in disconnected state, skipping further cleanup.');
        return;
    }

    // ===================================
    // 断开连接时，清除任何待确认的定时器和状态
    if (pendingCommandTimerId) {
        clearTimeout(pendingCommandTimerId);
        pendingCommandTimerId = null;
    }
    pendingCommandToSend = null;
    // ===================================

    try {
        if (serialWriter) {
            await serialWriter.close();
            serialWriter = null;
        }
        if (serialPort && serialPort.readable) {
            const reader = serialPort.readable.getReader();
            if (reader) {
                await reader.cancel();
                reader.releaseLock();
            }
        }
        if (serialPort && serialPort.isOpen) {
            await serialPort.close();
        }
        serialPort = null;
        console.log('串口已断开。');
        showStatus(SERIAL_STATUS, 'info', '串口已断开。');
    } catch (error) {
        console.error('断开串口失败或串口已处于非连接状态:', error);
        showStatus(SERIAL_STATUS, 'error', `断开串口失败: ${error.message}`);
    } finally {
        updateSerialUI(false); // 无论如何都确保UI和状态更新为断开
        lastSentClassCommand = null; // 断开连接时，重置上一次发送的命令，确保重连后第一次变化能发送
    }
}

/**
 * 低级串口发送函数。包含内部的频繁发送节流。
 * @param {string} command - 要发送的命令字符串。
 */
async function sendToSerialPort(command) {
    if (!isSerialConnectedState) {
        // console.warn('串口已断开（逻辑状态），无法发送命令。'); 
        return;
    }

    if (!serialWriter) {
        console.warn(`[${new Date().toLocaleString()}] 串口连接状态为 true，但 serialWriter 不可用。尝试重置串口连接。`);
        disconnectSerial(); 
        return;
    }

    // 避免频繁发送相同的命令 (sendToSerialPort 内部的节流)
    const currentTime = new Date().getTime();
    if (command === lastSerialCommand && (currentTime - lastSerialSendTime < SERIAL_SEND_MIN_INTERVAL)) {
        return; // 命令相同且发送间隔太短，不发送
    }

    try {
        const encoder = new TextEncoder();
        const data = encoder.encode(command + '\n');
        await serialWriter.write(data);
        console.log(`[${new Date().toLocaleTimeString()}] 实际发送串口命令: ${command}`); 
        lastSerialCommand = command; // 更新内部节流用的命令
        lastSerialSendTime = currentTime;
    } catch (error) {
        console.error(`[${new Date().toLocaleTimeString()}] 实际发送串口命令失败: ${error.message}`);
        showStatus(SERIAL_STATUS, 'error', `发送串口命令失败: ${error.message}，正在断开串口。`);
        disconnectSerial(); 
    }
}

// ===================================
// Event Listeners
// ===================================
CONNECT_SERIAL_BTN.addEventListener('click', connectSerial);
DISCONNECT_SERIAL_BTN.addEventListener('click', disconnectSerial);
LOAD_MODEL_BTN.addEventListener('click', () => loadKNNModel(null, null)); // 手动加载时，不传 CDN URL
START_WEBCAM_BTN.addEventListener('click', startWebcam);
STOP_WEBCAM_BTN.addEventListener('click', stopWebcam);

// ===================================
// Initialization
// ===================================
document.addEventListener('DOMContentLoaded', () => {
    checkWebSerialCompatibility(); // 检查 Web Serial API 兼容性
    initModel(); // 加载 MobileNet 和 KNN 分类器实例，并尝试自动加载 KNN 模型
});
