server.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. const http = require('http')
  2. const fs = require('fs')
  3. const path = require('path')
  4. const WebSocket = require('ws')
  5. const { WebSocketServer } = WebSocket
  6. const HOST = '0.0.0.0'
  7. const PORT = 17865
  8. const GPS_WS_PATH = '/mock-gps'
  9. const HEART_RATE_WS_PATH = '/mock-hr'
  10. const DEBUG_LOG_WS_PATH = '/debug-log'
  11. const PROXY_PATH = '/proxy'
  12. const BRIDGE_STATUS_PATH = '/bridge-status'
  13. const BRIDGE_CONFIG_PATH = '/bridge-config'
  14. const PUBLIC_DIR = path.join(__dirname, 'public')
  15. const DEFAULT_GATEWAY_BRIDGE_URL = 'ws://127.0.0.1:18080/ws'
  16. const INITIAL_BRIDGE_CONFIG = {
  17. enabled: process.env.MOCK_SIM_GATEWAY_ENABLED === '1',
  18. url: process.env.MOCK_SIM_GATEWAY_URL || DEFAULT_GATEWAY_BRIDGE_URL,
  19. token: process.env.MOCK_SIM_GATEWAY_TOKEN || 'dev-producer-token',
  20. channelId: process.env.MOCK_SIM_GATEWAY_CHANNEL_ID || '',
  21. deviceId: process.env.MOCK_SIM_GATEWAY_DEVICE_ID || 'child-001',
  22. groupId: process.env.MOCK_SIM_GATEWAY_GROUP_ID || '',
  23. sourceId: process.env.MOCK_SIM_GATEWAY_SOURCE_ID || 'mock-gps-sim',
  24. sourceMode: process.env.MOCK_SIM_GATEWAY_SOURCE_MODE || 'mock',
  25. reconnectMs: Math.max(1000, Number(process.env.MOCK_SIM_GATEWAY_RECONNECT_MS) || 3000),
  26. }
  27. function getContentType(filePath) {
  28. const ext = path.extname(filePath).toLowerCase()
  29. if (ext === '.html') {
  30. return 'text/html; charset=utf-8'
  31. }
  32. if (ext === '.css') {
  33. return 'text/css; charset=utf-8'
  34. }
  35. if (ext === '.js') {
  36. return 'application/javascript; charset=utf-8'
  37. }
  38. if (ext === '.json') {
  39. return 'application/json; charset=utf-8'
  40. }
  41. if (ext === '.svg') {
  42. return 'image/svg+xml'
  43. }
  44. return 'text/plain; charset=utf-8'
  45. }
  46. function respondJson(response, statusCode, payload) {
  47. response.writeHead(statusCode, {
  48. 'Content-Type': 'application/json; charset=utf-8',
  49. 'Cache-Control': 'no-store',
  50. 'Access-Control-Allow-Origin': '*',
  51. })
  52. response.end(JSON.stringify(payload))
  53. }
  54. function serveStatic(requestPath, response) {
  55. let safePath = requestPath === '/' ? '/index.html' : requestPath
  56. if (safePath.endsWith('/')) {
  57. safePath = `${safePath}index.html`
  58. } else if (!path.extname(safePath)) {
  59. safePath = `${safePath}/index.html`
  60. }
  61. const resolvedPath = path.normalize(path.join(PUBLIC_DIR, safePath))
  62. if (!resolvedPath.startsWith(PUBLIC_DIR)) {
  63. response.writeHead(403)
  64. response.end('Forbidden')
  65. return
  66. }
  67. fs.readFile(resolvedPath, (error, content) => {
  68. if (error) {
  69. response.writeHead(404)
  70. response.end('Not Found')
  71. return
  72. }
  73. response.writeHead(200, {
  74. 'Content-Type': getContentType(resolvedPath),
  75. 'Cache-Control': 'no-store',
  76. })
  77. response.end(content)
  78. })
  79. }
  80. function isMockGpsPayload(payload) {
  81. return payload
  82. && payload.type === 'mock_gps'
  83. && Number.isFinite(payload.lat)
  84. && Number.isFinite(payload.lon)
  85. }
  86. function isMockHeartRatePayload(payload) {
  87. return payload
  88. && payload.type === 'mock_heart_rate'
  89. && Number.isFinite(payload.bpm)
  90. }
  91. function isDebugLogPayload(payload) {
  92. return payload
  93. && payload.type === 'debug-log'
  94. && typeof payload.scope === 'string'
  95. && typeof payload.level === 'string'
  96. && typeof payload.message === 'string'
  97. }
  98. function normalizeChannelId(value) {
  99. const trimmed = String(value || '').trim()
  100. return trimmed || 'default'
  101. }
  102. async function handleProxyRequest(request, response) {
  103. const requestUrl = new URL(request.url || '/', `http://127.0.0.1:${PORT}`)
  104. const targetUrl = requestUrl.searchParams.get('url')
  105. if (!targetUrl) {
  106. response.writeHead(400, {
  107. 'Content-Type': 'text/plain; charset=utf-8',
  108. 'Access-Control-Allow-Origin': '*',
  109. })
  110. response.end('Missing url')
  111. return
  112. }
  113. try {
  114. const upstream = await fetch(targetUrl)
  115. const body = Buffer.from(await upstream.arrayBuffer())
  116. response.writeHead(upstream.status, {
  117. 'Content-Type': upstream.headers.get('content-type') || 'application/octet-stream',
  118. 'Cache-Control': 'no-store',
  119. 'Access-Control-Allow-Origin': '*',
  120. })
  121. response.end(body)
  122. } catch (error) {
  123. response.writeHead(502, {
  124. 'Content-Type': 'text/plain; charset=utf-8',
  125. 'Access-Control-Allow-Origin': '*',
  126. })
  127. response.end(error && error.message ? error.message : 'Proxy request failed')
  128. }
  129. }
  130. async function readJsonBody(request) {
  131. return new Promise((resolve, reject) => {
  132. const chunks = []
  133. request.on('data', (chunk) => {
  134. chunks.push(chunk)
  135. })
  136. request.on('end', () => {
  137. const raw = Buffer.concat(chunks).toString('utf8').trim()
  138. if (!raw) {
  139. resolve({})
  140. return
  141. }
  142. try {
  143. resolve(JSON.parse(raw))
  144. } catch (error) {
  145. reject(error)
  146. }
  147. })
  148. request.on('error', reject)
  149. })
  150. }
  151. function normalizeBridgeConfig(input, currentConfig) {
  152. const source = input || {}
  153. const fallback = currentConfig || INITIAL_BRIDGE_CONFIG
  154. return {
  155. enabled: typeof source.enabled === 'boolean' ? source.enabled : fallback.enabled,
  156. url: typeof source.url === 'string' && source.url.trim() ? source.url.trim() : fallback.url,
  157. token: typeof source.token === 'string' ? source.token.trim() : fallback.token,
  158. channelId: typeof source.channelId === 'string' ? source.channelId.trim() : fallback.channelId,
  159. deviceId: typeof source.deviceId === 'string' && source.deviceId.trim() ? source.deviceId.trim() : fallback.deviceId,
  160. groupId: typeof source.groupId === 'string' ? source.groupId.trim() : fallback.groupId,
  161. sourceId: typeof source.sourceId === 'string' && source.sourceId.trim() ? source.sourceId.trim() : fallback.sourceId,
  162. sourceMode: typeof source.sourceMode === 'string' && source.sourceMode.trim() ? source.sourceMode.trim() : fallback.sourceMode,
  163. reconnectMs: Math.max(1000, Number(source.reconnectMs) || fallback.reconnectMs),
  164. }
  165. }
  166. function createGatewayBridge() {
  167. const bridgeState = {
  168. config: { ...INITIAL_BRIDGE_CONFIG },
  169. socket: null,
  170. connecting: false,
  171. connected: false,
  172. authenticated: false,
  173. reconnectTimer: 0,
  174. lastError: '',
  175. lastSentAt: 0,
  176. lastSentTopic: '',
  177. sentCount: 0,
  178. droppedCount: 0,
  179. }
  180. function logBridge(message) {
  181. console.log(`[gateway-bridge] ${message}`)
  182. }
  183. function clearReconnectTimer() {
  184. if (!bridgeState.reconnectTimer) {
  185. return
  186. }
  187. clearTimeout(bridgeState.reconnectTimer)
  188. bridgeState.reconnectTimer = 0
  189. }
  190. function scheduleReconnect() {
  191. if (!bridgeState.config.enabled || bridgeState.reconnectTimer) {
  192. return
  193. }
  194. bridgeState.reconnectTimer = setTimeout(() => {
  195. bridgeState.reconnectTimer = 0
  196. connect()
  197. }, bridgeState.config.reconnectMs)
  198. }
  199. function resetSocketState() {
  200. bridgeState.socket = null
  201. bridgeState.connecting = false
  202. bridgeState.connected = false
  203. bridgeState.authenticated = false
  204. }
  205. function handleGatewayMessage(rawMessage) {
  206. let parsed
  207. try {
  208. parsed = JSON.parse(String(rawMessage))
  209. } catch (_error) {
  210. return
  211. }
  212. if (parsed.type === 'welcome') {
  213. if (!bridgeState.socket || bridgeState.socket.readyState !== WebSocket.OPEN) {
  214. return
  215. }
  216. if (bridgeState.config.channelId) {
  217. bridgeState.socket.send(JSON.stringify({
  218. type: 'join_channel',
  219. role: 'producer',
  220. channelId: bridgeState.config.channelId,
  221. token: bridgeState.config.token,
  222. }))
  223. } else {
  224. bridgeState.socket.send(JSON.stringify({
  225. type: 'authenticate',
  226. role: 'producer',
  227. token: bridgeState.config.token,
  228. }))
  229. }
  230. return
  231. }
  232. if (parsed.type === 'authenticated' || parsed.type === 'joined_channel') {
  233. bridgeState.authenticated = true
  234. bridgeState.lastError = ''
  235. if (bridgeState.config.channelId) {
  236. logBridge(`joined channel=${bridgeState.config.channelId}, device=${bridgeState.config.deviceId}, source=${bridgeState.config.sourceId}`)
  237. } else {
  238. logBridge(`authenticated, device=${bridgeState.config.deviceId}, source=${bridgeState.config.sourceId}`)
  239. }
  240. return
  241. }
  242. if (parsed.type === 'error') {
  243. bridgeState.lastError = parsed.error || 'gateway error'
  244. logBridge(`error: ${bridgeState.lastError}`)
  245. }
  246. }
  247. function closeSocket() {
  248. if (!bridgeState.socket) {
  249. return
  250. }
  251. try {
  252. bridgeState.socket.close()
  253. } catch (_error) {
  254. // noop
  255. }
  256. resetSocketState()
  257. }
  258. function connect() {
  259. if (!bridgeState.config.enabled || bridgeState.connecting) {
  260. return
  261. }
  262. if (bridgeState.socket && (bridgeState.socket.readyState === WebSocket.OPEN || bridgeState.socket.readyState === WebSocket.CONNECTING)) {
  263. return
  264. }
  265. clearReconnectTimer()
  266. bridgeState.connecting = true
  267. bridgeState.lastError = ''
  268. logBridge(`connecting to ${bridgeState.config.url}`)
  269. const socket = new WebSocket(bridgeState.config.url)
  270. bridgeState.socket = socket
  271. socket.on('open', () => {
  272. bridgeState.connecting = false
  273. bridgeState.connected = true
  274. logBridge('connected')
  275. })
  276. socket.on('message', handleGatewayMessage)
  277. socket.on('close', () => {
  278. const wasConnected = bridgeState.connected || bridgeState.authenticated
  279. resetSocketState()
  280. if (wasConnected) {
  281. logBridge('disconnected')
  282. }
  283. scheduleReconnect()
  284. })
  285. socket.on('error', (error) => {
  286. bridgeState.lastError = error && error.message ? error.message : 'gateway socket error'
  287. logBridge(`socket error: ${bridgeState.lastError}`)
  288. })
  289. }
  290. function toGatewayEnvelope(payload) {
  291. if (isMockGpsPayload(payload)) {
  292. return {
  293. schemaVersion: 1,
  294. messageId: `gps-${payload.timestamp}`,
  295. timestamp: payload.timestamp,
  296. topic: 'telemetry.location',
  297. source: {
  298. kind: 'producer',
  299. id: bridgeState.config.sourceId,
  300. mode: bridgeState.config.sourceMode,
  301. },
  302. target: {
  303. channelId: bridgeState.config.channelId,
  304. deviceId: bridgeState.config.deviceId,
  305. groupId: bridgeState.config.groupId,
  306. },
  307. payload: {
  308. lat: Number(payload.lat),
  309. lng: Number(payload.lon),
  310. speed: Number(payload.speedMps) || 0,
  311. bearing: Number(payload.headingDeg) || 0,
  312. accuracy: Number(payload.accuracyMeters) || 6,
  313. coordSystem: 'GCJ02',
  314. },
  315. }
  316. }
  317. if (isMockHeartRatePayload(payload)) {
  318. return {
  319. schemaVersion: 1,
  320. messageId: `hr-${payload.timestamp}`,
  321. timestamp: payload.timestamp,
  322. topic: 'telemetry.heart_rate',
  323. source: {
  324. kind: 'producer',
  325. id: bridgeState.config.sourceId,
  326. mode: bridgeState.config.sourceMode,
  327. },
  328. target: {
  329. channelId: bridgeState.config.channelId,
  330. deviceId: bridgeState.config.deviceId,
  331. groupId: bridgeState.config.groupId,
  332. },
  333. payload: {
  334. bpm: Math.max(1, Math.round(Number(payload.bpm))),
  335. },
  336. }
  337. }
  338. return null
  339. }
  340. function publish(payload) {
  341. if (!bridgeState.config.enabled) {
  342. return
  343. }
  344. if (!bridgeState.socket || bridgeState.socket.readyState !== WebSocket.OPEN || !bridgeState.authenticated) {
  345. bridgeState.droppedCount += 1
  346. connect()
  347. return
  348. }
  349. const envelope = toGatewayEnvelope(payload)
  350. if (!envelope) {
  351. return
  352. }
  353. bridgeState.socket.send(JSON.stringify({
  354. type: 'publish',
  355. envelope,
  356. }))
  357. bridgeState.lastSentAt = Date.now()
  358. bridgeState.lastSentTopic = envelope.topic
  359. bridgeState.sentCount += 1
  360. }
  361. function updateConfig(nextConfigInput) {
  362. const nextConfig = normalizeBridgeConfig(nextConfigInput, bridgeState.config)
  363. const changed = JSON.stringify(nextConfig) !== JSON.stringify(bridgeState.config)
  364. bridgeState.config = nextConfig
  365. if (!changed) {
  366. return getStatus()
  367. }
  368. bridgeState.lastError = ''
  369. if (!bridgeState.config.enabled) {
  370. clearReconnectTimer()
  371. closeSocket()
  372. logBridge('disabled')
  373. return getStatus()
  374. }
  375. clearReconnectTimer()
  376. closeSocket()
  377. connect()
  378. return getStatus()
  379. }
  380. function getConfig() {
  381. return { ...bridgeState.config }
  382. }
  383. function getStatus() {
  384. return {
  385. enabled: bridgeState.config.enabled,
  386. url: bridgeState.config.url,
  387. connected: bridgeState.connected,
  388. authenticated: bridgeState.authenticated,
  389. channelId: bridgeState.config.channelId,
  390. deviceId: bridgeState.config.deviceId,
  391. groupId: bridgeState.config.groupId,
  392. sourceId: bridgeState.config.sourceId,
  393. sourceMode: bridgeState.config.sourceMode,
  394. reconnectMs: bridgeState.config.reconnectMs,
  395. hasToken: Boolean(bridgeState.config.token),
  396. sentCount: bridgeState.sentCount,
  397. droppedCount: bridgeState.droppedCount,
  398. lastSentAt: bridgeState.lastSentAt,
  399. lastSentTopic: bridgeState.lastSentTopic,
  400. lastError: bridgeState.lastError,
  401. }
  402. }
  403. if (bridgeState.config.enabled) {
  404. connect()
  405. }
  406. return {
  407. publish,
  408. updateConfig,
  409. getConfig,
  410. getStatus,
  411. }
  412. }
  413. const gatewayBridge = createGatewayBridge()
  414. const server = http.createServer((request, response) => {
  415. if (request.method === 'OPTIONS') {
  416. response.writeHead(204, {
  417. 'Access-Control-Allow-Origin': '*',
  418. 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
  419. 'Access-Control-Allow-Headers': 'Content-Type',
  420. })
  421. response.end()
  422. return
  423. }
  424. if ((request.url || '').startsWith(PROXY_PATH)) {
  425. handleProxyRequest(request, response)
  426. return
  427. }
  428. if ((request.url || '').startsWith(BRIDGE_CONFIG_PATH)) {
  429. if (request.method === 'GET') {
  430. respondJson(response, 200, {
  431. config: gatewayBridge.getConfig(),
  432. status: gatewayBridge.getStatus(),
  433. })
  434. return
  435. }
  436. if (request.method === 'POST') {
  437. readJsonBody(request)
  438. .then((payload) => {
  439. const status = gatewayBridge.updateConfig(payload)
  440. respondJson(response, 200, {
  441. config: gatewayBridge.getConfig(),
  442. status,
  443. })
  444. })
  445. .catch((error) => {
  446. respondJson(response, 400, {
  447. error: error && error.message ? error.message : 'Invalid JSON body',
  448. })
  449. })
  450. return
  451. }
  452. respondJson(response, 405, {
  453. error: 'Method Not Allowed',
  454. })
  455. return
  456. }
  457. if ((request.url || '').startsWith(BRIDGE_STATUS_PATH)) {
  458. respondJson(response, 200, gatewayBridge.getStatus())
  459. return
  460. }
  461. serveStatic(request.url || '/', response)
  462. })
  463. const gpsWss = new WebSocketServer({ noServer: true })
  464. const heartRateWss = new WebSocketServer({ noServer: true })
  465. const debugLogWss = new WebSocketServer({ noServer: true })
  466. gpsWss.on('connection', (socket) => {
  467. socket.on('message', (rawMessage) => {
  468. const text = String(rawMessage)
  469. let parsed
  470. try {
  471. parsed = JSON.parse(text)
  472. } catch (_error) {
  473. return
  474. }
  475. if (!isMockGpsPayload(parsed)) {
  476. return
  477. }
  478. const outgoing = JSON.stringify({
  479. type: 'mock_gps',
  480. timestamp: Number.isFinite(parsed.timestamp) ? parsed.timestamp : Date.now(),
  481. channelId: normalizeChannelId(parsed.channelId),
  482. lat: Number(parsed.lat),
  483. lon: Number(parsed.lon),
  484. accuracyMeters: Number.isFinite(parsed.accuracyMeters) ? Number(parsed.accuracyMeters) : 6,
  485. speedMps: Number.isFinite(parsed.speedMps) ? Number(parsed.speedMps) : 0,
  486. headingDeg: Number.isFinite(parsed.headingDeg) ? Number(parsed.headingDeg) : 0,
  487. })
  488. gatewayBridge.publish(JSON.parse(outgoing))
  489. gpsWss.clients.forEach((client) => {
  490. if (client.readyState === client.OPEN) {
  491. client.send(outgoing)
  492. }
  493. })
  494. })
  495. })
  496. heartRateWss.on('connection', (socket) => {
  497. socket.on('message', (rawMessage) => {
  498. const text = String(rawMessage)
  499. let parsed
  500. try {
  501. parsed = JSON.parse(text)
  502. } catch (_error) {
  503. return
  504. }
  505. if (!isMockHeartRatePayload(parsed)) {
  506. return
  507. }
  508. const outgoing = JSON.stringify({
  509. type: 'mock_heart_rate',
  510. timestamp: Number.isFinite(parsed.timestamp) ? parsed.timestamp : Date.now(),
  511. channelId: normalizeChannelId(parsed.channelId),
  512. bpm: Math.max(1, Math.round(Number(parsed.bpm))),
  513. })
  514. gatewayBridge.publish(JSON.parse(outgoing))
  515. heartRateWss.clients.forEach((client) => {
  516. if (client.readyState === client.OPEN) {
  517. client.send(outgoing)
  518. }
  519. })
  520. })
  521. })
  522. debugLogWss.on('connection', (socket) => {
  523. socket.on('message', (rawMessage) => {
  524. const text = String(rawMessage)
  525. let parsed
  526. try {
  527. parsed = JSON.parse(text)
  528. } catch (_error) {
  529. return
  530. }
  531. if (!isDebugLogPayload(parsed)) {
  532. return
  533. }
  534. const outgoing = JSON.stringify({
  535. type: 'debug-log',
  536. timestamp: Number.isFinite(parsed.timestamp) ? parsed.timestamp : Date.now(),
  537. channelId: normalizeChannelId(parsed.channelId),
  538. scope: String(parsed.scope || 'app').slice(0, 64),
  539. level: parsed.level === 'warn' || parsed.level === 'error' ? parsed.level : 'info',
  540. message: String(parsed.message || '').slice(0, 400),
  541. ...(parsed.payload && typeof parsed.payload === 'object'
  542. ? { payload: parsed.payload }
  543. : {}),
  544. })
  545. debugLogWss.clients.forEach((client) => {
  546. if (client.readyState === client.OPEN) {
  547. client.send(outgoing)
  548. }
  549. })
  550. })
  551. })
  552. server.on('upgrade', (request, socket, head) => {
  553. const requestUrl = request.url || ''
  554. if (requestUrl.startsWith(GPS_WS_PATH)) {
  555. gpsWss.handleUpgrade(request, socket, head, (ws) => {
  556. gpsWss.emit('connection', ws, request)
  557. })
  558. return
  559. }
  560. if (requestUrl.startsWith(HEART_RATE_WS_PATH)) {
  561. heartRateWss.handleUpgrade(request, socket, head, (ws) => {
  562. heartRateWss.emit('connection', ws, request)
  563. })
  564. return
  565. }
  566. if (requestUrl.startsWith(DEBUG_LOG_WS_PATH)) {
  567. debugLogWss.handleUpgrade(request, socket, head, (ws) => {
  568. debugLogWss.emit('connection', ws, request)
  569. })
  570. return
  571. }
  572. if (!requestUrl) {
  573. socket.destroy()
  574. }
  575. socket.destroy()
  576. })
  577. server.listen(PORT, HOST, () => {
  578. console.log(`Mock GPS simulator running:`)
  579. console.log(` UI: http://127.0.0.1:${PORT}/`)
  580. console.log(` GPS WS: ws://127.0.0.1:${PORT}${GPS_WS_PATH}`)
  581. console.log(` HR WS: ws://127.0.0.1:${PORT}${HEART_RATE_WS_PATH}`)
  582. console.log(` Logger WS: ws://127.0.0.1:${PORT}${DEBUG_LOG_WS_PATH}`)
  583. console.log(` Proxy: http://127.0.0.1:${PORT}${PROXY_PATH}?url=<remote-url>`)
  584. console.log(` Bridge status: http://127.0.0.1:${PORT}${BRIDGE_STATUS_PATH}`)
  585. console.log(` Bridge config: http://127.0.0.1:${PORT}${BRIDGE_CONFIG_PATH}`)
  586. if (INITIAL_BRIDGE_CONFIG.enabled) {
  587. console.log(` Gateway bridge: enabled -> ${INITIAL_BRIDGE_CONFIG.url}`)
  588. console.log(` Gateway target device: ${INITIAL_BRIDGE_CONFIG.deviceId}`)
  589. } else {
  590. console.log(` Gateway bridge: disabled`)
  591. }
  592. })