Add realtime gateway and simulator bridge
This commit is contained in:
@@ -27,6 +27,122 @@ npm run mock-gps-sim
|
||||
- 上传轨迹文件回放(GPX / KML / GeoJSON)
|
||||
- 路径回放
|
||||
- 速度、频率、精度调节
|
||||
- 可选桥接到新实时网关
|
||||
|
||||
## 桥接到新网关
|
||||
|
||||
旧模拟器现在支持保留原有本地广播链路的同时,把数据旁路转发到新的 Go 实时网关。
|
||||
|
||||
默认行为:
|
||||
|
||||
- 小程序仍可继续连接 `ws://127.0.0.1:17865/mock-gps`
|
||||
- 页面里可以直接配置并启用新网关桥接
|
||||
- 环境变量只作为服务启动时的默认值
|
||||
|
||||
### 页面里直接配置
|
||||
|
||||
启动模拟器后,打开:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:17865/
|
||||
```
|
||||
|
||||
在“新网关桥接”区域可以直接配置:
|
||||
|
||||
- 是否启用桥接
|
||||
- 网关地址
|
||||
- Producer Token
|
||||
- Channel ID
|
||||
- 目标 Device ID
|
||||
- Group ID
|
||||
- Source ID
|
||||
- Source Mode
|
||||
- 本地桥接预设
|
||||
|
||||
点“应用桥接配置”后立即生效,不需要重启模拟器。
|
||||
|
||||
预设说明:
|
||||
|
||||
- 预设保存在浏览器本地存储
|
||||
- 适合多人联调时快速切换 `deviceId / groupId / sourceId`
|
||||
- “套用预设”只会填入表单,不会自动提交到服务端
|
||||
- 需要再点一次“应用桥接配置”才会真正切换运行时桥接目标
|
||||
|
||||
### PowerShell 启动示例
|
||||
|
||||
在仓库根目录执行:
|
||||
|
||||
```powershell
|
||||
$env:MOCK_SIM_GATEWAY_ENABLED='1'
|
||||
$env:MOCK_SIM_GATEWAY_URL='ws://127.0.0.1:18080/ws'
|
||||
$env:MOCK_SIM_GATEWAY_TOKEN='dev-producer-token'
|
||||
$env:MOCK_SIM_GATEWAY_CHANNEL_ID=''
|
||||
$env:MOCK_SIM_GATEWAY_DEVICE_ID='child-001'
|
||||
$env:MOCK_SIM_GATEWAY_SOURCE_ID='mock-gps-sim-a'
|
||||
npm run mock-gps-sim
|
||||
```
|
||||
|
||||
如果你使用新网关管理台创建的 `channel`,则要这样填:
|
||||
|
||||
```powershell
|
||||
$env:MOCK_SIM_GATEWAY_ENABLED='1'
|
||||
$env:MOCK_SIM_GATEWAY_URL='ws://127.0.0.1:18080/ws'
|
||||
$env:MOCK_SIM_GATEWAY_TOKEN='<producerToken>'
|
||||
$env:MOCK_SIM_GATEWAY_CHANNEL_ID='<channelId>'
|
||||
$env:MOCK_SIM_GATEWAY_DEVICE_ID='child-001'
|
||||
npm run mock-gps-sim
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 不填 `MOCK_SIM_GATEWAY_CHANNEL_ID` 时,旧模拟器走老的 `authenticate` 模式
|
||||
- 填了 `MOCK_SIM_GATEWAY_CHANNEL_ID` 时,旧模拟器自动走 `join_channel` 模式
|
||||
- 管理台里复制出来的 `producerToken` 只能和对应的 `channelId` 配套使用
|
||||
|
||||
### 可用环境变量
|
||||
|
||||
- `MOCK_SIM_GATEWAY_ENABLED`
|
||||
- `1` 表示启用桥接
|
||||
- `MOCK_SIM_GATEWAY_URL`
|
||||
- 新网关地址,默认 `ws://127.0.0.1:18080/ws`
|
||||
- `MOCK_SIM_GATEWAY_TOKEN`
|
||||
- Producer token,默认 `dev-producer-token`
|
||||
- `MOCK_SIM_GATEWAY_CHANNEL_ID`
|
||||
- 可选 channel id;填写后会改走 `join_channel`
|
||||
- `MOCK_SIM_GATEWAY_DEVICE_ID`
|
||||
- 转发目标 `deviceId`,默认 `child-001`
|
||||
- `MOCK_SIM_GATEWAY_GROUP_ID`
|
||||
- 可选 `groupId`
|
||||
- `MOCK_SIM_GATEWAY_SOURCE_ID`
|
||||
- source id,默认 `mock-gps-sim`
|
||||
- `MOCK_SIM_GATEWAY_SOURCE_MODE`
|
||||
- source mode,默认 `mock`
|
||||
- `MOCK_SIM_GATEWAY_RECONNECT_MS`
|
||||
- 断线重连间隔,默认 `3000`
|
||||
|
||||
### 桥接状态查看
|
||||
|
||||
启动后可查看:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:17865/bridge-status
|
||||
```
|
||||
|
||||
桥接配置接口:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:17865/bridge-config
|
||||
```
|
||||
|
||||
返回内容包含:
|
||||
|
||||
- 是否启用桥接
|
||||
- 是否已连上新网关
|
||||
- 是否已认证
|
||||
- 最近发送 topic
|
||||
- 已发送条数
|
||||
- 丢弃条数
|
||||
- 最近错误
|
||||
|
||||
## 加载自己的地图
|
||||
|
||||
|
||||
@@ -73,6 +73,66 @@
|
||||
</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>
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
const DEFAULT_TILE_URL = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
|
||||
const PROXY_BASE_URL = `${location.origin}/proxy?url=`
|
||||
const WS_URL = `ws://${location.hostname}:17865/mock-gps`
|
||||
const DEFAULT_GATEWAY_BRIDGE_URL = 'ws://127.0.0.1:18080/ws'
|
||||
const LEGACY_GATEWAY_BRIDGE_URLS = new Set([
|
||||
'ws://127.0.0.1:8080/ws',
|
||||
'ws://localhost:8080/ws',
|
||||
])
|
||||
const BRIDGE_CONFIG_STORAGE_KEY = 'mock-gps-sim.bridge-config'
|
||||
const BRIDGE_PRESETS_STORAGE_KEY = 'mock-gps-sim.bridge-presets'
|
||||
|
||||
const map = L.map('map').setView(DEFAULT_CENTER, 16)
|
||||
let tileLayer = createTileLayer(DEFAULT_TILE_URL, {
|
||||
@@ -52,6 +59,13 @@
|
||||
heartRateSampleStartedAt: 0,
|
||||
loadedCourse: null,
|
||||
resourceLoading: false,
|
||||
bridgeEnabled: false,
|
||||
bridgeConnected: false,
|
||||
bridgeAuthenticated: false,
|
||||
bridgeTargetText: '--',
|
||||
bridgeLastStatusText: '--',
|
||||
bridgeConfigSaving: false,
|
||||
bridgePresets: [],
|
||||
}
|
||||
|
||||
const elements = {
|
||||
@@ -70,6 +84,24 @@
|
||||
courseJumpList: document.getElementById('courseJumpList'),
|
||||
realtimeStatus: document.getElementById('realtimeStatus'),
|
||||
lastSendStatus: document.getElementById('lastSendStatus'),
|
||||
gatewayBridgeStatus: document.getElementById('gatewayBridgeStatus'),
|
||||
gatewayBridgeTarget: document.getElementById('gatewayBridgeTarget'),
|
||||
gatewayBridgeLast: document.getElementById('gatewayBridgeLast'),
|
||||
gatewayBridgePresetSelect: document.getElementById('gatewayBridgePresetSelect'),
|
||||
gatewayBridgePresetNameInput: document.getElementById('gatewayBridgePresetNameInput'),
|
||||
applyGatewayBridgePresetBtn: document.getElementById('applyGatewayBridgePresetBtn'),
|
||||
saveGatewayBridgePresetBtn: document.getElementById('saveGatewayBridgePresetBtn'),
|
||||
deleteGatewayBridgePresetBtn: document.getElementById('deleteGatewayBridgePresetBtn'),
|
||||
gatewayBridgeEnabledInput: document.getElementById('gatewayBridgeEnabledInput'),
|
||||
gatewayBridgeUrlInput: document.getElementById('gatewayBridgeUrlInput'),
|
||||
gatewayBridgeTokenInput: document.getElementById('gatewayBridgeTokenInput'),
|
||||
gatewayBridgeChannelIdInput: document.getElementById('gatewayBridgeChannelIdInput'),
|
||||
gatewayBridgeDeviceIdInput: document.getElementById('gatewayBridgeDeviceIdInput'),
|
||||
gatewayBridgeGroupIdInput: document.getElementById('gatewayBridgeGroupIdInput'),
|
||||
gatewayBridgeSourceIdInput: document.getElementById('gatewayBridgeSourceIdInput'),
|
||||
gatewayBridgeSourceModeInput: document.getElementById('gatewayBridgeSourceModeInput'),
|
||||
applyGatewayBridgeConfigBtn: document.getElementById('applyGatewayBridgeConfigBtn'),
|
||||
reloadGatewayBridgeConfigBtn: document.getElementById('reloadGatewayBridgeConfigBtn'),
|
||||
playbackStatus: document.getElementById('playbackStatus'),
|
||||
heartRateStatus: document.getElementById('heartRateStatus'),
|
||||
lastHeartRateStatus: document.getElementById('lastHeartRateStatus'),
|
||||
@@ -190,6 +222,23 @@
|
||||
elements.lastSendStatus.textContent = `最近发送: ${state.lastSentText}`
|
||||
elements.lastHeartRateStatus.textContent = `最近发送: ${state.lastHeartRateSentText}`
|
||||
elements.resourceDetail.textContent = state.lastResourceDetailText
|
||||
elements.gatewayBridgeTarget.textContent = `目标设备: ${state.bridgeTargetText}`
|
||||
elements.gatewayBridgeLast.textContent = `最近状态: ${state.bridgeLastStatusText}`
|
||||
elements.applyGatewayBridgeConfigBtn.disabled = state.bridgeConfigSaving
|
||||
elements.reloadGatewayBridgeConfigBtn.disabled = state.bridgeConfigSaving
|
||||
elements.applyGatewayBridgePresetBtn.disabled = state.bridgeConfigSaving || !elements.gatewayBridgePresetSelect.value
|
||||
elements.saveGatewayBridgePresetBtn.disabled = state.bridgeConfigSaving
|
||||
elements.deleteGatewayBridgePresetBtn.disabled = state.bridgeConfigSaving || !elements.gatewayBridgePresetSelect.value
|
||||
|
||||
if (!state.bridgeEnabled) {
|
||||
elements.gatewayBridgeStatus.textContent = '未启用'
|
||||
} else if (state.bridgeConnected && state.bridgeAuthenticated) {
|
||||
elements.gatewayBridgeStatus.textContent = '已连接并已认证'
|
||||
} else if (state.bridgeConnected) {
|
||||
elements.gatewayBridgeStatus.textContent = '已连接,等待认证'
|
||||
} else {
|
||||
elements.gatewayBridgeStatus.textContent = '已启用,未连接'
|
||||
}
|
||||
|
||||
if (state.connected && state.streaming) {
|
||||
elements.realtimeStatus.textContent = `桥接已连接,正在以 ${elements.hzSelect.value} Hz 连续发送`
|
||||
@@ -224,6 +273,204 @@
|
||||
}
|
||||
}
|
||||
|
||||
function bridgeConfigFromServerPayload(payload) {
|
||||
const config = payload && payload.config ? payload.config : {}
|
||||
return {
|
||||
enabled: Boolean(config.enabled),
|
||||
url: normalizeGatewayBridgeUrl(typeof config.url === 'string' ? config.url : ''),
|
||||
token: typeof config.token === 'string' ? config.token : '',
|
||||
channelId: typeof config.channelId === 'string' ? config.channelId : '',
|
||||
deviceId: typeof config.deviceId === 'string' ? config.deviceId : '',
|
||||
groupId: typeof config.groupId === 'string' ? config.groupId : '',
|
||||
sourceId: typeof config.sourceId === 'string' ? config.sourceId : '',
|
||||
sourceMode: typeof config.sourceMode === 'string' ? config.sourceMode : 'mock',
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeGatewayBridgeUrl(value) {
|
||||
const next = String(value || '').trim()
|
||||
if (!next) {
|
||||
return DEFAULT_GATEWAY_BRIDGE_URL
|
||||
}
|
||||
if (LEGACY_GATEWAY_BRIDGE_URLS.has(next)) {
|
||||
return DEFAULT_GATEWAY_BRIDGE_URL
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
function getBridgeConfigDraft() {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(BRIDGE_CONFIG_STORAGE_KEY)
|
||||
if (!raw) {
|
||||
return null
|
||||
}
|
||||
const parsed = JSON.parse(raw)
|
||||
return {
|
||||
enabled: Boolean(parsed.enabled),
|
||||
url: normalizeGatewayBridgeUrl(typeof parsed.url === 'string' ? parsed.url : ''),
|
||||
token: typeof parsed.token === 'string' ? parsed.token : '',
|
||||
channelId: typeof parsed.channelId === 'string' ? parsed.channelId : '',
|
||||
deviceId: typeof parsed.deviceId === 'string' ? parsed.deviceId : '',
|
||||
groupId: typeof parsed.groupId === 'string' ? parsed.groupId : '',
|
||||
sourceId: typeof parsed.sourceId === 'string' ? parsed.sourceId : '',
|
||||
sourceMode: typeof parsed.sourceMode === 'string' ? parsed.sourceMode : 'mock',
|
||||
}
|
||||
} catch (_error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function loadBridgePresets() {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(BRIDGE_PRESETS_STORAGE_KEY)
|
||||
if (!raw) {
|
||||
return []
|
||||
}
|
||||
const parsed = JSON.parse(raw)
|
||||
if (!Array.isArray(parsed)) {
|
||||
return []
|
||||
}
|
||||
return parsed
|
||||
.map((item) => {
|
||||
const config = item && item.config ? item.config : {}
|
||||
return {
|
||||
name: item && typeof item.name === 'string' ? item.name.trim() : '',
|
||||
config: {
|
||||
enabled: Boolean(config.enabled),
|
||||
url: normalizeGatewayBridgeUrl(typeof config.url === 'string' ? config.url : ''),
|
||||
token: typeof config.token === 'string' ? config.token : '',
|
||||
channelId: typeof config.channelId === 'string' ? config.channelId : '',
|
||||
deviceId: typeof config.deviceId === 'string' ? config.deviceId : '',
|
||||
groupId: typeof config.groupId === 'string' ? config.groupId : '',
|
||||
sourceId: typeof config.sourceId === 'string' ? config.sourceId : '',
|
||||
sourceMode: typeof config.sourceMode === 'string' ? config.sourceMode : 'mock',
|
||||
},
|
||||
}
|
||||
})
|
||||
.filter((item) => item.name)
|
||||
} catch (_error) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function saveBridgePresets() {
|
||||
try {
|
||||
window.localStorage.setItem(BRIDGE_PRESETS_STORAGE_KEY, JSON.stringify(state.bridgePresets))
|
||||
} catch (_error) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
function renderBridgePresetOptions(selectedName) {
|
||||
const currentValue = typeof selectedName === 'string'
|
||||
? selectedName
|
||||
: elements.gatewayBridgePresetSelect.value
|
||||
elements.gatewayBridgePresetSelect.innerHTML = '<option value="">选择预设</option>'
|
||||
|
||||
state.bridgePresets.forEach((preset) => {
|
||||
const option = document.createElement('option')
|
||||
option.value = preset.name
|
||||
option.textContent = preset.name
|
||||
if (preset.name === currentValue) {
|
||||
option.selected = true
|
||||
}
|
||||
elements.gatewayBridgePresetSelect.appendChild(option)
|
||||
})
|
||||
}
|
||||
|
||||
function saveBridgeConfigDraft(config) {
|
||||
try {
|
||||
window.localStorage.setItem(BRIDGE_CONFIG_STORAGE_KEY, JSON.stringify({
|
||||
...config,
|
||||
url: normalizeGatewayBridgeUrl(config && config.url),
|
||||
}))
|
||||
} catch (_error) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
function fillBridgeConfigForm(config) {
|
||||
elements.gatewayBridgeEnabledInput.checked = Boolean(config.enabled)
|
||||
elements.gatewayBridgeUrlInput.value = normalizeGatewayBridgeUrl(config && config.url)
|
||||
elements.gatewayBridgeTokenInput.value = config.token || ''
|
||||
elements.gatewayBridgeChannelIdInput.value = config.channelId || ''
|
||||
elements.gatewayBridgeDeviceIdInput.value = config.deviceId || ''
|
||||
elements.gatewayBridgeGroupIdInput.value = config.groupId || ''
|
||||
elements.gatewayBridgeSourceIdInput.value = config.sourceId || ''
|
||||
elements.gatewayBridgeSourceModeInput.value = config.sourceMode || 'mock'
|
||||
}
|
||||
|
||||
function readBridgeConfigForm() {
|
||||
return {
|
||||
enabled: elements.gatewayBridgeEnabledInput.checked,
|
||||
url: normalizeGatewayBridgeUrl(elements.gatewayBridgeUrlInput.value),
|
||||
token: String(elements.gatewayBridgeTokenInput.value || '').trim(),
|
||||
channelId: String(elements.gatewayBridgeChannelIdInput.value || '').trim(),
|
||||
deviceId: String(elements.gatewayBridgeDeviceIdInput.value || '').trim(),
|
||||
groupId: String(elements.gatewayBridgeGroupIdInput.value || '').trim(),
|
||||
sourceId: String(elements.gatewayBridgeSourceIdInput.value || '').trim(),
|
||||
sourceMode: String(elements.gatewayBridgeSourceModeInput.value || '').trim() || 'mock',
|
||||
}
|
||||
}
|
||||
|
||||
function selectedBridgePreset() {
|
||||
const name = String(elements.gatewayBridgePresetSelect.value || '').trim()
|
||||
if (!name) {
|
||||
return null
|
||||
}
|
||||
return state.bridgePresets.find((item) => item.name === name) || null
|
||||
}
|
||||
|
||||
function applyBridgePresetToForm() {
|
||||
const preset = selectedBridgePreset()
|
||||
if (!preset) {
|
||||
log('未选择桥接预设')
|
||||
return
|
||||
}
|
||||
fillBridgeConfigForm(preset.config)
|
||||
elements.gatewayBridgePresetNameInput.value = preset.name
|
||||
saveBridgeConfigDraft(preset.config)
|
||||
updateUiState()
|
||||
log(`已载入桥接预设: ${preset.name}`)
|
||||
}
|
||||
|
||||
function saveCurrentBridgePreset() {
|
||||
const name = String(elements.gatewayBridgePresetNameInput.value || '').trim()
|
||||
if (!name) {
|
||||
log('请先输入预设名称')
|
||||
return
|
||||
}
|
||||
const config = readBridgeConfigForm()
|
||||
const nextPreset = { name, config }
|
||||
const existingIndex = state.bridgePresets.findIndex((item) => item.name === name)
|
||||
if (existingIndex >= 0) {
|
||||
state.bridgePresets.splice(existingIndex, 1, nextPreset)
|
||||
} else {
|
||||
state.bridgePresets.push(nextPreset)
|
||||
state.bridgePresets.sort((left, right) => left.name.localeCompare(right.name, 'zh-CN'))
|
||||
}
|
||||
saveBridgePresets()
|
||||
renderBridgePresetOptions(name)
|
||||
log(`已保存桥接预设: ${name}`)
|
||||
updateUiState()
|
||||
}
|
||||
|
||||
function deleteSelectedBridgePreset() {
|
||||
const preset = selectedBridgePreset()
|
||||
if (!preset) {
|
||||
log('未选择桥接预设')
|
||||
return
|
||||
}
|
||||
state.bridgePresets = state.bridgePresets.filter((item) => item.name !== preset.name)
|
||||
saveBridgePresets()
|
||||
renderBridgePresetOptions('')
|
||||
if (elements.gatewayBridgePresetNameInput.value.trim() === preset.name) {
|
||||
elements.gatewayBridgePresetNameInput.value = ''
|
||||
}
|
||||
log(`已删除桥接预设: ${preset.name}`)
|
||||
updateUiState()
|
||||
}
|
||||
|
||||
function connectSocket() {
|
||||
if (state.socket && (state.socket.readyState === WebSocket.OPEN || state.socket.readyState === WebSocket.CONNECTING)) {
|
||||
return
|
||||
@@ -265,6 +512,100 @@
|
||||
})
|
||||
}
|
||||
|
||||
async function refreshGatewayBridgeStatus() {
|
||||
try {
|
||||
const response = await fetch('/bridge-status', {
|
||||
cache: 'no-store',
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
const status = await response.json()
|
||||
state.bridgeEnabled = Boolean(status.enabled)
|
||||
state.bridgeConnected = Boolean(status.connected)
|
||||
state.bridgeAuthenticated = Boolean(status.authenticated)
|
||||
if (status.channelId) {
|
||||
state.bridgeTargetText = `${status.channelId}${status.deviceId ? ` / ${status.deviceId}` : ''}${status.groupId ? ` / ${status.groupId}` : ''}`
|
||||
} else {
|
||||
state.bridgeTargetText = status.deviceId
|
||||
? `${status.deviceId}${status.groupId ? ` / ${status.groupId}` : ''}`
|
||||
: '--'
|
||||
}
|
||||
state.bridgeLastStatusText = status.lastError
|
||||
? `错误: ${status.lastError}`
|
||||
: status.lastSentAt
|
||||
? `${status.lastSentTopic || 'unknown'} @ ${formatClockTime(status.lastSentAt)}`
|
||||
: '待命'
|
||||
updateUiState()
|
||||
} catch (_error) {
|
||||
state.bridgeEnabled = false
|
||||
state.bridgeConnected = false
|
||||
state.bridgeAuthenticated = false
|
||||
state.bridgeTargetText = '--'
|
||||
state.bridgeLastStatusText = '状态读取失败'
|
||||
updateUiState()
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGatewayBridgeConfig(options) {
|
||||
const preserveForm = Boolean(options && options.preserveForm)
|
||||
const response = await fetch('/bridge-config', {
|
||||
cache: 'no-store',
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`桥接配置读取失败: HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
const payload = await response.json()
|
||||
if (!preserveForm) {
|
||||
fillBridgeConfigForm(bridgeConfigFromServerPayload(payload))
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
async function applyGatewayBridgeConfig() {
|
||||
const config = readBridgeConfigForm()
|
||||
if (!config.url) {
|
||||
log('桥接配置缺少网关地址')
|
||||
return
|
||||
}
|
||||
if (!config.deviceId) {
|
||||
log('桥接配置缺少目标 Device ID')
|
||||
return
|
||||
}
|
||||
if (!config.sourceId) {
|
||||
log('桥接配置缺少 Source ID')
|
||||
return
|
||||
}
|
||||
|
||||
state.bridgeConfigSaving = true
|
||||
updateUiState()
|
||||
try {
|
||||
const response = await fetch('/bridge-config', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(config),
|
||||
})
|
||||
const payload = await response.json()
|
||||
if (!response.ok) {
|
||||
throw new Error(payload && payload.error ? payload.error : `HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
saveBridgeConfigDraft(config)
|
||||
fillBridgeConfigForm(bridgeConfigFromServerPayload(payload))
|
||||
await refreshGatewayBridgeStatus()
|
||||
log(`已应用新网关桥接配置 -> ${config.deviceId}`)
|
||||
} catch (error) {
|
||||
log(error && error.message ? error.message : '桥接配置应用失败')
|
||||
} finally {
|
||||
state.bridgeConfigSaving = false
|
||||
updateUiState()
|
||||
}
|
||||
}
|
||||
|
||||
function proxyUrl(targetUrl) {
|
||||
return `${PROXY_BASE_URL}${encodeURIComponent(targetUrl)}`
|
||||
}
|
||||
@@ -1257,6 +1598,24 @@
|
||||
})
|
||||
|
||||
elements.connectBtn.addEventListener('click', connectSocket)
|
||||
elements.applyGatewayBridgePresetBtn.addEventListener('click', applyBridgePresetToForm)
|
||||
elements.saveGatewayBridgePresetBtn.addEventListener('click', saveCurrentBridgePreset)
|
||||
elements.deleteGatewayBridgePresetBtn.addEventListener('click', deleteSelectedBridgePreset)
|
||||
elements.gatewayBridgePresetSelect.addEventListener('change', () => {
|
||||
const preset = selectedBridgePreset()
|
||||
elements.gatewayBridgePresetNameInput.value = preset ? preset.name : ''
|
||||
updateUiState()
|
||||
})
|
||||
elements.applyGatewayBridgeConfigBtn.addEventListener('click', applyGatewayBridgeConfig)
|
||||
elements.reloadGatewayBridgeConfigBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
await loadGatewayBridgeConfig()
|
||||
await refreshGatewayBridgeStatus()
|
||||
log('已重新读取桥接配置')
|
||||
} catch (error) {
|
||||
log(error && error.message ? error.message : '桥接配置读取失败')
|
||||
}
|
||||
})
|
||||
elements.importTrackBtn.addEventListener('click', () => {
|
||||
elements.trackFileInput.click()
|
||||
})
|
||||
@@ -1335,6 +1694,25 @@
|
||||
updateReadout()
|
||||
setSocketBadge(false)
|
||||
setResourceStatus('支持直接载入 game.json,也支持单独填瓦片模板和 KML 地址。', null)
|
||||
state.bridgePresets = loadBridgePresets()
|
||||
renderBridgePresetOptions('')
|
||||
updateUiState()
|
||||
const draftBridgeConfig = getBridgeConfigDraft()
|
||||
if (draftBridgeConfig) {
|
||||
fillBridgeConfigForm(draftBridgeConfig)
|
||||
}
|
||||
loadGatewayBridgeConfig({ preserveForm: Boolean(draftBridgeConfig) })
|
||||
.then(async () => {
|
||||
if (draftBridgeConfig) {
|
||||
log('已恢复上次桥接配置草稿,可直接点“应用桥接配置”')
|
||||
}
|
||||
await refreshGatewayBridgeStatus()
|
||||
})
|
||||
.catch((error) => {
|
||||
log(error && error.message ? error.message : '桥接配置读取失败')
|
||||
refreshGatewayBridgeStatus()
|
||||
})
|
||||
refreshGatewayBridgeStatus()
|
||||
window.setInterval(refreshGatewayBridgeStatus, 3000)
|
||||
connectSocket()
|
||||
})()
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
const http = require('http')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { WebSocketServer } = require('ws')
|
||||
const WebSocket = require('ws')
|
||||
const { WebSocketServer } = WebSocket
|
||||
|
||||
const HOST = '0.0.0.0'
|
||||
const PORT = 17865
|
||||
const WS_PATH = '/mock-gps'
|
||||
const PROXY_PATH = '/proxy'
|
||||
const BRIDGE_STATUS_PATH = '/bridge-status'
|
||||
const BRIDGE_CONFIG_PATH = '/bridge-config'
|
||||
const PUBLIC_DIR = path.join(__dirname, 'public')
|
||||
const DEFAULT_GATEWAY_BRIDGE_URL = 'ws://127.0.0.1:18080/ws'
|
||||
|
||||
const INITIAL_BRIDGE_CONFIG = {
|
||||
enabled: process.env.MOCK_SIM_GATEWAY_ENABLED === '1',
|
||||
url: process.env.MOCK_SIM_GATEWAY_URL || DEFAULT_GATEWAY_BRIDGE_URL,
|
||||
token: process.env.MOCK_SIM_GATEWAY_TOKEN || 'dev-producer-token',
|
||||
channelId: process.env.MOCK_SIM_GATEWAY_CHANNEL_ID || '',
|
||||
deviceId: process.env.MOCK_SIM_GATEWAY_DEVICE_ID || 'child-001',
|
||||
groupId: process.env.MOCK_SIM_GATEWAY_GROUP_ID || '',
|
||||
sourceId: process.env.MOCK_SIM_GATEWAY_SOURCE_ID || 'mock-gps-sim',
|
||||
sourceMode: process.env.MOCK_SIM_GATEWAY_SOURCE_MODE || 'mock',
|
||||
reconnectMs: Math.max(1000, Number(process.env.MOCK_SIM_GATEWAY_RECONNECT_MS) || 3000),
|
||||
}
|
||||
|
||||
function getContentType(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase()
|
||||
@@ -29,6 +45,15 @@ function getContentType(filePath) {
|
||||
return 'text/plain; charset=utf-8'
|
||||
}
|
||||
|
||||
function respondJson(response, statusCode, payload) {
|
||||
response.writeHead(statusCode, {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Cache-Control': 'no-store',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
})
|
||||
response.end(JSON.stringify(payload))
|
||||
}
|
||||
|
||||
function serveStatic(requestPath, response) {
|
||||
const safePath = requestPath === '/' ? '/index.html' : requestPath
|
||||
const resolvedPath = path.normalize(path.join(PUBLIC_DIR, safePath))
|
||||
@@ -96,12 +121,379 @@ async function handleProxyRequest(request, response) {
|
||||
}
|
||||
}
|
||||
|
||||
async function readJsonBody(request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = []
|
||||
request.on('data', (chunk) => {
|
||||
chunks.push(chunk)
|
||||
})
|
||||
request.on('end', () => {
|
||||
const raw = Buffer.concat(chunks).toString('utf8').trim()
|
||||
if (!raw) {
|
||||
resolve({})
|
||||
return
|
||||
}
|
||||
try {
|
||||
resolve(JSON.parse(raw))
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
request.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeBridgeConfig(input, currentConfig) {
|
||||
const source = input || {}
|
||||
const fallback = currentConfig || INITIAL_BRIDGE_CONFIG
|
||||
|
||||
return {
|
||||
enabled: typeof source.enabled === 'boolean' ? source.enabled : fallback.enabled,
|
||||
url: typeof source.url === 'string' && source.url.trim() ? source.url.trim() : fallback.url,
|
||||
token: typeof source.token === 'string' ? source.token.trim() : fallback.token,
|
||||
channelId: typeof source.channelId === 'string' ? source.channelId.trim() : fallback.channelId,
|
||||
deviceId: typeof source.deviceId === 'string' && source.deviceId.trim() ? source.deviceId.trim() : fallback.deviceId,
|
||||
groupId: typeof source.groupId === 'string' ? source.groupId.trim() : fallback.groupId,
|
||||
sourceId: typeof source.sourceId === 'string' && source.sourceId.trim() ? source.sourceId.trim() : fallback.sourceId,
|
||||
sourceMode: typeof source.sourceMode === 'string' && source.sourceMode.trim() ? source.sourceMode.trim() : fallback.sourceMode,
|
||||
reconnectMs: Math.max(1000, Number(source.reconnectMs) || fallback.reconnectMs),
|
||||
}
|
||||
}
|
||||
|
||||
function createGatewayBridge() {
|
||||
const bridgeState = {
|
||||
config: { ...INITIAL_BRIDGE_CONFIG },
|
||||
socket: null,
|
||||
connecting: false,
|
||||
connected: false,
|
||||
authenticated: false,
|
||||
reconnectTimer: 0,
|
||||
lastError: '',
|
||||
lastSentAt: 0,
|
||||
lastSentTopic: '',
|
||||
sentCount: 0,
|
||||
droppedCount: 0,
|
||||
}
|
||||
|
||||
function logBridge(message) {
|
||||
console.log(`[gateway-bridge] ${message}`)
|
||||
}
|
||||
|
||||
function clearReconnectTimer() {
|
||||
if (!bridgeState.reconnectTimer) {
|
||||
return
|
||||
}
|
||||
clearTimeout(bridgeState.reconnectTimer)
|
||||
bridgeState.reconnectTimer = 0
|
||||
}
|
||||
|
||||
function scheduleReconnect() {
|
||||
if (!bridgeState.config.enabled || bridgeState.reconnectTimer) {
|
||||
return
|
||||
}
|
||||
bridgeState.reconnectTimer = setTimeout(() => {
|
||||
bridgeState.reconnectTimer = 0
|
||||
connect()
|
||||
}, bridgeState.config.reconnectMs)
|
||||
}
|
||||
|
||||
function resetSocketState() {
|
||||
bridgeState.socket = null
|
||||
bridgeState.connecting = false
|
||||
bridgeState.connected = false
|
||||
bridgeState.authenticated = false
|
||||
}
|
||||
|
||||
function handleGatewayMessage(rawMessage) {
|
||||
let parsed
|
||||
try {
|
||||
parsed = JSON.parse(String(rawMessage))
|
||||
} catch (_error) {
|
||||
return
|
||||
}
|
||||
|
||||
if (parsed.type === 'welcome') {
|
||||
if (!bridgeState.socket || bridgeState.socket.readyState !== WebSocket.OPEN) {
|
||||
return
|
||||
}
|
||||
if (bridgeState.config.channelId) {
|
||||
bridgeState.socket.send(JSON.stringify({
|
||||
type: 'join_channel',
|
||||
role: 'producer',
|
||||
channelId: bridgeState.config.channelId,
|
||||
token: bridgeState.config.token,
|
||||
}))
|
||||
} else {
|
||||
bridgeState.socket.send(JSON.stringify({
|
||||
type: 'authenticate',
|
||||
role: 'producer',
|
||||
token: bridgeState.config.token,
|
||||
}))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (parsed.type === 'authenticated' || parsed.type === 'joined_channel') {
|
||||
bridgeState.authenticated = true
|
||||
bridgeState.lastError = ''
|
||||
if (bridgeState.config.channelId) {
|
||||
logBridge(`joined channel=${bridgeState.config.channelId}, device=${bridgeState.config.deviceId}, source=${bridgeState.config.sourceId}`)
|
||||
} else {
|
||||
logBridge(`authenticated, device=${bridgeState.config.deviceId}, source=${bridgeState.config.sourceId}`)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (parsed.type === 'error') {
|
||||
bridgeState.lastError = parsed.error || 'gateway error'
|
||||
logBridge(`error: ${bridgeState.lastError}`)
|
||||
}
|
||||
}
|
||||
|
||||
function closeSocket() {
|
||||
if (!bridgeState.socket) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
bridgeState.socket.close()
|
||||
} catch (_error) {
|
||||
// noop
|
||||
}
|
||||
resetSocketState()
|
||||
}
|
||||
|
||||
function connect() {
|
||||
if (!bridgeState.config.enabled || bridgeState.connecting) {
|
||||
return
|
||||
}
|
||||
if (bridgeState.socket && (bridgeState.socket.readyState === WebSocket.OPEN || bridgeState.socket.readyState === WebSocket.CONNECTING)) {
|
||||
return
|
||||
}
|
||||
|
||||
clearReconnectTimer()
|
||||
bridgeState.connecting = true
|
||||
bridgeState.lastError = ''
|
||||
logBridge(`connecting to ${bridgeState.config.url}`)
|
||||
|
||||
const socket = new WebSocket(bridgeState.config.url)
|
||||
bridgeState.socket = socket
|
||||
|
||||
socket.on('open', () => {
|
||||
bridgeState.connecting = false
|
||||
bridgeState.connected = true
|
||||
logBridge('connected')
|
||||
})
|
||||
|
||||
socket.on('message', handleGatewayMessage)
|
||||
|
||||
socket.on('close', () => {
|
||||
const wasConnected = bridgeState.connected || bridgeState.authenticated
|
||||
resetSocketState()
|
||||
if (wasConnected) {
|
||||
logBridge('disconnected')
|
||||
}
|
||||
scheduleReconnect()
|
||||
})
|
||||
|
||||
socket.on('error', (error) => {
|
||||
bridgeState.lastError = error && error.message ? error.message : 'gateway socket error'
|
||||
logBridge(`socket error: ${bridgeState.lastError}`)
|
||||
})
|
||||
}
|
||||
|
||||
function toGatewayEnvelope(payload) {
|
||||
if (isMockGpsPayload(payload)) {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
messageId: `gps-${payload.timestamp}`,
|
||||
timestamp: payload.timestamp,
|
||||
topic: 'telemetry.location',
|
||||
source: {
|
||||
kind: 'producer',
|
||||
id: bridgeState.config.sourceId,
|
||||
mode: bridgeState.config.sourceMode,
|
||||
},
|
||||
target: {
|
||||
channelId: bridgeState.config.channelId,
|
||||
deviceId: bridgeState.config.deviceId,
|
||||
groupId: bridgeState.config.groupId,
|
||||
},
|
||||
payload: {
|
||||
lat: Number(payload.lat),
|
||||
lng: Number(payload.lon),
|
||||
speed: Number(payload.speedMps) || 0,
|
||||
bearing: Number(payload.headingDeg) || 0,
|
||||
accuracy: Number(payload.accuracyMeters) || 6,
|
||||
coordSystem: 'GCJ02',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if (isMockHeartRatePayload(payload)) {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
messageId: `hr-${payload.timestamp}`,
|
||||
timestamp: payload.timestamp,
|
||||
topic: 'telemetry.heart_rate',
|
||||
source: {
|
||||
kind: 'producer',
|
||||
id: bridgeState.config.sourceId,
|
||||
mode: bridgeState.config.sourceMode,
|
||||
},
|
||||
target: {
|
||||
channelId: bridgeState.config.channelId,
|
||||
deviceId: bridgeState.config.deviceId,
|
||||
groupId: bridgeState.config.groupId,
|
||||
},
|
||||
payload: {
|
||||
bpm: Math.max(1, Math.round(Number(payload.bpm))),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function publish(payload) {
|
||||
if (!bridgeState.config.enabled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!bridgeState.socket || bridgeState.socket.readyState !== WebSocket.OPEN || !bridgeState.authenticated) {
|
||||
bridgeState.droppedCount += 1
|
||||
connect()
|
||||
return
|
||||
}
|
||||
|
||||
const envelope = toGatewayEnvelope(payload)
|
||||
if (!envelope) {
|
||||
return
|
||||
}
|
||||
|
||||
bridgeState.socket.send(JSON.stringify({
|
||||
type: 'publish',
|
||||
envelope,
|
||||
}))
|
||||
bridgeState.lastSentAt = Date.now()
|
||||
bridgeState.lastSentTopic = envelope.topic
|
||||
bridgeState.sentCount += 1
|
||||
}
|
||||
|
||||
function updateConfig(nextConfigInput) {
|
||||
const nextConfig = normalizeBridgeConfig(nextConfigInput, bridgeState.config)
|
||||
const changed = JSON.stringify(nextConfig) !== JSON.stringify(bridgeState.config)
|
||||
bridgeState.config = nextConfig
|
||||
|
||||
if (!changed) {
|
||||
return getStatus()
|
||||
}
|
||||
|
||||
bridgeState.lastError = ''
|
||||
if (!bridgeState.config.enabled) {
|
||||
clearReconnectTimer()
|
||||
closeSocket()
|
||||
logBridge('disabled')
|
||||
return getStatus()
|
||||
}
|
||||
|
||||
clearReconnectTimer()
|
||||
closeSocket()
|
||||
connect()
|
||||
return getStatus()
|
||||
}
|
||||
|
||||
function getConfig() {
|
||||
return { ...bridgeState.config }
|
||||
}
|
||||
|
||||
function getStatus() {
|
||||
return {
|
||||
enabled: bridgeState.config.enabled,
|
||||
url: bridgeState.config.url,
|
||||
connected: bridgeState.connected,
|
||||
authenticated: bridgeState.authenticated,
|
||||
channelId: bridgeState.config.channelId,
|
||||
deviceId: bridgeState.config.deviceId,
|
||||
groupId: bridgeState.config.groupId,
|
||||
sourceId: bridgeState.config.sourceId,
|
||||
sourceMode: bridgeState.config.sourceMode,
|
||||
reconnectMs: bridgeState.config.reconnectMs,
|
||||
hasToken: Boolean(bridgeState.config.token),
|
||||
sentCount: bridgeState.sentCount,
|
||||
droppedCount: bridgeState.droppedCount,
|
||||
lastSentAt: bridgeState.lastSentAt,
|
||||
lastSentTopic: bridgeState.lastSentTopic,
|
||||
lastError: bridgeState.lastError,
|
||||
}
|
||||
}
|
||||
|
||||
if (bridgeState.config.enabled) {
|
||||
connect()
|
||||
}
|
||||
|
||||
return {
|
||||
publish,
|
||||
updateConfig,
|
||||
getConfig,
|
||||
getStatus,
|
||||
}
|
||||
}
|
||||
|
||||
const gatewayBridge = createGatewayBridge()
|
||||
|
||||
const server = http.createServer((request, response) => {
|
||||
if (request.method === 'OPTIONS') {
|
||||
response.writeHead(204, {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type',
|
||||
})
|
||||
response.end()
|
||||
return
|
||||
}
|
||||
|
||||
if ((request.url || '').startsWith(PROXY_PATH)) {
|
||||
handleProxyRequest(request, response)
|
||||
return
|
||||
}
|
||||
|
||||
if ((request.url || '').startsWith(BRIDGE_CONFIG_PATH)) {
|
||||
if (request.method === 'GET') {
|
||||
respondJson(response, 200, {
|
||||
config: gatewayBridge.getConfig(),
|
||||
status: gatewayBridge.getStatus(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (request.method === 'POST') {
|
||||
readJsonBody(request)
|
||||
.then((payload) => {
|
||||
const status = gatewayBridge.updateConfig(payload)
|
||||
respondJson(response, 200, {
|
||||
config: gatewayBridge.getConfig(),
|
||||
status,
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
respondJson(response, 400, {
|
||||
error: error && error.message ? error.message : 'Invalid JSON body',
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
respondJson(response, 405, {
|
||||
error: 'Method Not Allowed',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if ((request.url || '').startsWith(BRIDGE_STATUS_PATH)) {
|
||||
respondJson(response, 200, gatewayBridge.getStatus())
|
||||
return
|
||||
}
|
||||
|
||||
serveStatic(request.url || '/', response)
|
||||
})
|
||||
|
||||
@@ -137,6 +529,8 @@ wss.on('connection', (socket) => {
|
||||
bpm: Math.max(1, Math.round(Number(parsed.bpm))),
|
||||
})
|
||||
|
||||
gatewayBridge.publish(JSON.parse(serialized))
|
||||
|
||||
wss.clients.forEach((client) => {
|
||||
if (client.readyState === client.OPEN) {
|
||||
client.send(serialized)
|
||||
@@ -161,4 +555,12 @@ server.listen(PORT, HOST, () => {
|
||||
console.log(` UI: http://127.0.0.1:${PORT}/`)
|
||||
console.log(` WS: ws://127.0.0.1:${PORT}${WS_PATH}`)
|
||||
console.log(` Proxy: http://127.0.0.1:${PORT}${PROXY_PATH}?url=<remote-url>`)
|
||||
console.log(` Bridge status: http://127.0.0.1:${PORT}${BRIDGE_STATUS_PATH}`)
|
||||
console.log(` Bridge config: http://127.0.0.1:${PORT}${BRIDGE_CONFIG_PATH}`)
|
||||
if (INITIAL_BRIDGE_CONFIG.enabled) {
|
||||
console.log(` Gateway bridge: enabled -> ${INITIAL_BRIDGE_CONFIG.url}`)
|
||||
console.log(` Gateway target device: ${INITIAL_BRIDGE_CONFIG.deviceId}`)
|
||||
} else {
|
||||
console.log(` Gateway bridge: disabled`)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user