Add mock GPS simulator and configurable location sources
This commit is contained in:
152
tools/mock-gps-sim/server.js
Normal file
152
tools/mock-gps-sim/server.js
Normal file
@@ -0,0 +1,152 @@
|
||||
const http = require('http')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { WebSocketServer } = require('ws')
|
||||
|
||||
const HOST = '0.0.0.0'
|
||||
const PORT = 17865
|
||||
const WS_PATH = '/mock-gps'
|
||||
const PROXY_PATH = '/proxy'
|
||||
const PUBLIC_DIR = path.join(__dirname, 'public')
|
||||
|
||||
function getContentType(filePath) {
|
||||
const ext = path.extname(filePath).toLowerCase()
|
||||
if (ext === '.html') {
|
||||
return 'text/html; charset=utf-8'
|
||||
}
|
||||
if (ext === '.css') {
|
||||
return 'text/css; charset=utf-8'
|
||||
}
|
||||
if (ext === '.js') {
|
||||
return 'application/javascript; charset=utf-8'
|
||||
}
|
||||
if (ext === '.json') {
|
||||
return 'application/json; charset=utf-8'
|
||||
}
|
||||
if (ext === '.svg') {
|
||||
return 'image/svg+xml'
|
||||
}
|
||||
return 'text/plain; charset=utf-8'
|
||||
}
|
||||
|
||||
function serveStatic(requestPath, response) {
|
||||
const safePath = requestPath === '/' ? '/index.html' : requestPath
|
||||
const resolvedPath = path.normalize(path.join(PUBLIC_DIR, safePath))
|
||||
if (!resolvedPath.startsWith(PUBLIC_DIR)) {
|
||||
response.writeHead(403)
|
||||
response.end('Forbidden')
|
||||
return
|
||||
}
|
||||
|
||||
fs.readFile(resolvedPath, (error, content) => {
|
||||
if (error) {
|
||||
response.writeHead(404)
|
||||
response.end('Not Found')
|
||||
return
|
||||
}
|
||||
|
||||
response.writeHead(200, {
|
||||
'Content-Type': getContentType(resolvedPath),
|
||||
'Cache-Control': 'no-store',
|
||||
})
|
||||
response.end(content)
|
||||
})
|
||||
}
|
||||
|
||||
function isMockGpsPayload(payload) {
|
||||
return payload
|
||||
&& payload.type === 'mock_gps'
|
||||
&& Number.isFinite(payload.lat)
|
||||
&& Number.isFinite(payload.lon)
|
||||
}
|
||||
|
||||
async function handleProxyRequest(request, response) {
|
||||
const requestUrl = new URL(request.url || '/', `http://127.0.0.1:${PORT}`)
|
||||
const targetUrl = requestUrl.searchParams.get('url')
|
||||
if (!targetUrl) {
|
||||
response.writeHead(400, {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
})
|
||||
response.end('Missing url')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const upstream = await fetch(targetUrl)
|
||||
const body = Buffer.from(await upstream.arrayBuffer())
|
||||
response.writeHead(upstream.status, {
|
||||
'Content-Type': upstream.headers.get('content-type') || 'application/octet-stream',
|
||||
'Cache-Control': 'no-store',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
})
|
||||
response.end(body)
|
||||
} catch (error) {
|
||||
response.writeHead(502, {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
})
|
||||
response.end(error && error.message ? error.message : 'Proxy request failed')
|
||||
}
|
||||
}
|
||||
|
||||
const server = http.createServer((request, response) => {
|
||||
if ((request.url || '').startsWith(PROXY_PATH)) {
|
||||
handleProxyRequest(request, response)
|
||||
return
|
||||
}
|
||||
|
||||
serveStatic(request.url || '/', response)
|
||||
})
|
||||
|
||||
const wss = new WebSocketServer({ noServer: true })
|
||||
|
||||
wss.on('connection', (socket) => {
|
||||
socket.on('message', (rawMessage) => {
|
||||
const text = String(rawMessage)
|
||||
let parsed
|
||||
try {
|
||||
parsed = JSON.parse(text)
|
||||
} catch (_error) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!isMockGpsPayload(parsed)) {
|
||||
return
|
||||
}
|
||||
|
||||
const serialized = JSON.stringify({
|
||||
type: 'mock_gps',
|
||||
timestamp: Number.isFinite(parsed.timestamp) ? parsed.timestamp : Date.now(),
|
||||
lat: Number(parsed.lat),
|
||||
lon: Number(parsed.lon),
|
||||
accuracyMeters: Number.isFinite(parsed.accuracyMeters) ? Number(parsed.accuracyMeters) : 6,
|
||||
speedMps: Number.isFinite(parsed.speedMps) ? Number(parsed.speedMps) : 0,
|
||||
headingDeg: Number.isFinite(parsed.headingDeg) ? Number(parsed.headingDeg) : 0,
|
||||
})
|
||||
|
||||
wss.clients.forEach((client) => {
|
||||
if (client.readyState === client.OPEN) {
|
||||
client.send(serialized)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
if (!request.url || !request.url.startsWith(WS_PATH)) {
|
||||
socket.destroy()
|
||||
return
|
||||
}
|
||||
|
||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
wss.emit('connection', ws, request)
|
||||
})
|
||||
})
|
||||
|
||||
server.listen(PORT, HOST, () => {
|
||||
console.log(`Mock GPS simulator running:`)
|
||||
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>`)
|
||||
})
|
||||
Reference in New Issue
Block a user