feat: UI test mode + per-game port configuration

- Settings: UI test mode toggle generates simulated telemetry data (speed/RPM/gear/laps...)
- Simulated data broadcasts via WebSocket and forwards to downstream devices
- game_ports config: per-game independent port overrides (shown when unified port off)
- Settings UI: game port list with save button, hidden when unified port enabled
- Sidebar: switch game reads game_ports[gameId] fallback to default_port
This commit is contained in:
2026-07-26 17:49:18 +08:00
parent bbaef43cfd
commit b00e59d5bf
5 changed files with 130 additions and 4 deletions
+3 -3
View File
@@ -83,9 +83,9 @@ const Sidebar = {
async _selectGame(gameId) {
await API.updateConfig({ selected_game_id: gameId });
const cfg = await API.getConfig();
const game = this._gameData.find(g => g.id === gameId);
if (game && !cfg.use_unified_port && game.default_port) {
await API.updateConfig({ telemetry_port: game.default_port });
if (!cfg.use_unified_port) {
const gamePort = (cfg.game_ports || {})[gameId] || this._gameData.find(g => g.id === gameId)?.default_port || 20777;
await API.updateConfig({ telemetry_port: gamePort });
}
await API.stopTelemetry();
await API.startTelemetry();
+39 -1
View File
@@ -11,6 +11,12 @@ const PageSettings = {
const forwards = await this._getForwards();
this._el.innerHTML = this._template(cfg, games, forwards);
this._bindEvents();
this._toggleGamePorts(cfg.use_unified_port !== false);
},
_toggleGamePorts(hide) {
const el = document.getElementById('game-ports-section');
if (el) el.style.display = hide ? 'none' : 'block';
},
async _getForwards() {
@@ -30,6 +36,7 @@ const PageSettings = {
document.getElementById('settings-unified-port')?.addEventListener('change', async (e) => {
await API.updateConfig({ use_unified_port: e.target.checked });
this._toggleGamePorts(e.target.checked);
Toast.show('已更新', 'info');
});
@@ -92,8 +99,37 @@ const PageSettings = {
a.download = 'turbosu_backup.tsb';
a.click();
});
document.getElementById('settings-test-mode')?.addEventListener('change', async (e) => {
const on = e.target.checked;
await API.updateConfig({ ui_test_mode: on });
if (on) {
await fetch('/api/test-mode/start', { method: 'POST' });
Toast.show('测试模式已开启', 'success');
} else {
await fetch('/api/test-mode/stop', { method: 'POST' });
Toast.show('测试模式已关闭', 'info');
}
});
document.getElementById('btn-save-game-ports')?.addEventListener('click', async () => {
const rows = document.querySelectorAll('#game-ports-section .game-port-row');
const gamePorts = {};
rows.forEach(r => {
const id = r.querySelector('.gp-id')?.textContent;
const port = parseInt(r.querySelector('.gp-port')?.value) || 0;
if (id && port > 0) gamePorts[id] = port;
});
await API.updateConfig({ game_ports: gamePorts });
Toast.show('端口已保存,切换游戏后生效', 'success');
});
},
_gamePortListHtml(games, cfg) {
const gamePorts = cfg.game_ports || {};
return games.map(g => `<div class="game-port-row" style="display:flex;gap:8px;align-items:center;margin-bottom:6px"><span class="gp-id" style="width:140px;font-size:13px;color:var(--text-secondary)">${g.name}</span><input class="gp-port" type="number" value="${gamePorts[g.id] || g.default_port || 20777}" min="1024" max="65535" style="width:100px"><span style="font-size:11px;color:var(--text-tertiary)">默认: ${g.default_port||20777}</span></div>`).join("");
}
_addForward() {
const list = document.getElementById('forward-list');
const row = document.createElement('div');
@@ -131,12 +167,14 @@ const PageSettings = {
'<div class="settings-section"><h3>数据转发</h3><p style="font-size:12px;color:var(--text-tertiary);margin-bottom:12px">将收到的游戏原始 UDP 数据包完整转发到下游物理外设</p><div id="forward-list">' + this._forwardListHtml(forwards) + '</div><div style="display:flex;gap:8px;margin-top:8px"><button id="btn-add-forward" class="btn btn-sm btn-secondary">+ 添加目标</button><button id="btn-save-forwards" class="btn btn-sm btn-primary">保存并生效</button></div></div>' +
'<div class="settings-section"><h3>连接设置</h3><div class="settings-row"><div><div class="settings-label">遥测监听端口</div><div class="settings-desc">全局统一端口</div></div><div class="settings-control"><input type="number" id="settings-telemetry-port" value="' + (cfg.telemetry_port||20777) + '" min="1024" max="65535"></div></div><div class="settings-row"><div><div class="settings-label">使用游戏独立端口</div><div class="settings-desc">关闭后切游戏自动切换到各游戏的默认端口</div></div><div class="settings-control"><label class="forward-toggle"><input type="checkbox" id="settings-unified-port" ' + (cfg.use_unified_port!==false?'checked':'') + '> 统一端口</label></div></div><div class="settings-row"><div><div class="settings-label">重启遥测监听</div><div class="settings-desc">手动重启</div></div><div class="settings-control"><button id="settings-restart-telemetry" class="btn btn-secondary btn-sm">重启监听</button></div></div></div>' +
'<div class="settings-section"><h3>连接设置</h3><div class="settings-row"><div><div class="settings-label">遥测监听端口</div><div class="settings-desc">全局统一端口</div></div><div class="settings-control"><input type="number" id="settings-telemetry-port" value="' + (cfg.telemetry_port||20777) + '" min="1024" max="65535"></div></div><div class="settings-row"><div><div class="settings-label">使用游戏独立端口</div><div class="settings-desc">关闭后切游戏自动切换到各游戏的默认端口</div></div><div class="settings-control"><label style="display:flex;align-items:center;gap:6px"><input type="checkbox" id="settings-unified-port" ' + (cfg.use_unified_port!==false?'checked':'') + '> 统一端口</label></div></div><div id="game-ports-section" style="margin-top:8px;padding:12px;background:var(--bg-tertiary);border-radius:8px"><h4 style="font-size:13px;color:var(--text-secondary);margin-bottom:10px">各游戏独立端口</h4>' + this._gamePortListHtml(games, cfg) + '<button id="btn-save-game-ports" class="btn btn-sm btn-primary" style="margin-top:8px">保存端口</button></div><div class="settings-row"><div><div class="settings-label">重启遥测监听</div><div class="settings-desc">手动重启</div></div><div class="settings-control"><button id="settings-restart-telemetry" class="btn btn-secondary btn-sm">重启监听</button></div></div></div>' +
'<div class="settings-section"><h3>仪表盘管理</h3><div style="display:flex;gap:8px;margin-bottom:16px"><button id="btn-import-dashboard" class="btn btn-secondary btn-sm">导入 .tsd</button><span style="font-size:12px;color:var(--text-tertiary);display:flex;align-items:center">右键卡片导出</span></div></div>' +
'<div class="settings-section"><h3>数据备份</h3><div class="settings-row"><div><div class="settings-label">导出全量备份</div><div class="settings-desc">打包仪表盘+场景+配置为 .tsb 文件</div></div><div class="settings-control"><button id="btn-export-backup" class="btn btn-secondary btn-sm">下载 .tsb</button></div></div></div>' +
'<div class="settings-section"><h3>开发工具</h3><div class="settings-row"><div><div class="settings-label">UI 测试模式</div><div class="settings-desc">本地随机模拟遥测数据,用于测试仪表盘 UI</div></div><div class="settings-control"><label style="display:flex;align-items:center;gap:6px"><input type="checkbox" id="settings-test-mode" ' + (cfg.ui_test_mode?'checked':'') + '> 开启模拟</label></div></div></div>' +
'<div class="settings-section"><h3>游戏插件管理</h3><div style="display:flex;gap:8px;margin-bottom:16px"><button id="btn-import-plugin" class="btn btn-secondary btn-sm">导入 .tsp</button><button id="btn-reload-plugins" class="btn btn-secondary btn-sm">重新加载</button></div><div id="settings-game-list">' + this._gamePluginListHtml(games) + '</div><div class="glass-card" style="padding:16px;margin-top:12px;font-size:12px;color:var(--text-tertiary);line-height:1.6"><strong style="color:var(--text-secondary)">社区插件开发指南:</strong><br>1. 创建包含 manifest.json + parser.py 的文件夹<br>2. manifest.json 定义元信息,parser.py 实现 get_parser() 函数<br>3. 返回对象实现 game_id() 和 parse(data, addr) 方法<br>4. 打包 zip 改后缀 .tsp 即可导入<br>5. TelemetryData 字段参考 server/telemetry/data.py</div></div>' +
'<div class="settings-section"><h3>关于</h3><div class="settings-row"><div><div class="settings-label">TurboSu</div><div class="settings-desc">赛车遥测仪表盘 v1.0.0 · Yei.J. (AskaEth)</div></div></div></div>' +