feat(工具箱/MSU2 USB 小屏幕控制): 完善功能

- 添加旧算法
- 支持设置显示方向
- 支持配置渲染间隔和发送间隔
This commit is contained in:
2026-02-24 23:40:03 +08:00
parent 266a1764be
commit 2d805cf02d
3 changed files with 200 additions and 24 deletions

View File

@@ -9,6 +9,10 @@ export const CHANGE_LOGS = {
'[2] - 2024-10-13\n优化界面样式背景添加圆角。', '[2] - 2024-10-13\n优化界面样式背景添加圆角。',
'[1] - 2024-10-11\n初始版本。', '[1] - 2024-10-11\n初始版本。',
], ],
'msu2-usb-monitor-controller': [
'[2] - 2026-02-24\n完善功能添加旧算法支持设置显示方向支持配置渲染间隔和发送间隔。',
'[1] - 2026-02-21\n初始版本。',
],
'qrcode-reader-and-generator': [ 'qrcode-reader-and-generator': [
'[2] - 2025-02-23\n支持生成二维码。\n支持解析剪贴板中的二维码图片。\n优化解析功能在二维码所在位置添加矩形标记。', '[2] - 2025-02-23\n支持生成二维码。\n支持解析剪贴板中的二维码图片。\n优化解析功能在二维码所在位置添加矩形标记。',
'[1] - 2025-02-21\n初始版本。', '[1] - 2025-02-21\n初始版本。',

View File

@@ -363,9 +363,10 @@ export const toolList = [
iconClass: 'mdi mdi-usb', iconClass: 'mdi mdi-usb',
desc: 'MSU2 USB 小屏幕控制工具,参考了原 Python 程序的代码。', desc: 'MSU2 USB 小屏幕控制工具,参考了原 Python 程序的代码。',
createdAt: '2026-02-21', createdAt: '2026-02-21',
updatedAt: '2026-02-21', updatedAt: '2026-02-24',
version: '1', version: '2',
enabled: true, enabled: true,
changelogs: CHANGE_LOGS['msu2-usb-monitor-controller'],
}, },
{ {
id: 'genshin-impact-clock', id: 'genshin-impact-clock',

View File

@@ -2,10 +2,10 @@
<div class="tool-detail-page"> <div class="tool-detail-page">
<!-- 操作 --> <!-- 操作 -->
<n-card size="small" title="操作"> <n-card class="actions" size="small" title="操作">
<n-flex> <n-flex>
<n-button <n-button
type="primary" type="success"
@click="handleConnectDevice" @click="handleConnectDevice"
>连接设备</n-button> >连接设备</n-button>
<n-button <n-button
@@ -21,15 +21,35 @@
@click="handleStopRender" @click="handleStopRender"
>停止渲染</n-button> >停止渲染</n-button>
</n-flex> </n-flex>
<n-flex>
<n-button
type="primary"
@click="lcdSetDirection(0)"
>设置显示方向</n-button>
<n-button
type="primary"
@click="lcdSetDirection(1)"
>设置显示方向</n-button>
<n-button
type="primary"
:disabled="isUseNewDisplayDataConvertFunction"
@click="isUseNewDisplayDataConvertFunction = true"
>显示数据转换算法</n-button>
<n-button
type="primary"
:disabled="!isUseNewDisplayDataConvertFunction"
@click="isUseNewDisplayDataConvertFunction = false"
>显示数据转换算法</n-button>
</n-flex>
</n-card> </n-card>
<!-- 配置参数 --> <!-- 配置参数 -->
<n-card size="small" title="配置参数"> <n-card size="small" title="配置参数">
<n-form <n-form
class="form-no-feedback config-form" class="form-no-feedback config-form"
label-align="left" label-align="right"
label-placement="top" label-placement="left"
:inline="true" label-width="6.5em"
> >
<n-form-item label="分辨率:"> <n-form-item label="分辨率:">
@@ -58,6 +78,26 @@
/> />
</n-form-item> </n-form-item>
<n-form-item label="渲染间隔:">
<n-input-number
v-model:value="renderInterval"
title="单位:毫秒"
:min="10"
:max="60000"
:step="1"
/>
</n-form-item>
<n-form-item label="发送间隔:">
<n-input-number
v-model:value="sendInterval"
title="单位:毫秒"
:min="50"
:max="2000"
:step="1"
/>
</n-form-item>
</n-form> </n-form>
</n-card> </n-card>
@@ -74,7 +114,7 @@
<script setup> <script setup>
import { import {
NButton, NCard, NFlex, NButton, NCard, NFlex,
NForm, NFormItem, NSelect, NForm, NFormItem, NInputNumber, NSelect,
} from 'naive-ui'; } from 'naive-ui';
import { import {
@@ -139,6 +179,15 @@ const deviceResolution = ref('320x172');
/** 显示模式 */ /** 显示模式 */
const displayMode = ref('dataText'); const displayMode = ref('dataText');
/** 是否使用新的显示数据转换算法 */
const isUseNewDisplayDataConvertFunction = ref(true);
/** 渲染间隔,毫秒 */
const renderInterval = ref(100);
/** 发送间隔,毫秒 */
const sendInterval = ref(100);
/** /**
* @desc 串口对象 * @desc 串口对象
* @type {SerialPort} * @type {SerialPort}
@@ -160,12 +209,6 @@ let serialWriter = null;
/** 连接状态0: 未连接, 1: 已连接 */ /** 连接状态0: 未连接, 1: 已连接 */
let deviceState = 0; let deviceState = 0;
/** 渲染间隔,毫秒 */
let renderInterval = 100;
/** 发送间隔,毫秒 */
let sendInterval = 100;
/** 最后一次渲染的时间戳 */ /** 最后一次渲染的时间戳 */
let lastRenderTime = 0; let lastRenderTime = 0;
@@ -296,17 +339,20 @@ async function handleDisconnectDevice() {
await serialReader.cancel(); await serialReader.cancel();
serialReader.releaseLock(); serialReader.releaseLock();
serialReader = null; serialReader = null;
console.info('serialReader 已释放');
} }
if (serialWriter) { if (serialWriter) {
await serialWriter.close(); await serialWriter.close();
serialWriter.releaseLock(); serialWriter.releaseLock();
serialWriter = null; serialWriter = null;
console.info('serialWriter 已释放');
} }
if (serialPort) { if (serialPort) {
await serialPort.close(); await serialPort.close();
serialPort = null; serialPort = null;
console.info('serialPort 已关闭');
} }
deviceState = 0; deviceState = 0;
@@ -686,10 +732,119 @@ function convertDisplayData(imageData = []) {
} }
/**
* 对图像数据进行压缩处理,减少传输数据量
* - 旧算法,适配 `160x80` 设备
*/
function convertDisplayDataLegacy(imageData = []) {
let totalDataSize = imageData.length;
let dataPerPage = 128;
let dataPage1 = 0;
let dataPage2 = 0;
let hexUse = [];
// -- 按页处理数据,每页 128 个像素 --
for (let i = 0; i < Math.floor(totalDataSize / dataPerPage); i++) {
dataPage1 = dataPage2;
dataPage2 += dataPerPage;
let dataW = imageData.slice(dataPage1, dataPage2);
let cmpUse = [];
for (let j = 0; j < dataW.length; j += 2) {
cmpUse.push((dataW[j] << 16) | dataW[j + 1]);
}
// 找出最频繁的颜色作为背景色
let colorCount = {};
cmpUse.forEach(color => {
colorCount[color] = (colorCount[color] || 0) + 1;
});
let maxCount = 0;
let backgroundColor = 0;
for (let [color, count] of Object.entries(colorCount)) {
if (count > maxCount) {
maxCount = count;
backgroundColor = parseInt(color);
}
}
hexUse.push(2, 4);
hexUse.push(...convertDigitToInts(backgroundColor));
// 只记录与背景色不同的像素
cmpUse.forEach((cmpValue, index) => {
if (cmpValue !== backgroundColor) {
hexUse.push(4, index);
hexUse.push(...convertDigitToInts(cmpValue));
}
});
hexUse.push(2, 3, 8, 1, 0, 0);
}
// -- 处理剩余数据 --
let remainingDataSize = totalDataSize % dataPerPage;
if (remainingDataSize !== 0) {
let dataW = imageData.slice(-remainingDataSize);
// 补全数据
while (dataW.length < dataPerPage) {
dataW.push(0xFFFF);
}
let cmpUse = [];
for (let j = 0; j < dataW.length; j += 2) {
cmpUse.push((dataW[j] << 16) | dataW[j + 1]);
}
cmpUse.forEach((cmpValue, index) => {
hexUse.push(4, index);
hexUse.push(...convertDigitToInts(cmpValue));
});
hexUse.push(2, 3, 8, 0, remainingDataSize * 2, 0);
}
return hexUse;
}
/**
* @description 设置显示方向
* @param {0|1} direction 显示方向
* - 0正常1上下翻转
*/
async function lcdSetDirection(direction = 0) {
let hexUse = [];
hexUse.push(2); // LCD 多次写入指令
hexUse.push(3); // 设置指令
hexUse.push(10); // 显示方向指令
hexUse.push(direction);
hexUse.push(0, 0);
await sendData(hexUse);
}
/** 设置 LCD 显示起始坐标 */ /** 设置 LCD 显示起始坐标 */
function lcdSetDisplayXY(lcdD0 = 0, lcdD1 = 0) { function lcdSetDisplayXY(lcdD0 = 0, lcdD1 = 0) {
let hexUse = []; let hexUse = [];
hexUse.push(2); // LCD 多次写入 hexUse.push(2); // LCD 多次写入
hexUse.push(0); // 设置起始位置指令 hexUse.push(0); // 设置起始位置指令
hexUse.push(Math.floor(lcdD0 / 256)); // X 坐标高字节 hexUse.push(Math.floor(lcdD0 / 256)); // X 坐标高字节
hexUse.push(lcdD0 % 256); // X 坐标低字节 hexUse.push(lcdD0 % 256); // X 坐标低字节
@@ -701,7 +856,7 @@ function lcdSetDisplayXY(lcdD0 = 0, lcdD1 = 0) {
/** 设置 LCD 显示区域大小 */ /** 设置 LCD 显示区域大小 */
function lcdSetDisplaySize(lcdD0 = 0, lcdD1 = 0) { function lcdSetDisplaySize(lcdD0 = 0, lcdD1 = 0) {
let hexUse = []; let hexUse = [];
hexUse.push(2); // LCD 多次写入 hexUse.push(2); // LCD 多次写入
hexUse.push(1); // 设置大小指令 hexUse.push(1); // 设置大小指令
hexUse.push(Math.floor(lcdD0 / 256)); // 宽度高字节 hexUse.push(Math.floor(lcdD0 / 256)); // 宽度高字节
hexUse.push(lcdD0 % 256); // 宽度低字节 hexUse.push(lcdD0 % 256); // 宽度低字节
@@ -723,7 +878,7 @@ async function lcdSetDisplaySend(lcdX = 0, lcdY = 0, lcdW = 0, lcdH = 0) {
hexUse.push(...lcdSetDisplayXY(lcdX, lcdY)); hexUse.push(...lcdSetDisplayXY(lcdX, lcdY));
hexUse.push(...lcdSetDisplaySize(lcdW, lcdH)); hexUse.push(...lcdSetDisplaySize(lcdW, lcdH));
hexUse.push(2); // LCD 多次写入 hexUse.push(2); // LCD 多次写入
hexUse.push(3); // 设置指令 hexUse.push(3); // 设置指令
hexUse.push(7); // 载入地址 hexUse.push(7); // 载入地址
hexUse.push(0, 0, 0); hexUse.push(0, 0, 0);
@@ -753,7 +908,7 @@ async function listenSerialData(reader, decoder) {
// 处理接收到的数据 // 处理接收到的数据
let data = decoder.decode(value); let data = decoder.decode(value);
console.debug('接收数据:', data); console.debug('接收数据:', { decoded: data });
// 检测是否为 MSN 设备 // 检测是否为 MSN 设备
if (data.length > 5) { if (data.length > 5) {
@@ -784,7 +939,7 @@ async function listenSerialData(reader, decoder) {
async function sendCanvas(timestamp = 0) { async function sendCanvas(timestamp = 0) {
// 限制发送频率 // 限制发送频率
if (timestamp - lastSendTime >= sendInterval) { if (timestamp - lastSendTime >= sendInterval.value) {
// 更新时间戳 // 更新时间戳
lastSendTime = timestamp; lastSendTime = timestamp;
@@ -812,7 +967,11 @@ async function sendCanvas(timestamp = 0) {
let rgb565 = convertRgb888ToRgb565(imageData); let rgb565 = convertRgb888ToRgb565(imageData);
// 压缩数据 // 压缩数据
let hexUse = convertDisplayData(rgb565); let hexUse = (
isUseNewDisplayDataConvertFunction.value ?
convertDisplayData(rgb565) :
convertDisplayDataLegacy(rgb565)
);
// 设置显示区域 // 设置显示区域
await lcdSetDisplaySend(0, 0, LCD_WIDTH, LCD_HEIGHT); await lcdSetDisplaySend(0, 0, LCD_WIDTH, LCD_HEIGHT);
@@ -878,7 +1037,7 @@ async function sendText(text = '') {
async function renderCanvas(timestamp = 0) { async function renderCanvas(timestamp = 0) {
// 限制渲染频率 // 限制渲染频率
if (timestamp - lastRenderTime >= renderInterval) { if (timestamp - lastRenderTime >= renderInterval.value) {
// 更新时间戳 // 更新时间戳
lastRenderTime = timestamp; lastRenderTime = timestamp;
@@ -1182,11 +1341,23 @@ onBeforeUnmount(() => {
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.actions {
.n-flex {
gap: 10px !important;
&:not(:first-child) {
margin-top: 10px;
}
}
}
.config-form { .config-form {
.n-form-item { .n-form-item {
flex-grow: 1; max-width: 400px;
width: 0; }
max-width: 200px;
.n-input-number {
width: 100%;
} }
} }