chore: 提交调试文档与模拟器改动

This commit is contained in:
2026-04-01 13:12:39 +08:00
parent 3ef841ecc7
commit 175a16001e
14 changed files with 1695 additions and 315 deletions

View File

@@ -11,12 +11,36 @@ npm run mock-gps-sim
启动后:
- 新版工作台: `http://127.0.0.1:17865/`
- 旧版面板: `http://127.0.0.1:17865/v1/`
- 小程序定位模拟地址: `ws://127.0.0.1:17865/mock-gps`
- 小程序心率模拟地址: `ws://127.0.0.1:17865/mock-hr`
- 小程序调试日志地址: `ws://127.0.0.1:17865/debug-log`
- 资源代理: `http://127.0.0.1:17865/proxy?url=<remote-url>`
## 多通道联调
模拟器现在支持一个最小的多通道隔离方案:
- GPS 模拟消息带 `channelId`
- 心率模拟消息带 `channelId`
- 调试日志消息带 `channelId`
- 小程序端按同一个模拟通道号过滤三条链
默认通道号:
```text
default
```
如果需要多人并行联调,可以在模拟器工作台里把“模拟通道号”改成例如:
```text
runner-a
runner-b
group-01
```
然后在小程序调试面板里把“模拟通道号”也配成同一个值。
## 当前能力
- 直接载入 `game.json`
@@ -47,6 +71,7 @@ ws://127.0.0.1:17865/debug-log
{
"type": "debug-log",
"timestamp": 1712345678901,
"channelId": "runner-a",
"scope": "gps-logo",
"level": "info",
"message": "wx.getImageInfo success",
@@ -89,12 +114,6 @@ ws://127.0.0.1:17865/debug-log
http://127.0.0.1:17865/
```
如果需要旧版稳定界面,打开:
```text
http://127.0.0.1:17865/v1/
```
在“新网关桥接”区域可以直接配置:
- 是否启用桥接

View File

@@ -14,10 +14,16 @@
<div class="wb-topbar__eyebrow">MOCK GPS SIM</div>
<h1>模拟器工作台</h1>
<div class="wb-topbar__links">
<a href="/v1/">打开旧版面板</a>
<div id="socketStatus" class="badge badge--muted">未连接</div>
</div>
</div>
<div class="wb-topbar__status">
<div class="wb-topbar__global">
<label class="field wb-topbar__field">
<span>模拟通道号</span>
<input id="simChannelIdInput" type="text" placeholder="default / runner-a">
</label>
</div>
<div class="wb-connection-bar">
<div class="wb-connection-pill">
<span class="wb-connection-pill__label">定位模拟</span>
@@ -36,29 +42,11 @@
<strong id="topGatewayStatus" class="wb-connection-pill__value">未启用</strong>
</div>
</div>
<div id="socketStatus" class="badge badge--muted">未连接</div>
</div>
</header>
<div class="wb-layout">
<aside class="wb-sidebar">
<section class="wb-card">
<div class="wb-card__title">运行摘要</div>
<div class="stat"><span>资源状态</span><strong id="summaryResourceText">未载入</strong></div>
<div class="stat"><span>定位发送</span><strong id="summaryGpsSendText">待命</strong></div>
<div class="stat"><span>心率发送</span><strong id="summaryHrSendText">待命</strong></div>
<div class="stat"><span>路径状态</span><strong id="summaryPathText">待命</strong></div>
<div class="stat"><span>网关桥接</span><strong id="summaryGatewayText">未启用</strong></div>
</section>
<section class="wb-card">
<div class="wb-card__title">当前位置</div>
<div class="stat"><span>纬度</span><strong id="latText">--</strong></div>
<div class="stat"><span>经度</span><strong id="lonText">--</strong></div>
<div class="stat"><span>航向</span><strong id="headingText">--</strong></div>
<div class="stat"><span>路径点</span><strong id="pathCountText">0</strong></div>
</section>
<details class="wb-section" open>
<summary>资源加载</summary>
<div class="wb-section__body">
@@ -254,15 +242,15 @@
<main class="wb-stage">
<div id="map"></div>
<section id="floatingDebugLogPanel" class="floating-debug-log">
<div class="floating-debug-log__header">
<div class="floating-debug-log__title-wrap">
<div class="floating-debug-log__title">调试日志</div>
<div id="debugLogMeta" class="floating-debug-log__meta">全部 · 0 条</div>
</div>
<div class="floating-debug-log__actions">
<label class="floating-debug-log__filter">
<span>范围</span>
<section id="floatingDebugLogPanel" class="floating-debug-log">
<div class="floating-debug-log__header">
<div class="floating-debug-log__title-wrap">
<div class="floating-debug-log__title">调试日志</div>
<div id="debugLogMeta" class="floating-debug-log__meta">全部 · 0 条</div>
</div>
<div class="floating-debug-log__actions">
<label class="floating-debug-log__filter">
<span>范围</span>
<select id="debugLogScopeFilter">
<option value="all">全部</option>
</select>
@@ -274,13 +262,31 @@
<div id="debugLog" class="log log--debug log--floating"></div>
</section>
</main>
<aside class="wb-rail">
<section class="wb-card">
<div class="wb-card__title">运行摘要</div>
<div class="stat"><span>资源状态</span><strong id="summaryResourceText">未载入</strong></div>
<div class="stat"><span>定位发送</span><strong id="summaryGpsSendText">待命</strong></div>
<div class="stat"><span>心率发送</span><strong id="summaryHrSendText">待命</strong></div>
<div class="stat"><span>路径状态</span><strong id="summaryPathText">待命</strong></div>
<div class="stat"><span>网关桥接</span><strong id="summaryGatewayText">未启用</strong></div>
</section>
<section class="wb-card">
<div class="wb-card__title">当前位置</div>
<div class="stat"><span>纬度</span><strong id="latText">--</strong></div>
<div class="stat"><span>经度</span><strong id="lonText">--</strong></div>
<div class="stat"><span>航向</span><strong id="headingText">--</strong></div>
<div class="stat"><span>路径点</span><strong id="pathCountText">0</strong></div>
</section>
<section class="wb-card">
<div class="wb-card__title">最近事件</div>
<div id="log" class="log"></div>
</section>
</aside>
</div>
<section class="wb-bottom-strip">
<section class="wb-card wb-card--bottom">
<div class="wb-card__title">最近事件</div>
<div id="log" class="log"></div>
</section>
</section>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>

View File

@@ -13,8 +13,14 @@
])
const BRIDGE_CONFIG_STORAGE_KEY = 'mock-gps-sim.bridge-config'
const BRIDGE_PRESETS_STORAGE_KEY = 'mock-gps-sim.bridge-presets'
const SIM_CHANNEL_STORAGE_KEY = 'mock-gps-sim.channel-id'
const MAX_DEBUG_LOG_LINES = 400
function normalizeSimChannelId(rawValue) {
const trimmed = String(rawValue || '').trim()
return trimmed || 'default'
}
const map = L.map('map').setView(DEFAULT_CENTER, 16)
let tileLayer = createTileLayer(DEFAULT_TILE_URL, {
maxZoom: 20,
@@ -60,6 +66,7 @@
lastHeartRateSentText: '--',
lastResourceDetailText: '尚未载入资源',
lastTrackSourceText: '路径待命',
simChannelId: 'default',
currentLatLng: L.latLng(DEFAULT_CENTER[0], DEFAULT_CENTER[1]),
headingDeg: 0,
currentSegmentIndex: 0,
@@ -128,6 +135,7 @@
trackFileInput: document.getElementById('trackFileInput'),
importTrackBtn: document.getElementById('importTrackBtn'),
connectBtn: document.getElementById('connectBtn'),
simChannelIdInput: document.getElementById('simChannelIdInput'),
sendOnceBtn: document.getElementById('sendOnceBtn'),
streamBtn: document.getElementById('streamBtn'),
stopStreamBtn: document.getElementById('stopStreamBtn'),
@@ -164,6 +172,7 @@
}
elements.configUrlInput.value = DEFAULT_CONFIG_URL
applySimChannelId(loadSimChannelId(), false)
function createTileLayer(urlTemplate, extraOptions) {
return L.tileLayer(urlTemplate, Object.assign({
@@ -177,6 +186,33 @@
elements.log.textContent = `[${time}] ${message}\n` + elements.log.textContent
}
function loadSimChannelId() {
try {
return normalizeSimChannelId(window.localStorage.getItem(SIM_CHANNEL_STORAGE_KEY))
} catch (_error) {
return 'default'
}
}
function saveSimChannelId(channelId) {
try {
window.localStorage.setItem(SIM_CHANNEL_STORAGE_KEY, normalizeSimChannelId(channelId))
} catch (_error) {
// noop
}
}
function applySimChannelId(channelId, persist) {
state.simChannelId = normalizeSimChannelId(channelId)
if (elements.simChannelIdInput) {
elements.simChannelIdInput.value = state.simChannelId
}
if (persist) {
saveSimChannelId(state.simChannelId)
}
renderDebugLog()
}
function logDebug(entry) {
if (!elements.debugLog) {
return
@@ -184,6 +220,7 @@
const normalized = {
timestamp: entry.timestamp || Date.now(),
channelId: normalizeSimChannelId(entry.channelId),
scope: String(entry.scope || 'app'),
level: String(entry.level || 'info'),
message: String(entry.message || ''),
@@ -231,12 +268,15 @@
}
const filteredEntries = state.debugLogEntries.filter((entry) => {
if (normalizeSimChannelId(entry.channelId) !== state.simChannelId) {
return false
}
return state.debugLogScopeFilter === 'all' || entry.scope === state.debugLogScopeFilter
})
if (elements.debugLogMeta) {
const scopeLabel = state.debugLogScopeFilter === 'all' ? '全部' : state.debugLogScopeFilter
elements.debugLogMeta.textContent = `${scopeLabel} · ${filteredEntries.length}`
elements.debugLogMeta.textContent = `通道 ${state.simChannelId} · ${scopeLabel} · ${filteredEntries.length}`
}
elements.debugLog.textContent = filteredEntries
@@ -244,7 +284,7 @@
const time = new Date(entry.timestamp || Date.now()).toLocaleTimeString()
const level = String(entry.level || 'info').toUpperCase()
const payloadText = entry.payload ? ` ${JSON.stringify(entry.payload)}` : ''
return `[${time}] [${entry.scope}] [${level}] ${entry.message}${payloadText}`
return `[${time}] [${entry.channelId}] [${entry.scope}] [${level}] ${entry.message}${payloadText}`
})
.join('\n')
}
@@ -1389,6 +1429,7 @@
const payload = {
type: 'mock_gps',
timestamp: Date.now(),
channelId: state.simChannelId,
lat: Number(state.currentLatLng.lat.toFixed(6)),
lon: Number(state.currentLatLng.lng.toFixed(6)),
accuracyMeters: getAccuracy(),
@@ -1409,6 +1450,7 @@
const payload = {
type: 'mock_heart_rate',
timestamp: Date.now(),
channelId: state.simChannelId,
bpm: state.heartRateSampleMode ? getSampleHeartRateBpm() : getHeartRateBpm(),
}
state.heartRateSocket.send(JSON.stringify(payload))
@@ -1840,6 +1882,12 @@
})
elements.connectBtn.addEventListener('click', connectSocket)
if (elements.simChannelIdInput) {
elements.simChannelIdInput.addEventListener('change', () => {
applySimChannelId(elements.simChannelIdInput.value, true)
log(`已切换模拟通道 ${state.simChannelId}`)
})
}
elements.applyGatewayBridgePresetBtn.addEventListener('click', applyBridgePresetToForm)
elements.saveGatewayBridgePresetBtn.addEventListener('click', saveCurrentBridgePreset)
elements.deleteGatewayBridgePresetBtn.addEventListener('click', deleteSelectedBridgePreset)

View File

@@ -1,231 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mock GPS Simulator v1</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<link rel="stylesheet" href="../style.css">
</head>
<body>
<div class="layout">
<aside class="panel">
<div class="panel__header">
<div class="panel__eyebrow">MOCK GPS SIM</div>
<h1>外部模拟器</h1>
<div class="panel__links"><a href="/">打开新版工作台</a></div>
<div id="socketStatus" class="badge badge--muted">未连接</div>
</div>
<section class="group">
<div class="group__title">资源加载</div>
<label class="field">
<span>游戏配置 URL</span>
<input id="configUrlInput" type="text" value="https://oss-mbh5.colormaprun.com/wxmini/test/game.json">
</label>
<div class="row">
<button id="loadConfigBtn" class="btn btn--primary">载入配置</button>
<button id="fitCourseBtn" class="btn">适配视野</button>
</div>
<label class="field">
<span>瓦片模板</span>
<input id="tileUrlInput" type="text" placeholder="https://host/tiles/{z}/{x}/{y}.webp">
</label>
<div class="row">
<button id="applyTilesBtn" class="btn">应用瓦片</button>
<button id="resetTilesBtn" class="btn">恢复 OSM</button>
</div>
<label class="field">
<span>KML URL</span>
<input id="courseUrlInput" type="text" placeholder="https://host/course/c01.kml">
</label>
<div class="row">
<button id="loadCourseBtn" class="btn">载入控制点</button>
<button id="clearCourseBtn" class="btn">清空控制点</button>
</div>
<div id="resourceStatus" class="hint">支持直接载入 game.json也支持单独填瓦片模板和 KML 地址。</div>
<div id="resourceDetail" class="group__status">尚未载入资源</div>
<div id="courseJumpList" class="jump-list"></div>
</section>
<section class="group">
<div class="group__title">实时发送</div>
<div id="realtimeStatus" class="group__status">桥接未连接</div>
<div id="lastSendStatus" class="group__status">最近发送: --</div>
<div class="row">
<button id="connectBtn" class="btn btn--primary">连接桥接</button>
<button id="sendOnceBtn" class="btn">发送一次</button>
</div>
<div class="row">
<button id="streamBtn" class="btn btn--accent">开始连续发送</button>
<button id="stopStreamBtn" class="btn">停止发送</button>
</div>
<label class="field">
<span>发送频率</span>
<select id="hzSelect">
<option value="2">2 Hz</option>
<option value="5" selected>5 Hz</option>
<option value="10">10 Hz</option>
</select>
</label>
<label class="field">
<span>精度 (m)</span>
<input id="accuracyInput" type="number" min="1" max="100" value="6">
</label>
</section>
<section class="group">
<div class="group__title">新网关桥接</div>
<div id="gatewayBridgeStatus" class="group__status">未启用</div>
<div id="gatewayBridgeTarget" class="group__status">目标设备: --</div>
<div id="gatewayBridgeLast" class="group__status">最近状态: --</div>
<label class="field">
<span>桥接预设</span>
<select id="gatewayBridgePresetSelect">
<option value="">选择预设</option>
</select>
</label>
<label class="field">
<span>预设名称</span>
<input id="gatewayBridgePresetNameInput" type="text" placeholder="例如:家长端-A / 场控-B">
</label>
<div class="row">
<button id="applyGatewayBridgePresetBtn" class="btn">套用预设</button>
<button id="saveGatewayBridgePresetBtn" class="btn">保存预设</button>
</div>
<div class="row">
<button id="deleteGatewayBridgePresetBtn" class="btn">删除预设</button>
</div>
<label class="field field--check">
<input id="gatewayBridgeEnabledInput" type="checkbox">
<span>启用新网关桥接</span>
</label>
<label class="field">
<span>网关地址</span>
<input id="gatewayBridgeUrlInput" type="text" placeholder="ws://127.0.0.1:18080/ws">
</label>
<label class="field">
<span>Producer Token / Channel Token</span>
<input id="gatewayBridgeTokenInput" type="text" placeholder="producerToken 或 dev-producer-token">
</label>
<label class="field">
<span>Channel ID</span>
<input id="gatewayBridgeChannelIdInput" type="text" placeholder="ch-xxxx">
</label>
<label class="field">
<span>目标 Device ID</span>
<input id="gatewayBridgeDeviceIdInput" type="text" placeholder="child-001">
</label>
<label class="field">
<span>目标 Group ID</span>
<input id="gatewayBridgeGroupIdInput" type="text" placeholder="class-a">
</label>
<label class="field">
<span>Source ID</span>
<input id="gatewayBridgeSourceIdInput" type="text" placeholder="mock-gps-sim">
</label>
<label class="field">
<span>Source Mode</span>
<input id="gatewayBridgeSourceModeInput" type="text" placeholder="mock">
</label>
<div class="row">
<button id="applyGatewayBridgeConfigBtn" class="btn btn--primary">应用桥接配置</button>
<button id="reloadGatewayBridgeConfigBtn" class="btn">重新读取</button>
</div>
</section>
<section class="group">
<div class="group__title">心率模拟</div>
<div id="heartRateStatus" class="group__status">心率模拟待命</div>
<div id="lastHeartRateStatus" class="group__status">最近发送: --</div>
<div class="row">
<button id="sendHeartRateOnceBtn" class="btn">发送一次</button>
<button id="startHeartRateStreamBtn" class="btn btn--accent">开始连续发送</button>
</div>
<div class="row">
<button id="stopHeartRateStreamBtn" class="btn">停止发送</button>
<button id="applyHeartRatePresetBtn" class="btn">应用分区样本</button>
</div>
<div class="row">
<button id="toggleHeartRateSampleBtn" class="btn">模拟真实样本</button>
</div>
<label class="field">
<span>心率值 (bpm)</span>
<input id="heartRateInput" type="number" min="40" max="220" value="120">
</label>
<label class="field">
<span>发送频率</span>
<select id="heartRateHzSelect">
<option value="1" selected>1 Hz</option>
<option value="2">2 Hz</option>
<option value="4">4 Hz</option>
</select>
</label>
<label class="field">
<span>样本模板</span>
<select id="heartRateSampleTemplateSelect">
<option value="jog" selected>慢跑样本</option>
<option value="tempo">节奏跑样本</option>
<option value="interval">间歇跑样本</option>
<option value="recovery">恢复走样本</option>
</select>
</label>
</section>
<section class="group">
<div class="group__title">路径回放</div>
<div id="playbackStatus" class="group__status">路径待命</div>
<input id="trackFileInput" class="file-input-hidden" type="file" accept=".gpx,.kml,.geojson,.json,application/json,application/gpx+xml,application/vnd.google-earth.kml+xml">
<div class="row">
<button id="importTrackBtn" class="btn">导入轨迹文件</button>
<button id="togglePathModeBtn" class="btn">开启路径编辑</button>
</div>
<div class="row">
<button id="clearPathBtn" class="btn">清空路径</button>
<button id="fitPathBtn" class="btn">适配路径</button>
</div>
<div class="row">
<button id="playPathBtn" class="btn btn--accent">开始回放</button>
<button id="pausePathBtn" class="btn">暂停回放</button>
</div>
<label class="field">
<span>移动速度 (km/h)</span>
<input id="speedInput" type="number" min="1" max="25" step="0.1" value="6">
</label>
<label class="field field--check">
<input id="loopPathInput" type="checkbox" checked>
<span>循环回放</span>
</label>
<div id="pathHint" class="hint">点击“开启路径编辑”后,在地图上逐点添加路径。</div>
</section>
<section class="group">
<div class="group__title">当前位置</div>
<div class="stat"><span>纬度</span><strong id="latText">--</strong></div>
<div class="stat"><span>经度</span><strong id="lonText">--</strong></div>
<div class="stat"><span>航向</span><strong id="headingText">--</strong></div>
<div class="stat"><span>路径点</span><strong id="pathCountText">0</strong></div>
</section>
<section class="group">
<div class="group__title">日志</div>
<div id="log" class="log"></div>
</section>
</aside>
<main class="map-shell">
<div id="map"></div>
<section class="floating-debug-log">
<div class="floating-debug-log__header">
<div class="floating-debug-log__title">调试日志</div>
<button id="clearDebugLogBtn" class="floating-debug-log__clear" type="button">清空</button>
</div>
<div id="debugLog" class="log log--debug log--floating"></div>
</section>
</main>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="../simulator.js"></script>
</body>
</html>

View File

@@ -48,6 +48,7 @@ body {
display: flex;
flex-wrap: wrap;
gap: 14px;
align-items: center;
}
.wb-topbar__links a {
@@ -63,6 +64,26 @@ body {
gap: 16px;
}
.wb-topbar__global {
min-width: 220px;
}
.wb-topbar__field {
margin-bottom: 0;
}
.wb-topbar__field span {
font-size: 12px;
font-weight: 700;
letter-spacing: 0.06em;
color: #5d786c;
}
.wb-topbar__field input {
min-width: 220px;
background: rgba(255, 255, 255, 0.92);
}
.wb-connection-bar {
display: flex;
flex-wrap: wrap;
@@ -104,7 +125,7 @@ body {
.wb-layout {
min-height: 0;
display: grid;
grid-template-columns: 380px 1fr;
grid-template-columns: 380px 1fr 280px;
gap: 18px;
padding: 18px;
}
@@ -115,6 +136,12 @@ body {
padding-right: 4px;
}
.wb-rail {
min-height: 0;
overflow-y: auto;
padding-left: 4px;
}
.wb-stage {
position: relative;
min-height: 0;
@@ -123,14 +150,6 @@ body {
box-shadow: 0 28px 60px rgba(20, 41, 31, 0.18);
}
.wb-bottom-strip {
padding: 0 18px 18px;
}
.wb-card--bottom .log {
max-height: 180px;
}
#map {
width: 100%;
height: 100%;
@@ -515,7 +534,7 @@ body {
@media (max-width: 1380px) {
.wb-layout {
grid-template-columns: 340px 1fr;
grid-template-columns: 340px 1fr 250px;
}
.floating-debug-log {
@@ -526,10 +545,11 @@ body {
@media (max-width: 1120px) {
.wb-layout {
grid-template-columns: 1fr;
grid-template-rows: auto minmax(420px, 1fr);
grid-template-rows: auto minmax(420px, 1fr) auto;
}
.wb-sidebar {
.wb-sidebar,
.wb-rail {
max-height: 32vh;
}
@@ -549,7 +569,9 @@ body {
align-items: flex-start;
}
.wb-bottom-strip {
padding-top: 18px;
.wb-topbar__global,
.wb-topbar__field input {
width: 100%;
min-width: 0;
}
}