mirror of
https://github.com/ALLFATHER-BV/wadamesh.git
synced 2026-11-12 02:27:35 +00:00
Location: the web board has CAP_GPS 1. web/platform/WebLocation.* is its SensorManager + LocationProvider, fed by navigator.geolocation (page shell, only while the UI's GPS is on, the prompt deferred to a tap) or WADA_GPS on the host, so the GPS switch, status, map centring, distances and wada.sys.gps (with speed/course) work unchanged. The position reaches a connected node (CMD_SET_ADVERT_LATLON, displaced by the Location privacy radius) only while the node's advert-location setting is "share"; the demo moves its cast to the visitor's location. Map tiles: wadaHttpRequest serves the firmware's default tile server only: a synchronous emscripten_fetch from the tile_fetch pthread to the same-origin tiles/ path (serve.py proxies it; nginx location for the site), plain HTTP natively. WiFi reports connected while the browser is online. Fixes the static-init order of the host VFS (the UI's own tiles LittleFS copied an empty impl). Polish: Lua apps that need the board's own radio carry "radio": "own" in the catalog (baked under CAP_RADIO_TOOLS, greyed in the Store); mouse wheel and trackpad scrolling; Settings > Wi-Fi / Bluetooth explain the page bar (CAP_WIFI_SETUP / CAP_BT_SETUP); no Download mode (CAP_USB_FLASH); no receiver settings on Settings > GPS (CAP_GPS_RECEIVER). Tests: webtest checks the map centres on Playwright's Ghent location and shows tiles; nodetest checks the location privacy rule against the mock node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
65 lines
3.2 KiB
Python
65 lines
3.2 KiB
Python
# A fake MeshCore node behind the WebSocket companion server (the device's port 8765):
|
|
# the host sends '<' + len16 + payload inside one binary message, the node answers with
|
|
# the raw payload, one message per frame. Same node as mock_companion_node.py.
|
|
# python3 web/mesh/test/mock_ws_node.py 18765
|
|
# then connect the page (Connect > Wi-Fi) to ws://localhost:18765/
|
|
import asyncio, base64, hashlib, json, os, struct, sys, threading
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 18765
|
|
sys.argv = sys.argv[:1]
|
|
import mock_companion_node as node
|
|
|
|
async def handle(r, w):
|
|
hdr = b''
|
|
while b'\r\n\r\n' not in hdr:
|
|
d = await r.read(1024)
|
|
if not d: return
|
|
hdr += d
|
|
key = [l.split(b':', 1)[1].strip() for l in hdr.split(b'\r\n') if l.lower().startswith(b'sec-websocket-key')]
|
|
if not key: # a plain GET, as the device serves an info page; /latlon and /share/N are for the tests
|
|
path = hdr.split(b' ', 2)[1] if hdr.count(b' ') >= 2 else b'/'
|
|
if path.startswith(b'/share/'): node.STATE['share'] = int(path[7:8] or b'0')
|
|
body = json.dumps(node.STATE).encode() if path != b'/' else b'mock node\n'
|
|
w.write(b'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: '
|
|
+ str(len(body)).encode() + b'\r\n\r\n' + body); await w.drain(); w.close(); return
|
|
acc = base64.b64encode(hashlib.sha1(key[0] + b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest())
|
|
w.write(b'HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ' + acc + b'\r\n\r\n')
|
|
loop = asyncio.get_running_loop()
|
|
lock = threading.Lock()
|
|
def send_now(payload):
|
|
n = len(payload)
|
|
hdr = bytes([0x82, n]) if n < 126 else bytes([0x82, 126]) + struct.pack('>H', n)
|
|
w.write(hdr + payload)
|
|
def send(payload): # callable from any thread (the handler's timers)
|
|
loop.call_soon_threadsafe(send_now, payload)
|
|
def later(delay, payload):
|
|
loop.call_later(delay, send_now, payload)
|
|
print('mock ws: client', flush=True)
|
|
node.on_connect(send, later)
|
|
try:
|
|
while True:
|
|
h = await r.readexactly(2)
|
|
n = h[1] & 0x7f
|
|
if n == 126: n = struct.unpack('>H', await r.readexactly(2))[0]
|
|
elif n == 127: n = struct.unpack('>Q', await r.readexactly(8))[0]
|
|
mask = await r.readexactly(4) if h[1] & 0x80 else b'\0\0\0\0'
|
|
data = bytes(b ^ mask[i % 4] for i, b in enumerate(await r.readexactly(n)))
|
|
op = h[0] & 0x0f
|
|
if op == 8: break
|
|
if op == 9: w.write(bytes([0x8A, 0])); continue
|
|
if op != 2 or len(data) < 3 or data[0] != 0x3c: continue
|
|
ln = data[1] | data[2] << 8
|
|
node.handle(data[3:3 + ln], send_now, later)
|
|
await w.drain()
|
|
except (asyncio.IncompleteReadError, ConnectionResetError):
|
|
pass
|
|
print('mock ws: client gone', flush=True)
|
|
w.close()
|
|
|
|
async def main():
|
|
s = await asyncio.start_server(handle, '127.0.0.1', PORT)
|
|
print('mock ws node on ws://localhost:%d/' % PORT, flush=True)
|
|
async with s: await s.serve_forever()
|
|
|
|
asyncio.run(main())
|