diff --git a/assets/main.css b/assets/main.css index ba854e2..3af9933 100644 --- a/assets/main.css +++ b/assets/main.css @@ -144,8 +144,8 @@ button.disabled:hover { } /* 选择器容器 */ -.resolution-selector, -.display-mode-selector { +.resolution-config, +.display-mode-config { display: flex; flex-direction: column; gap: 8px; diff --git a/assets/main.js b/assets/main.js index 251bae4..4cc406d 100644 --- a/assets/main.js +++ b/assets/main.js @@ -1,76 +1,144 @@ window.addEventListener('DOMContentLoaded', () => { + /** 显示模式列表 */ + const DISPLAY_MODES = { + data: { + isDefault: true, + label: '数据显示', + }, + spectrum: { + isDefault: false, + label: '频谱显示', + }, + }; + + /** 分辨率配置选项 */ + const RESOLUTION_LIST = { + '160x80': { + isDefault: true, + label: '160x80', + lcdX: 160, lcdY: 80, + showWidth: 320, showHeight: 240, + }, + '320x172': { + isDefault: false, + label: '320x172', + lcdX: 320, lcdY: 172, + showWidth: 320, showHeight: 240, + }, + '320x240': { + isDefault: false, + label: '320x240', + lcdX: 320, lcdY: 240, + showWidth: 320, showHeight: 240, + }, + }; + + /** @type {HTMLButtonElement} */ const connectButton = document.getElementById('connect-button'); + + /** @type {HTMLButtonElement} */ const disconnectButton = document.getElementById('disconnect-button'); + + /** @type {HTMLButtonElement} */ const startButton = document.getElementById('start-button'); + + /** @type {HTMLButtonElement} */ const stopButton = document.getElementById('stop-button'); + + /** @type {HTMLSelectElement} */ + const displayModeSelector = document.getElementById('display-mode-selector'); + + /** @type {HTMLSelectElement} */ + const resolutionSelector = document.getElementById('resolution-selector'); + + /** @type {HTMLCanvasElement} */ const canvas = document.getElementById('canvas-element'); const ctx = canvas.getContext('2d'); - // 全局变量 - let port = null; - let reader = null; - let writer = null; - let deviceState = 0; // 0: 未连接, 1: 已连接 - let refreshInterval = 100; // 刷新间隔(毫秒) - let animationId = null; - let lastRefreshTime = 0; - let displayMode = 'data'; // 显示模式: 'data' 或 'spectrum' + /** + * @desc 串口对象 + * @type {SerialPort} + */ + let serialPort = null; - // 初始化UI状态 - function initUI() { - // 初始绘制 - if (displayMode === 'data') { - monitor.createDisplayImage(); - } else { - drawSpectrum(); - } - } + /** + * @desc 串口读取器 + * @type {ReturnType} + */ + let serialReader = null; + + /** + * @desc 串口写入器 + * @type {ReturnType} + */ + let serialWriter = null; + + /** 连接状态,0: 未连接, 1: 已连接 */ + let deviceState = 0; + + /** 渲染间隔,毫秒 */ + let renderInterval = 100; + + /** 发送间隔,毫秒 */ + let sendInterval = 100; + + /** 最后一次渲染的时间戳 */ + let lastRenderTime = 0; + + /** 最后一次发送的时间戳 */ + let lastSendTime = 0; + + /** 渲染循环 requestAnimationFrame ID */ + let renderAnimationId = null; + + /** 发送循环 requestAnimationFrame ID */ + let sendAnimationId = null; + + /** + * @desc 显示模式 + * @type {keyof (typeof DISPLAY_MODES)} + */ + let displayMode = 'data'; // 音频相关变量 + + /** @type {AudioContext} */ let audioContext = null; - let analyser = null; + + /** @type {ReturnType} */ + let audioAnalyser = null; + + /** @type {number} */ + let audioBufferLength = null; + + /** @type {Uint8Array} */ + let audioDataArray = null; + + /** @type {ReturnType} */ let microphone = null; - let dataArray = null; - let bufferLength = null; - let isMicrophoneActive = false; + + /** 音频是否已初始化 */ + let isAudioReady = false; // 显示参数 - let SHOW_WIDTH = 320; // Canvas 显示宽度(用于网页显示) - let SHOW_HEIGHT = 240; // Canvas 显示高度(用于网页显示) - let LCD_X = 160; // 设备实际宽度 - let LCD_Y = 80; // 设备实际高度 + let CANVAS_WIDTH = 0; // 画布显示宽度 + let CANVAS_HEIGHT = 0; // 画布显示高度 + let LCD_WIDTH = 0; // 设备实际宽度 + let LCD_HEIGHT = 0; // 设备实际高度 - // 分辨率选项配置 - const RESOLUTIONS = { - '160x80': { showWidth: 320, showHeight: 240, lcdX: 160, lcdY: 80 }, - '320x172': { showWidth: 320, showHeight: 240, lcdX: 320, lcdY: 172 }, - '320x240': { showWidth: 320, showHeight: 240, lcdX: 320, lcdY: 240 } - }; + /** 数据文本渲染类 */ + class DataTextRender { - // RGB565颜色定义 - const RED = 0xF800; - const GREEN = 0x07E0; - const BLUE = 0x001F; - const WHITE = 0xFFFF; - const BLACK = 0x0000; - const YELLOW = 0xFFE0; - const CYAN = 0x07FF; - const MAGENTA = 0xF81F; - const ORANGE = 0xFC00; - - // 模拟系统监控数据 - class SystemMonitor { constructor() { this.monitorData = {}; this.font = '32px Arial'; } - collectSystemData() { + updateDataText() { let date = new Date(); - // 生成模拟数据 this.monitorData = { row_1: { value: `${String(date.getFullYear()).padStart(2, '0')}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`, @@ -111,66 +179,41 @@ window.addEventListener('DOMContentLoaded', () => { } formatDisplayText(key, showIcon = false) { - if (!this.monitorData[key]) { + + let item = this.monitorData[key]; + + if (!item) { return ''; } - const item = this.monitorData[key]; - const iconPart = showIcon ? `[${item.icon}] ` : ''; - const detailPart = item.detail || ''; - if (detailPart) { - return `${iconPart}${item.label}: ${item.value}${item.unit} ${detailPart}`; + let icon = showIcon ? `[${item.icon}] ` : ''; + let detail = item.detail || ''; + + if (detail) { + return `${icon}${item.label}: ${item.value}${item.unit} ${detail}`; } else { - return `${iconPart}${item.label}: ${item.value}${item.unit}`; - } - } - - rgb565ToRgb(color565) { - const r = ((color565 >> 11) & 0x1F) << 3; - const g = ((color565 >> 5) & 0x3F) << 2; - const b = (color565 & 0x1F) << 3; - return [r, g, b]; - } - - hexToRgb(hexColor) { - if (typeof hexColor === 'number') { - return this.rgb565ToRgb(hexColor); + return `${icon}${item.label}: ${item.value}${item.unit}`; } - if (typeof hexColor === 'string' && hexColor.startsWith('#')) { - const hex = hexColor.slice(1); - - if (hex.length === 3) { - const r = parseInt(hex[0] + hex[0], 16); - const g = parseInt(hex[1] + hex[1], 16); - const b = parseInt(hex[2] + hex[2], 16); - return [r, g, b]; - } else if (hex.length === 6) { - const r = parseInt(hex.slice(0, 2), 16); - const g = parseInt(hex.slice(2, 4), 16); - const b = parseInt(hex.slice(4, 6), 16); - return [r, g, b]; - } - } - - return [255, 255, 255]; } createDisplayImage() { - this.collectSystemData(); + + // 更新数据 + this.updateDataText(); // 清空画布 ctx.fillStyle = 'black'; - ctx.fillRect(0, 0, SHOW_WIDTH, SHOW_HEIGHT); + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); // 绘制网格布局 this.drawGridLayout(); - return canvas; } drawGridLayout() { - const layouts = [ + + let layouts = [ { key: 'row_1', position: [10, 10] }, { key: 'row_2', position: [10, 50] }, { key: 'row_3', position: [10, 90] }, @@ -183,337 +226,146 @@ window.addEventListener('DOMContentLoaded', () => { layouts.forEach(layout => { if (this.monitorData[layout.key]) { - const item = this.monitorData[layout.key]; - const text = this.formatDisplayText(layout.key, false); - const color = this.hexToRgb(item['color']); - ctx.fillStyle = `rgb(${color[0]}, ${color[1]}, ${color[2]})`; + let item = this.monitorData[layout.key]; + let text = this.formatDisplayText(layout.key, false); + ctx.fillStyle = item['color']; ctx.fillText(text, layout.position[0], layout.position[1]); } }); + } + } - // 创建全局监控器实例 - const monitor = new SystemMonitor(); + /** 数据文本渲染实例 */ + const dataTextRender = new DataTextRender(); - // 初始化UI - initUI(); - - // 初始化音频上下文和麦克风 - async function initAudio() { + /** 开始音频分析,初始化音频上下文和麦克风 */ + async function audioAnalyserStart() { try { + // 创建音频上下文 audioContext = new (window.AudioContext || window.webkitAudioContext)(); // 请求麦克风访问 - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + let stream = await navigator.mediaDevices.getUserMedia({ + audio: true, + video: false, + }); // 创建麦克风源 microphone = audioContext.createMediaStreamSource(stream); // 创建分析器 - analyser = audioContext.createAnalyser(); - analyser.fftSize = 256; - analyser.smoothingTimeConstant = 0.25; + audioAnalyser = audioContext.createAnalyser(); + audioAnalyser.fftSize = 256; + audioAnalyser.smoothingTimeConstant = 0.25; // 连接麦克风到分析器 - microphone.connect(analyser); + microphone.connect(audioAnalyser); // 获取分析器数据 - bufferLength = analyser.frequencyBinCount; - dataArray = new Uint8Array(bufferLength); + audioBufferLength = audioAnalyser.frequencyBinCount; + audioDataArray = new Uint8Array(audioBufferLength); + + // 更新状态 + isAudioReady = true; + + console.info('音频初始化成功,音频分析已开始'); - isMicrophoneActive = true; - console.log('麦克风初始化成功'); } catch (error) { - console.error('初始化音频时出错:', error); - isMicrophoneActive = false; + console.error('音频初始化失败:', error); + isAudioReady = false; } } - // 停止音频分析 - function stopAudio() { + /** 停止音频分析 */ + function audioAnalyserStop() { + if (audioContext) { audioContext.close(); audioContext = null; } + if (microphone) { microphone.disconnect(); microphone = null; } - if (analyser) { - analyser.disconnect(); - analyser = null; + + if (audioAnalyser) { + audioAnalyser.disconnect(); + audioAnalyser = null; } - isMicrophoneActive = false; - console.log('音频分析已停止'); + + isAudioReady = false; + + console.info('音频分析已停止'); + } - // 绘制频谱 - function drawSpectrum() { - // 清空画布 - ctx.fillStyle = 'black'; - ctx.fillRect(0, 0, SHOW_WIDTH, SHOW_HEIGHT); - - if (!isMicrophoneActive || !analyser) { - ctx.fillStyle = 'white'; - ctx.font = '16px Arial'; - ctx.textAlign = 'center'; - ctx.fillText('麦克风未初始化', SHOW_WIDTH / 2, SHOW_HEIGHT / 2); - return; - } - - // 获取频谱数据 - analyser.getByteFrequencyData(dataArray); - - // 计算柱状图宽度和间距 - const barWidth = (SHOW_WIDTH / bufferLength) * 2.5; - let x = 0; - - // 绘制频谱柱状图 - for (let i = 0; i < bufferLength; i++) { - // 计算柱状图高度 - const barHeight = (dataArray[i] / 255) * SHOW_HEIGHT; - - // 生成渐变颜色 - const r = Math.floor((i / bufferLength) * 255); - const g = Math.floor((dataArray[i] / 255) * 255); - const b = 150; - - ctx.fillStyle = `rgb(${r}, ${g}, ${b})`; - - // 绘制柱状图 - ctx.fillRect(x, SHOW_HEIGHT - barHeight, barWidth, barHeight); - - // 移动到下一个柱状图位置 - x += barWidth + 1; - } - } - - // 分辨率选择事件监听器 - const resolutionSelect = document.getElementById('resolution'); - - resolutionSelect.addEventListener('change', (e) => { - - const selectedResolution = e.target.value; - const resolutionConfig = RESOLUTIONS[selectedResolution]; - - // 更新显示参数 - SHOW_WIDTH = resolutionConfig.showWidth; - SHOW_HEIGHT = resolutionConfig.showHeight; - LCD_X = resolutionConfig.lcdX; - LCD_Y = resolutionConfig.lcdY; - - // 更新Canvas尺寸 - canvas.width = SHOW_WIDTH; - canvas.height = SHOW_HEIGHT; - - // 重新创建显示图像 - if (displayMode === 'data') { - monitor.createDisplayImage(); - } else { - drawSpectrum(); - } - - // 显示信息 - console.log(`分辨率已切换至: ${selectedResolution}`); - - }); - - // 显示模式选择事件监听器 - const displayModeSelect = document.getElementById('display-mode'); - - displayModeSelect.addEventListener('change', (e) => { - - displayMode = e.target.value; - - // 重新创建显示图像 - if (displayMode === 'data') { - monitor.createDisplayImage(); - } else { - // 如果切换到频谱模式,初始化麦克风 - if (!isMicrophoneActive) { - initAudio(); - } - drawSpectrum(); - } - - // 显示信息 - console.log(`显示模式已切换至: ${displayMode === 'data' ? '文本数据' : '频谱分析'}`); - - }); - - // 连接按钮点击事件 - connectButton.addEventListener('click', async () => { - try { - - // 请求用户授权访问串口设备 - port = await navigator.serial.requestPort(); - - // 打开串口 - await port.open({ - baudRate: 115200, - dataBits: 8, - stopBits: 1, - parity: 'none', - flowControl: 'hardware', - }); - - // 获取读写器 - const encoder = new TextEncoder(); - const decoder = new TextDecoder('gbk'); - writer = port.writable.getWriter(); - reader = port.readable.getReader(); - - // 开始监听串口数据 - listenForData(reader, decoder); - - // 发送连接确认消息 - await sendMessage('MSNCN'); - - console.log('MSN设备连接中...'); - - // 连接成功后自动开始监控 - setTimeout(() => { - if (!animationId) { - console.log('自动开始...'); - animationId = requestAnimationFrame(animate); - } - }, 100); - - } catch (error) { - console.error('连接设备时出错:', error); - } - }); - - // 监听串口数据 - async function listenForData(reader, decoder) { - try { - while (true) { - const { value, done } = await reader.read(); - if (done) { - // 端口已关闭 - console.log('串口已关闭'); - deviceState = 0; - break; - } - - // 处理接收到的数据 - const data = decoder.decode(value); - console.debug('接收数据:', data); - - // 检测是否为 MSN 设备 - if (data.length > 5) { - for (let n = 0; n < data.length - 5; n++) { - if (data.charCodeAt(n) === 0) { - if (data.substring(n + 1, n + 4) === 'MSN') { - // 确认是 MSN 设备 - console.log('检测到 MSN 设备'); - deviceState = 1; - - // 检查是否收到连接确认 - if (data.substring(n + 1, n + 6) === 'MSNCN') { - console.log('MSN 设备连接完成'); - } - break; - } - } - } - } - } - } catch (error) { - console.error('读取数据时出错:', error); - deviceState = 0; - } - } - - // 发送消息到设备 - async function sendMessage(message) { - if (!writer) return; - - try { - // 构建消息:开头是 0x00,然后是消息内容 - const data = new Uint8Array(message.length + 1); - data[0] = 0x00; // 开头的 0 字节 - for (let i = 0; i < message.length; i++) { - data[i + 1] = message.charCodeAt(i); - } - - await writer.write(data); - console.debug('发送数据:', message); - } catch (error) { - console.error('发送消息时出错:', error); - } - } - - // 断开连接按钮点击事件 - disconnectButton.addEventListener('click', async () => { - - // 断开连接前自动停止监控 - if (animationId) { - console.log('自动停止监控...'); - cancelAnimationFrame(animationId); - animationId = null; - } - - if (reader) { - await reader.cancel(); - reader.releaseLock(); - } - - if (writer) { - await writer.close(); - writer.releaseLock(); - } - - if (port) { - await port.close(); - port = null; - } - - deviceState = 0; - console.log('设备已断开连接'); - - }); - - // 辅助函数:将32位整数拆分为4个字节 - function digitToInts(di) { + /** 将 32 位整数拆分为 4 个字节 */ + function convertDigitToInts(di) { return [(di >> 24) & 0xFF, (di >> 16) & 0xFF, (di >> 8) & 0xFF, di & 0xFF]; } - // RGB888转RGB565格式 - function rgb888ToRgb565(imageData) { - const rgb565Array = []; - for (let i = 0; i < imageData.data.length; i += 4) { - const r = imageData.data[i]; - const g = imageData.data[i + 1]; - const b = imageData.data[i + 2]; - - const rgb565 = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3); - rgb565Array.push(rgb565); - } - return rgb565Array; + /** + * @description 将 RGB565 颜色转换为 RGB 颜色 + * @param {number} color565 RGB565 颜色值 + * @returns {number[]} RGB 颜色值数组,格式:`[r, g, b]` + */ + function convertRgb565ToRgb(color565) { + const r = ((color565 >> 11) & 0x1F) << 3; + const g = ((color565 >> 5) & 0x3F) << 2; + const b = (color565 & 0x1F) << 3; + return [r, g, b]; } - // 对图像数据进行压缩处理,减少传输数据量 - // 基于 compaction.c 中的 MSN_compaction 算法移植 - function ScreenDateProcess(photoData) { - const totalDataSize = photoData.length; - const dataPerPage = 128; - const hexUse = []; + /** + * @description 将 RGB888 转换为 RGB565 格式 + * @param {ImageData} imageData 包含 RGB888 数据的 ImageData 对象 + * @returns {number[]} 包含 RGB565 数据的数组 + */ + function convertRgb888ToRgb565(imageData) { + + let rgb565Array = []; + + for (let i = 0; i < imageData.data.length; i += 4) { + let r = imageData.data[i]; + let g = imageData.data[i + 1]; + let b = imageData.data[i + 2]; + let rgb565 = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3); + rgb565Array.push(rgb565); + } + + return rgb565Array; + + } + + /** + * 对图像数据进行压缩处理,减少传输数据量 + * - 基于 `compaction.c` 中的 `MSN_compaction` 算法移植 + */ + function convertDisplayData(imageData = []) { + + let totalDataSize = imageData.length; + let dataPerPage = 128; + let hexUse = []; let dataIndex = 0; - // 按页处理数据(每页128个像素) - const totalPages = Math.floor(totalDataSize / dataPerPage); + // -- 按页处理数据,每页 128 个像素 -- + + let totalPages = Math.floor(totalDataSize / dataPerPage); + for (let page = 0; page < totalPages; page++) { let i = 0; while (i < 128) { - let a = 0; - let a_data = photoData[dataIndex]; - // 统计第一种颜色的连续出现次数(最多15次) + let a = 0; + let a_data = imageData[dataIndex]; + + // 统计第 1 种颜色的连续出现次数(最多 15 次) for (let s = 0; s < 15; s++) { - if (i < 128 && a_data === photoData[dataIndex]) { + if (i < 128 && a_data === imageData[dataIndex]) { a++; i++; dataIndex++; @@ -525,11 +377,11 @@ window.addEventListener('DOMContentLoaded', () => { let b = 0; let b_data = 0; - // 统计第二种颜色的连续出现次数(最多15次) + // 统计第 2 种颜色的连续出现次数(最多 15 次) if (i < 128) { - b_data = photoData[dataIndex]; + b_data = imageData[dataIndex]; for (let s = 0; s < 15; s++) { - if (i < 128 && b_data === photoData[dataIndex]) { + if (i < 128 && b_data === imageData[dataIndex]) { b++; i++; dataIndex++; @@ -539,29 +391,36 @@ window.addEventListener('DOMContentLoaded', () => { } } - // 输出压缩数据:[9, a*16+b, a_data高字节, a_data低字节, b_data高字节, b_data低字节] + // 输出压缩数据: + // [9, a*16+b, a_data 高字节, a_data 低字节, b_data 高字节, b_data 低字节] hexUse.push(9); hexUse.push(a * 16 + b); hexUse.push((a_data >> 8) & 0xFF); hexUse.push(a_data & 0xFF); hexUse.push((b_data >> 8) & 0xFF); hexUse.push(b_data & 0xFF); + } } - // 处理剩余数据 - const remainingDataSize = totalDataSize % dataPerPage; - if (remainingDataSize !== 0) { - const remainingData = photoData.slice(totalDataSize - remainingDataSize); + // -- 处理剩余数据 -- - // 补全数据到128字节 + let remainingDataSize = totalDataSize % dataPerPage; + + if (remainingDataSize !== 0) { + + let remainingData = imageData.slice(totalDataSize - remainingDataSize); + + // 补全数据到 128 字节 while (remainingData.length < dataPerPage) { remainingData.push(0xFFFF); } let i = 0; let dataIndex = 0; + while (i < 128) { + let a = 0; let a_data = remainingData[dataIndex]; @@ -597,166 +456,485 @@ window.addEventListener('DOMContentLoaded', () => { hexUse.push(a_data & 0xFF); hexUse.push((b_data >> 8) & 0xFF); hexUse.push(b_data & 0xFF); + } } return hexUse; + } - // 设置LCD显示起始坐标 - function LCD_Set_XY(lcdD0, lcdD1) { - const hexUse = []; - hexUse.push(2); // LCD多次写入命令 - hexUse.push(0); // 设置起始位置指令 - hexUse.push(Math.floor(lcdD0 / 256)); // X坐标高字节 - hexUse.push(lcdD0 % 256); // X坐标低字节 - hexUse.push(Math.floor(lcdD1 / 256)); // Y坐标高字节 - hexUse.push(lcdD1 % 256); // Y坐标低字节 + /** 设置 LCD 显示起始坐标 */ + function lcdSetDisplayXY(lcdD0 = 0, lcdD1 = 0) { + let hexUse = []; + hexUse.push(2); // LCD 多次写入命令 + hexUse.push(0); // 设置起始位置指令 + hexUse.push(Math.floor(lcdD0 / 256)); // X 坐标高字节 + hexUse.push(lcdD0 % 256); // X 坐标低字节 + hexUse.push(Math.floor(lcdD1 / 256)); // Y 坐标高字节 + hexUse.push(lcdD1 % 256); // Y 坐标低字节 return hexUse; } - // 设置LCD显示区域大小 - function LCD_Set_Size(lcdD0, lcdD1) { - const hexUse = []; - hexUse.push(2); // LCD多次写入命令 - hexUse.push(1); // 设置大小指令 + /** 设置 LCD 显示区域大小 */ + function lcdSetDisplaySize(lcdD0 = 0, lcdD1 = 0) { + let hexUse = []; + hexUse.push(2); // LCD 多次写入命令 + hexUse.push(1); // 设置大小指令 hexUse.push(Math.floor(lcdD0 / 256)); // 宽度高字节 - hexUse.push(lcdD0 % 256); // 宽度低字节 + hexUse.push(lcdD0 % 256); // 宽度低字节 hexUse.push(Math.floor(lcdD1 / 256)); // 高度高字节 - hexUse.push(lcdD1 % 256); // 高度低字节 + hexUse.push(lcdD1 % 256); // 高度低字节 return hexUse; } - // 设置LCD显示区域并准备写入数据 - async function LCD_ADD(lcdX, lcdY, lcdXSize, lcdYSize) { - const hexUse = []; - hexUse.push(...LCD_Set_XY(lcdX, lcdY)); - hexUse.push(...LCD_Set_Size(lcdXSize, lcdYSize)); - hexUse.push(2); // LCD多次写入命令 + /** + * @description 设置 LCD 显示区域并准备写入数据 + * @param {number} lcdX - 显示区域起始 X 坐标 + * @param {number} lcdY - 显示区域起始 Y 坐标 + * @param {number} lcdW - 显示区域宽度 + * @param {number} lcdH - 显示区域高度 + */ + async function lcdSetDisplaySend(lcdX = 0, lcdY = 0, lcdW = 0, lcdH = 0) { + + let hexUse = []; + + hexUse.push(...lcdSetDisplayXY(lcdX, lcdY)); + hexUse.push(...lcdSetDisplaySize(lcdW, lcdH)); + hexUse.push(2); // LCD 多次写入命令 hexUse.push(3); // 设置指令 hexUse.push(7); // 载入地址 hexUse.push(0, 0, 0); await sendData(hexUse); + } - // 发送数据到设备 - async function sendData(dataArray) { - if (!writer) return; + /** + * @description 监听串口数据 + * @param {typeof serialReader} reader + * @param {TextDecoder} decoder + */ + async function listenSerialData(reader, decoder) { try { - const data = new Uint8Array(dataArray); - await writer.write(data); - console.debug('发送数据:', dataArray.length, '字节'); - } catch (error) { - console.error('发送数据时出错:', error); - } - } + while (true) { - // 显示系统状态到LCD屏幕 - async function showPCState() { - // 创建显示图像 - const image = monitor.createDisplayImage(); + let { value, done } = await reader.read(); - // 创建一个临时Canvas用于图像缩放 - const tempCanvas = document.createElement('canvas'); - tempCanvas.width = LCD_X; - tempCanvas.height = LCD_Y; - const tempCtx = tempCanvas.getContext('2d'); - - // 将原始Canvas内容缩放到设备实际分辨率 - tempCtx.drawImage(canvas, 0, 0, LCD_X, LCD_Y); - - // 获取缩放后的图像数据 - const imageData = tempCtx.getImageData(0, 0, LCD_X, LCD_Y); - - // 转换为RGB565格式 - const rgb565 = rgb888ToRgb565(imageData); - - // 压缩数据 - const hexUse = ScreenDateProcess(rgb565); - - // 发送数据到设备 - await sendData(hexUse); - } - - // 显示频谱到LCD屏幕 - async function showFrequencySpectrum() { - // 绘制频谱 - drawSpectrum(); - - // 创建一个临时Canvas用于图像缩放 - const tempCanvas = document.createElement('canvas'); - tempCanvas.width = LCD_X; - tempCanvas.height = LCD_Y; - const tempCtx = tempCanvas.getContext('2d'); - - // 将原始Canvas内容缩放到设备实际分辨率 - tempCtx.drawImage(canvas, 0, 0, LCD_X, LCD_Y); - - // 获取缩放后的图像数据 - const imageData = tempCtx.getImageData(0, 0, LCD_X, LCD_Y); - - // 转换为RGB565格式 - const rgb565 = rgb888ToRgb565(imageData); - - // 压缩数据 - const hexUse = ScreenDateProcess(rgb565); - - // 发送数据到设备 - await sendData(hexUse); - } - - // 动画循环 - async function animate(timestamp) { - if (!lastRefreshTime) { - lastRefreshTime = timestamp; - } - - const elapsed = timestamp - lastRefreshTime; - if (elapsed >= refreshInterval) { - lastRefreshTime = timestamp; - - // 检查设备状态 - if (deviceState === 1) { - try { - // 设置显示区域 - await LCD_ADD(0, 0, LCD_X, LCD_Y); - - // 根据显示模式选择要显示的内容 - if (displayMode === 'spectrum') { - // 如果是频谱模式但麦克风未初始化,则初始化麦克风 - if (!isMicrophoneActive) { - await initAudio(); - } - await showFrequencySpectrum(); - } else { - await showPCState(); - } - } catch (error) { - console.error('显示内容时出错:', error); + // 端口已关闭 + if (done) { + console.info('串口已关闭'); + deviceState = 0; + break; } - } - } - // 继续动画循环 - animationId = requestAnimationFrame(animate); + // 处理接收到的数据 + let data = decoder.decode(value); + + console.debug('接收数据:', data); + + // 检测是否为 MSN 设备 + if (data.length > 5) { + for (let n = 0; n < data.length - 5; n++) { + if (data.charCodeAt(n) === 0) { + if (data.substring(n + 1, n + 4) === 'MSN') { + // 确认是 MSN 设备 + console.info('检测到 MSN 设备'); + deviceState = 1; + // 检查是否收到连接确认 + if (data.substring(n + 1, n + 6) === 'MSNCN') { + console.info('MSN 设备连接完成'); + } + break; + } + } + } + } + + } + } catch (error) { + console.error('读取串口数据失败:', error); + deviceState = 0; + } } - // 开始按钮点击事件 - startButton.addEventListener('click', () => { - if (!animationId) { - console.log('开始监控...'); - animationId = requestAnimationFrame(animate); - } - }); + /** 循环发送画布内容到设备 */ + async function sendCanvas(timestamp = 0) { + + // 限制发送频率 + if (timestamp - lastSendTime >= sendInterval) { + + // 更新时间戳 + lastSendTime = timestamp; + + // 未连接,结束后续处理 + if (!deviceState) { + sendAnimationId = requestAnimationFrame(sendCanvas); + return; + } + + // 创建一个临时画布用于图像缩放 + let tempCanvas = document.createElement('canvas'); + let tempCtx = tempCanvas.getContext('2d'); + + tempCanvas.width = LCD_WIDTH; + tempCanvas.height = LCD_HEIGHT; + + // 将原始画布内容缩放到设备实际分辨率 + tempCtx.drawImage(canvas, 0, 0, LCD_WIDTH, LCD_HEIGHT); + + // 获取缩放后的图像数据 + let imageData = tempCtx.getImageData(0, 0, LCD_WIDTH, LCD_HEIGHT); + + // 转换为 RGB565 格式 + let rgb565 = convertRgb888ToRgb565(imageData); + + // 压缩数据 + let hexUse = convertDisplayData(rgb565); + + // 设置显示区域 + await lcdSetDisplaySend(0, 0, LCD_WIDTH, LCD_HEIGHT); + + // 发送数据到设备 + await sendData(hexUse); - // 停止按钮点击事件 - stopButton.addEventListener('click', () => { - if (animationId) { - console.log('停止监控...'); - cancelAnimationFrame(animationId); - animationId = null; } - }); + + // 循环发送 + sendAnimationId = requestAnimationFrame(sendCanvas); + + } + + /** 发送数据到设备 */ + async function sendData(dataArray = []) { + + if (!serialWriter) { + return; + } + + try { + let data = new Uint8Array(dataArray); + await serialWriter.write(data); + console.debug('发送数据成功:', dataArray.length, '字节'); + } catch (error) { + console.error('发送数据失败:', error); + } + + } + + /** 发送文本到设备 */ + async function sendText(text = '') { + + if (!serialWriter) { + return; + } + + try { + + // 构建消息 + let data = new Uint8Array(text.length + 1); + + // 添加消息头 + data[0] = 0x00; + + // 处理消息内容 + for (let i = 0; i < text.length; i++) { + data[i + 1] = text.charCodeAt(i); + } + + await serialWriter.write(data); + + console.debug('发送文本成功:', text); + + } catch (error) { + console.error('发送文本失败:', error); + } + + } + + /** 循环渲染画布内容 */ + async function renderCanvas(timestamp = 0) { + + // 限制渲染频率 + if (timestamp - lastRenderTime >= renderInterval) { + + // 更新时间戳 + lastRenderTime = timestamp; + + // 更新画布内容 + switch (displayMode) { + case 'data': + await renderDataText(); + break; + case 'spectrum': + await renderAudioSpectrum(); + break; + default: + break; + } + + } + + // 循环渲染 + renderAnimationId = requestAnimationFrame(renderCanvas); + + } + + /** 渲染音频频谱内容 */ + async function renderAudioSpectrum() { + + // 初始化音频 + if (!isAudioReady) { + await audioAnalyserStart(); + } + + // 清空画布 + ctx.fillStyle = 'black'; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + if (!audioAnalyser) { + ctx.fillStyle = 'white'; + ctx.font = '16px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('音频未初始化', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2); + return; + } + + // 获取频谱数据 + audioAnalyser.getByteFrequencyData(audioDataArray); + + // 计算柱状图宽度和间距 + let barWidth = (CANVAS_WIDTH / audioBufferLength) * 2.5; + let x = 0; + + // 绘制频谱柱状图 + for (let i = 0; i < audioBufferLength; i++) { + + // 计算柱状图高度 + let barHeight = (audioDataArray[i] / 255) * CANVAS_HEIGHT; + + // 生成渐变颜色 + let r = Math.floor((i / audioBufferLength) * 255); + let g = Math.floor((audioDataArray[i] / 255) * 255); + let b = 150; + + ctx.fillStyle = `rgb(${r}, ${g}, ${b})`; + + // 绘制柱状图 + ctx.fillRect(x, CANVAS_HEIGHT - barHeight, barWidth, barHeight); + + // 移动到下一个柱状图位置 + x += barWidth + 1; + + } + + } + + /** 渲染数据文本内容 */ + async function renderDataText() { + + // 更新画布内容 + dataTextRender.createDisplayImage(); + + } + + // 初始化下拉选项 + (function () { + + let displayModeElements = []; + let resolutionElements = []; + + // 显示模式 + for (let key in DISPLAY_MODES) { + + /** @type {typeof DISPLAY_MODES['data']} */ + let item = DISPLAY_MODES[key]; + let element = document.createElement('option'); + + element.value = key; + element.textContent = item.label; + displayModeElements.push(element); + + if (item.isDefault) { + element.selected = true; + } + + } + + // 分辨率 + for (let key in RESOLUTION_LIST) { + + /** @type {typeof RESOLUTION_LIST['160x80']} */ + let item = RESOLUTION_LIST[key]; + let element = document.createElement('option'); + + element.value = key; + element.textContent = item.label; + resolutionElements.push(element); + + if (item.isDefault) { + + element.selected = true; + + // 更新画布属性 + canvas.width = item.showWidth; + canvas.height = item.showHeight; + + // 更新显示参数 + CANVAS_WIDTH = item.showWidth; + CANVAS_HEIGHT = item.showHeight; + LCD_WIDTH = item.lcdX; + LCD_HEIGHT = item.lcdY; + + } + + } + + // 添加到下拉列表 + displayModeSelector.append.apply(displayModeSelector, displayModeElements); + resolutionSelector.append.apply(resolutionSelector, resolutionElements); + + })(); + + // 初始化事件 + (function () { + + // 点击连接设备按钮 + connectButton.addEventListener('click', async () => { + try { + + // 请求用户授权访问串口设备 + serialPort = await navigator.serial.requestPort(); + + // 打开串口 + await serialPort.open({ + baudRate: 115200, + dataBits: 8, + stopBits: 1, + parity: 'none', + flowControl: 'hardware', + }); + + const encoder = new TextEncoder(); + const decoder = new TextDecoder('gbk'); + + serialReader = serialPort.readable.getReader(); + serialWriter = serialPort.writable.getWriter(); + + // 开始监听串口数据 + listenSerialData(serialReader, decoder); + + console.info('MSN 设备连接中...'); + + // 发送连接确认消息 + await sendText('MSNCN'); + + // 连接成功后自动开始发送 + setTimeout(() => { + if (!sendAnimationId) { + console.info('自动开始发送'); + sendAnimationId = requestAnimationFrame(sendCanvas); + } + }, 100); + + } catch (error) { + console.error('连接设备失败:', error); + } + }); + + // 点击断开连接按钮 + disconnectButton.addEventListener('click', async () => { + + // 断开连接前自动停止发送 + if (sendAnimationId) { + console.info('自动停止发送'); + cancelAnimationFrame(sendAnimationId); + sendAnimationId = null; + } + + if (serialReader) { + await serialReader.cancel(); + serialReader.releaseLock(); + serialReader = null; + } + + if (serialWriter) { + await serialWriter.close(); + serialWriter.releaseLock(); + serialWriter = null; + } + + if (serialPort) { + await serialPort.close(); + serialPort = null; + } + + deviceState = 0; + console.info('断开连接成功'); + + }); + + // 点击开始渲染按钮 + startButton.addEventListener('click', () => { + if (!renderAnimationId) { + console.info('开始渲染'); + renderAnimationId = requestAnimationFrame(renderCanvas); + } + }); + + // 点击停止渲染按钮 + stopButton.addEventListener('click', () => { + + // 停止循环渲染 + if (renderAnimationId) { + console.info('停止渲染'); + cancelAnimationFrame(renderAnimationId); + renderAnimationId = null; + } + + // 停止音频分析 + audioAnalyserStop(); + + }); + + // 显示模式选择 + displayModeSelector.addEventListener('change', () => { + + displayMode = displayModeSelector.value; + + let info = DISPLAY_MODES[displayMode]; + + if (info) { + console.info(`显示模式切换成功: ${info.label}`); + } else { + console.error('显示模式切换失败:模式无效'); + displayMode = ''; + return; + } + + }); + + // 分辨率选择 + resolutionSelector.addEventListener('change', () => { + + let value = resolutionSelector.value; + let config = RESOLUTION_LIST[value]; + + // 更新画布属性 + canvas.width = CANVAS_WIDTH; + canvas.height = CANVAS_HEIGHT; + + // 更新显示参数 + CANVAS_WIDTH = config.showWidth; + CANVAS_HEIGHT = config.showHeight; + LCD_WIDTH = config.lcdX; + LCD_HEIGHT = config.lcdY; + + // 显示信息 + console.info(`分辨率切换成功:${value}`); + + }); + + })(); }); diff --git a/index.html b/index.html index 23e86ca..f9de239 100644 --- a/index.html +++ b/index.html @@ -19,30 +19,23 @@
- - + +
-
- - +
+ +
-
- - +
+ +
@@ -51,7 +44,7 @@
- +