Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| da92f7f4fc | |||
| b8d74ff0f5 | |||
| cb976ed0d9 | |||
| baac89448d | |||
| f9515ba843 | |||
| 1dc35034f6 | |||
| 12d8fb432d | |||
| deb5dd0aa9 | |||
| 71c5963639 | |||
| dbef8b66ec |
@@ -1,3 +1,5 @@
|
|||||||
# msu2-usb-monitor-controller
|
# msu2-usb-monitor-controller
|
||||||
|
|
||||||
|
> 本项目中的网页功能停止维护,已整合至 [frost-navigation](https://github.com/Frost-ZX/frost-navigation)。
|
||||||
|
|
||||||
MSU2 USB 小屏幕控制网页 Demo,使用 TRAE 辅助编写,参考了原 Python 程序的代码。
|
MSU2 USB 小屏幕控制网页 Demo,使用 TRAE 辅助编写,参考了原 Python 程序的代码。
|
||||||
|
|||||||
+353
-16
@@ -2,26 +2,38 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
/** 显示模式列表 */
|
/** 显示模式列表 */
|
||||||
const DISPLAY_MODES = {
|
const DISPLAY_MODES = {
|
||||||
data: {
|
audioSpectrum: {
|
||||||
|
isDefault: false,
|
||||||
|
label: '音频频谱',
|
||||||
|
},
|
||||||
|
audioVisualizer1: {
|
||||||
|
isDefault: false,
|
||||||
|
label: '音频可视化 - 1',
|
||||||
|
},
|
||||||
|
audioVisualizer2: {
|
||||||
|
isDefault: false,
|
||||||
|
label: '音频可视化 - 2',
|
||||||
|
},
|
||||||
|
dataText: {
|
||||||
isDefault: true,
|
isDefault: true,
|
||||||
label: '数据显示',
|
label: '数据显示',
|
||||||
},
|
},
|
||||||
spectrum: {
|
digitalClock: {
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
label: '频谱显示',
|
label: '数字时钟',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 分辨率配置选项 */
|
/** 分辨率配置选项 */
|
||||||
const RESOLUTION_LIST = {
|
const RESOLUTION_LIST = {
|
||||||
'160x80': {
|
'160x80': {
|
||||||
isDefault: true,
|
isDefault: false,
|
||||||
label: '160x80',
|
label: '160x80',
|
||||||
lcdX: 160, lcdY: 80,
|
lcdX: 160, lcdY: 80,
|
||||||
showWidth: 480, showHeight: 240,
|
showWidth: 480, showHeight: 240,
|
||||||
},
|
},
|
||||||
'320x172': {
|
'320x172': {
|
||||||
isDefault: false,
|
isDefault: true,
|
||||||
label: '320x172',
|
label: '320x172',
|
||||||
lcdX: 320, lcdY: 172,
|
lcdX: 320, lcdY: 172,
|
||||||
showWidth: 320, showHeight: 172,
|
showWidth: 320, showHeight: 172,
|
||||||
@@ -99,7 +111,7 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
* @desc 显示模式
|
* @desc 显示模式
|
||||||
* @type {keyof (typeof DISPLAY_MODES)}
|
* @type {keyof (typeof DISPLAY_MODES)}
|
||||||
*/
|
*/
|
||||||
let displayMode = 'data';
|
let displayMode = 'dataText';
|
||||||
|
|
||||||
// 音频相关变量
|
// 音频相关变量
|
||||||
|
|
||||||
@@ -464,6 +476,115 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对图像数据进行压缩处理,减少传输数据量
|
||||||
|
* - 旧算法,适配 `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 = [];
|
||||||
@@ -531,7 +652,7 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
// 处理接收到的数据
|
// 处理接收到的数据
|
||||||
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) {
|
||||||
@@ -663,11 +784,20 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// 更新画布内容
|
// 更新画布内容
|
||||||
switch (displayMode) {
|
switch (displayMode) {
|
||||||
case 'data':
|
case 'audioSpectrum':
|
||||||
|
await renderAudioSpectrum();
|
||||||
|
break;
|
||||||
|
case 'audioVisualizer1':
|
||||||
|
await renderAudioVisualizer1();
|
||||||
|
break;
|
||||||
|
case 'audioVisualizer2':
|
||||||
|
await renderAudioVisualizer2();
|
||||||
|
break;
|
||||||
|
case 'dataText':
|
||||||
await renderDataText();
|
await renderDataText();
|
||||||
break;
|
break;
|
||||||
case 'spectrum':
|
case 'digitalClock':
|
||||||
await renderAudioSpectrum();
|
await renderDigitalClock();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -689,14 +819,11 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 清空画布
|
// 清空画布
|
||||||
ctx.fillStyle = 'black';
|
ctx.fillStyle = '#000000';
|
||||||
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
|
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
|
||||||
|
|
||||||
if (!audioAnalyser) {
|
if (!audioAnalyser) {
|
||||||
ctx.fillStyle = 'white';
|
await renderCustomText('音频未初始化');
|
||||||
ctx.font = '16px Arial';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.fillText('音频未初始化', CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -730,6 +857,183 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 渲染音频可视化内容 */
|
||||||
|
async function renderAudioVisualizer1() {
|
||||||
|
|
||||||
|
// 初始化音频
|
||||||
|
if (!isAudioReady) {
|
||||||
|
await audioAnalyserStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空画布
|
||||||
|
ctx.fillStyle = '#000000';
|
||||||
|
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
|
||||||
|
|
||||||
|
if (!audioAnalyser) {
|
||||||
|
await renderCustomText('音频未初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取频谱数据
|
||||||
|
audioAnalyser.getByteFrequencyData(audioDataArray);
|
||||||
|
|
||||||
|
// 计算圆心
|
||||||
|
let centerX = CANVAS_WIDTH / 2;
|
||||||
|
let centerY = CANVAS_HEIGHT / 2;
|
||||||
|
|
||||||
|
// 基础半径
|
||||||
|
let baseRadius = Math.min(CANVAS_WIDTH, CANVAS_HEIGHT) * 0.25;
|
||||||
|
let maxRadiusOffset = Math.min(CANVAS_WIDTH, CANVAS_HEIGHT) * 0.2;
|
||||||
|
|
||||||
|
// 获取绘制点数量
|
||||||
|
let pointSum = Math.min(audioBufferLength, 64);
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
|
||||||
|
// 绘制一个声波圆环
|
||||||
|
for (let i = 0; i <= pointSum; i++) {
|
||||||
|
|
||||||
|
let index = i % pointSum;
|
||||||
|
let angle = (index / pointSum) * Math.PI * 2 - Math.PI / 2;
|
||||||
|
|
||||||
|
let value = audioDataArray[index];
|
||||||
|
let radiusOffset = (value / 255) * maxRadiusOffset;
|
||||||
|
|
||||||
|
let radius = baseRadius + radiusOffset;
|
||||||
|
let x = centerX + Math.cos(angle) * radius;
|
||||||
|
let y = centerY + Math.sin(angle) * radius;
|
||||||
|
|
||||||
|
if (i === 0) {
|
||||||
|
ctx.moveTo(x, y);
|
||||||
|
} else {
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.strokeStyle = '#00FF88';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
let dataSum = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < audioBufferLength; i++) {
|
||||||
|
dataSum += audioDataArray[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算平均音量
|
||||||
|
let avgVolume0 = dataSum / audioBufferLength;
|
||||||
|
let avgVolume1 = avgVolume0 / 255;
|
||||||
|
|
||||||
|
// 计算中心圆形半径(音量越高,圆形越大)
|
||||||
|
let centerCircleRadius = Math.min(CANVAS_WIDTH, CANVAS_HEIGHT) * 0.05 + avgVolume1 * Math.min(CANVAS_WIDTH, CANVAS_HEIGHT) * 0.15;
|
||||||
|
|
||||||
|
// 绘制中心圆形
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX, centerY, centerCircleRadius, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = `rgba(0, 255, 136, ${0.2 + avgVolume1 * 0.8})`;
|
||||||
|
ctx.fill();
|
||||||
|
ctx.strokeStyle = '#00FF88';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// 绘制外层圆形
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX, centerY, baseRadius - 2, 0, Math.PI * 2);
|
||||||
|
ctx.strokeStyle = '#004422';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// 绘制线段
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.strokeStyle = '#00CCFF';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
// 上
|
||||||
|
ctx.moveTo(0, 1);
|
||||||
|
ctx.lineTo(avgVolume1 * CANVAS_WIDTH, 1);
|
||||||
|
ctx.stroke();
|
||||||
|
// 右
|
||||||
|
ctx.moveTo(CANVAS_WIDTH - 4, 0);
|
||||||
|
ctx.lineTo(CANVAS_WIDTH - 4, avgVolume1 * CANVAS_HEIGHT);
|
||||||
|
ctx.stroke();
|
||||||
|
// 下
|
||||||
|
ctx.moveTo(CANVAS_WIDTH, CANVAS_HEIGHT - 1);
|
||||||
|
ctx.lineTo(CANVAS_WIDTH - avgVolume1 * CANVAS_WIDTH, CANVAS_HEIGHT - 1);
|
||||||
|
ctx.stroke();
|
||||||
|
// 左
|
||||||
|
ctx.moveTo(4, CANVAS_HEIGHT);
|
||||||
|
ctx.lineTo(4, CANVAS_HEIGHT - avgVolume1 * CANVAS_HEIGHT);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 渲染音频可视化内容 */
|
||||||
|
async function renderAudioVisualizer2() {
|
||||||
|
|
||||||
|
// 初始化音频
|
||||||
|
if (!isAudioReady) {
|
||||||
|
await audioAnalyserStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空画布
|
||||||
|
ctx.fillStyle = '#000000';
|
||||||
|
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
|
||||||
|
|
||||||
|
if (!audioAnalyser) {
|
||||||
|
await renderCustomText('音频未初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取频谱数据
|
||||||
|
audioAnalyser.getByteFrequencyData(audioDataArray);
|
||||||
|
|
||||||
|
// 频谱粒子矩阵
|
||||||
|
let cols = 20;
|
||||||
|
let rows = 10;
|
||||||
|
let padding = 10;
|
||||||
|
let cellWidth = (CANVAS_WIDTH - padding * 2) / cols;
|
||||||
|
let cellHeight = (CANVAS_HEIGHT - padding * 2) / rows;
|
||||||
|
|
||||||
|
for (let row = 0; row < rows; row++) {
|
||||||
|
for (let col = 0; col < cols; col++) {
|
||||||
|
|
||||||
|
let x = padding + col * cellWidth + cellWidth / 2;
|
||||||
|
let y = padding + row * cellHeight + cellHeight / 2;
|
||||||
|
|
||||||
|
let dataIndex = Math.floor((col / cols) * audioBufferLength);
|
||||||
|
let value = audioDataArray[dataIndex];
|
||||||
|
|
||||||
|
let threshold = (rows - 1 - row) / rows;
|
||||||
|
let normalizedValue = value / 255;
|
||||||
|
|
||||||
|
if (normalizedValue > threshold) {
|
||||||
|
|
||||||
|
let size = cellHeight * 0.35;
|
||||||
|
let brightness = normalizedValue;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, size, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = `rgba(0, 255, 255, ${0.2 + brightness * 0.8})`;
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 渲染自定义文本 */
|
||||||
|
async function renderCustomText(text = '') {
|
||||||
|
ctx.fillStyle = '#000000';
|
||||||
|
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
|
||||||
|
ctx.fillStyle = '#FFFFFF';
|
||||||
|
ctx.font = '16px Arial';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText(text, CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2);
|
||||||
|
}
|
||||||
|
|
||||||
/** 渲染数据文本内容 */
|
/** 渲染数据文本内容 */
|
||||||
async function renderDataText() {
|
async function renderDataText() {
|
||||||
|
|
||||||
@@ -738,6 +1042,34 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 渲染数字时钟内容 */
|
||||||
|
async function renderDigitalClock() {
|
||||||
|
|
||||||
|
// 清空画布
|
||||||
|
ctx.fillStyle = '#000000';
|
||||||
|
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
|
||||||
|
|
||||||
|
// 获取当前时间
|
||||||
|
let date = new Date();
|
||||||
|
let hours = String(date.getHours()).padStart(2, '0');
|
||||||
|
let minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
let seconds = String(date.getSeconds()).padStart(2, '0');
|
||||||
|
|
||||||
|
// 计算字体大小,确保不超出画布
|
||||||
|
let padding = 10;
|
||||||
|
let fontSize = Math.floor((CANVAS_HEIGHT - padding * 2) / 3);
|
||||||
|
let timeFont = `${fontSize}px monospace`;
|
||||||
|
let timeText = `${hours}:${minutes}:${seconds}`;
|
||||||
|
|
||||||
|
// 绘制时间
|
||||||
|
ctx.font = timeFont;
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillStyle = '#FFFFFF';
|
||||||
|
ctx.fillText(timeText, CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化下拉选项
|
// 初始化下拉选项
|
||||||
(function () {
|
(function () {
|
||||||
|
|
||||||
@@ -747,7 +1079,7 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
// 显示模式
|
// 显示模式
|
||||||
for (let key in DISPLAY_MODES) {
|
for (let key in DISPLAY_MODES) {
|
||||||
|
|
||||||
/** @type {typeof DISPLAY_MODES['data']} */
|
/** @type {typeof DISPLAY_MODES['dataText']} */
|
||||||
let item = DISPLAY_MODES[key];
|
let item = DISPLAY_MODES[key];
|
||||||
let element = document.createElement('option');
|
let element = document.createElement('option');
|
||||||
|
|
||||||
@@ -937,4 +1269,9 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// 调试
|
||||||
|
(function () {
|
||||||
|
window.lcdSetDirection = lcdSetDirection;
|
||||||
|
})();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "msu2-usb-monitor-controller",
|
"name": "msu2-usb-monitor-controller-web",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "msu2-usb-monitor-controller",
|
"name": "msu2-usb-monitor-controller-web",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "msu2-usb-monitor-controller",
|
"name": "msu2-usb-monitor-controller-web",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "MSU2 USB 小屏幕控制网页 Demo,使用 TRAE 辅助编写,参考了原 Python 程序的代码。",
|
"description": "MSU2 USB 小屏幕控制网页 Demo,使用 TRAE 辅助编写,参考了原 Python 程序的代码。",
|
||||||
"author": "Frost-ZX",
|
"author": "Frost-ZX",
|
||||||
|
|||||||
Reference in New Issue
Block a user