Files
wadamesh/web/mesh/BrowserTransport.cpp
T

150 lines
5.4 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// Browser companion transports (Emscripten): Web Serial, Web Bluetooth, WebSocket.
//
// The browser APIs live in web/mesh/js/companion-transports.js (linked with --pre-js);
// it only moves bytes. The framing is here, in C++, as for the native transports:
//
// kind we hand JS JS hands us
// web-serial '<' len16 payload (bytes) a byte stream ('>' len16 payload)
// ws '<' len16 payload (ONE message) one frame per binary message
// ble payload (one write to NUS RX) one frame per NUS TX notification
//
// JS -> C++ entry points are plain C functions (EMSCRIPTEN_KEEPALIVE, no
// EXPORTED_FUNCTIONS needed): wada_companion_js_alloc / _rx / _state. Received bytes
// are queued under a mutex and delivered from poll(), on the UI thread, so the glue is
// correct whether the page runs single-threaded or with pthreads.
#include "CompanionTransport.h"
#include <stdio.h>
#if !defined(__EMSCRIPTEN__)
CompanionTransport* wadaCreateBrowserTransport(const char*, const char*) { return nullptr; }
#else
#include <emscripten.h>
#include <mutex>
#include <stdlib.h>
#include <string>
// C++ -> JS (the functions WadaCompanion defines in companion-transports.js).
EM_JS(void, wada_js_companion_open, (int kind, const uint8_t* arg, int arg_len), {
var a = new TextDecoder().decode(HEAPU8.slice(arg, arg + arg_len));
WadaCompanion.open(["web-serial", "ble", "ws"][kind], a);
});
EM_JS(int, wada_js_companion_send, (const uint8_t* p, int n), {
return WadaCompanion.send(HEAPU8.slice(p, p + n)) ? 1 : 0;
});
EM_JS(void, wada_js_companion_close, (), { WadaCompanion.close(); });
namespace {
enum Kind { K_SERIAL = 0, K_BLE = 1, K_WS = 2 };
class BrowserTransport;
BrowserTransport* s_active = nullptr;
class BrowserTransport : public CompanionTransport {
public:
BrowserTransport(Kind k, const char* arg) : _kind(k), _arg(arg ? arg : "") {
_desc = _kind == K_WS ? _arg : (_kind == K_BLE ? "Bluetooth" : "USB");
}
const char* name() const override { return _kind == K_SERIAL ? "web-serial" : _kind == K_BLE ? "ble" : "ws"; }
const char* describe() const override { return _desc.c_str(); }
bool open() override {
s_active = this;
// WS connects at once; Web Serial / Bluetooth reuse a device this page was granted
// before, otherwise they wait for WadaCompanion.connect() from a user gesture.
wada_js_companion_open((int)_kind, (const uint8_t*)_arg.data(), (int)_arg.size());
return true;
}
void close() override { wada_js_companion_close(); }
bool isConnected() const override { return _up; }
bool sendFrame(const uint8_t* frame, size_t len) override {
if (!_up) return false;
if (_kind == K_BLE) return wada_js_companion_send(frame, (int)len) != 0;
const std::vector<uint8_t> out = companionEncodeHostFrame(frame, len);
return wada_js_companion_send(out.data(), (int)out.size()) != 0;
}
void poll(uint32_t now_ms) override {
(void)now_ms;
std::vector<std::vector<uint8_t>> rx;
int state = -1;
std::string why;
{
std::lock_guard<std::mutex> lk(_mu);
rx.swap(_rx);
if (_state_changed) { state = _pending_state; why = _why; _state_changed = false; }
}
if (state == 1 && !_up) {
_up = true;
_deframer.reset();
fprintf(stderr, "[companion] %s %s: open\n", name(), describe());
if (_listener) _listener->onTransportConnected();
}
for (auto& chunk : rx) {
if (_kind == K_SERIAL) {
_deframer.feed(chunk.data(), chunk.size(), [this](const uint8_t* f, size_t l) {
if (_listener) _listener->onTransportFrame(f, l);
});
} else if (!chunk.empty() && _listener) {
_listener->onTransportFrame(chunk.data(), chunk.size());
}
}
if (state == 0 && _up) {
_up = false;
fprintf(stderr, "[companion] %s %s: %s\n", name(), describe(), why.c_str());
if (_listener) _listener->onTransportDisconnected(why.c_str());
}
}
// from JS (any thread)
void jsRx(const uint8_t* p, size_t n) {
std::lock_guard<std::mutex> lk(_mu);
_rx.emplace_back(p, p + n);
}
void jsState(int up, const char* why) {
std::lock_guard<std::mutex> lk(_mu);
_pending_state = up;
_why = why ? why : "";
_state_changed = true;
}
private:
Kind _kind;
std::string _arg, _desc;
bool _up = false;
StreamDeframer _deframer;
std::mutex _mu;
std::vector<std::vector<uint8_t>> _rx;
bool _state_changed = false;
int _pending_state = 0;
std::string _why;
};
} // namespace
extern "C" {
// JS allocates a buffer here, fills it, and hands it to _rx / _state, which free it.
EMSCRIPTEN_KEEPALIVE uint8_t* wada_companion_js_alloc(int n) { return (uint8_t*)malloc(n > 0 ? (size_t)n : 1); }
EMSCRIPTEN_KEEPALIVE void wada_companion_js_rx(uint8_t* p, int n) {
if (s_active && p && n > 0) s_active->jsRx(p, (size_t)n);
free(p);
}
// up = 1 connected, 0 closed; why = UTF-8 detail (may be null), freed here.
EMSCRIPTEN_KEEPALIVE void wada_companion_js_state(int up, char* why) {
if (s_active) s_active->jsState(up, why);
free(why);
}
}
CompanionTransport* wadaCreateBrowserTransport(const char* kind, const char* arg) {
const std::string k = kind ? kind : "";
if (k == "web-serial" || k == "serial") return new BrowserTransport(K_SERIAL, arg);
if (k == "ble" || k == "bluetooth") return new BrowserTransport(K_BLE, arg);
if (k == "ws" || k == "websocket") return new BrowserTransport(K_WS, arg);
return nullptr;
}
#endif