Skip to content

Add SignalK BLE Gateway API support and upgrade to React 19#137

Open
dirkwa wants to merge 1 commit into
naugehyde:mainfrom
dirkwa:ble-gateway-api-support
Open

Add SignalK BLE Gateway API support and upgrade to React 19#137
dirkwa wants to merge 1 commit into
naugehyde:mainfrom
dirkwa:ble-gateway-api-support

Conversation

@dirkwa

@dirkwa dirkwa commented Apr 13, 2026

Copy link
Copy Markdown

Summary

  • Support the SignalK server's BLE API (app.bleApi) as an alternative to local USB/D-Bus/BlueZ, enabling BLE data from remote ESP32 gateways without local Bluetooth hardware
  • New "BLE Source" config option (auto/local/api) — defaults to auto which prefers the API when available
  • Virtual device layer (BleApiDevice, BleApiGattServer, BleApiDiscovery) implements the node-ble interface so all 50+ sensor classes work unmodified
  • Upgrade UI from React 16 to React 19, matching the server admin UI's Module Federation singleton config
  • Replace deprecated dependencies: @material-ui@mui, react-bootstrap v1 → v2, react-html-parserhtml-react-parser
  • Remove unused devDependencies (react-redux, reactstrap, recharts, etc.)

Testing

  • Verified plugin loads and appears in SignalK plugin config UI
  • Webpack build succeeds with React 19 singleton sharing
  • Syntax validated on all new and modified files

@NearlCrews

Copy link
Copy Markdown
Contributor

Code review

Found 2 issues:

  1. BleApiGattServer.jsreadValue/writeValue/writeValueWithoutResponse/startNotifications/stopNotifications dereference this._service._server._device._gattConnection with no null-check. After BleApiDevice._disconnectGATT() sets _gattConnection = null, any in-flight or queued call on these characteristics will throw TypeError: Cannot read properties of null. _discoverServices in the same file guards correctly with conn ? await conn.discoverServices() : [] — the same guard is needed in the characteristic methods.

async readValue() {
const conn = this._service._server._device._gattConnection
return conn.read(this._service._uuid, this._uuid)
}
async writeValue(buffer, options) {
const conn = this._service._server._device._gattConnection
const withResponse = options?.type !== 'command'
return conn.write(this._service._uuid, this._uuid, buffer, withResponse)
}
async writeValueWithoutResponse(buffer) {
const conn = this._service._server._device._gattConnection
return conn.write(this._service._uuid, this._uuid, buffer, false)
}
async startNotifications() {
const conn = this._service._server._device._gattConnection
await conn.startNotifications(
this._service._uuid,
this._uuid,
(data) => {
this.emit('valuechanged', data)
}
)
this._notifying = true
}
async stopNotifications() {
if (!this._notifying) return
const conn = this._service._server._device._gattConnection
await conn.stopNotifications(this._service._uuid, this._uuid)
this._notifying = false
}

Compare:

async _discoverServices() {
if (!this._discoveredServices) {
const conn = this._device._gattConnection
this._discoveredServices = conn
? await conn.discoverServices()
: []
}
return this._discoveredServices
}
}

  1. webpack.config.js — changing shared back to { singleton: true, requiredVersion: '^19.0.0' } reverts commit 93728be, which deliberately set singleton: false, strictVersion: true to isolate the plugin's React from the SignalK host's copy. With singleton: true, whichever React the server-admin-ui loads first wins and requiredVersion is downgraded to a warning — so on any SignalK host still shipping React 18, the plugin will silently run against the host's React 18 rather than fail loudly, which can produce subtle hook/runtime breakage. If this revert is intentional (e.g. because the host is confirmed to be on React 19), worth calling out in the commit/PR description.

},
shared: [{
react: { singleton: true, requiredVersion: '^19.0.0' },
'react-dom': { singleton: true, requiredVersion: '^19.0.0' }
}],
}),

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@dirkwa

dirkwa commented Apr 24, 2026

Copy link
Copy Markdown
Author

Thanks. Let's wait if BLE API in it's current state merges.

@NearlCrews

Copy link
Copy Markdown
Contributor

Quick follow-up after digging into the upstream.

Server-side status. The BLE Provider API the plugin depends on lives in SignalK/signalk-server#2588. That PR has been "ready for human review" since Apr 13. Until it lands this one is effectively unmergeable, so the items below are organized around "what to revisit when you pick this back up," not "what blocks merge today."

API usage check. Spot-checked the plugin's call sites against packages/server-api/src/bleapi.ts on your ble-provider-api branch. The methods used (onAdvertisement, connectGATT, and the BLEGattConnection surface) all match. No interface mismatches.

Items to address on next pass:

  1. Missing cleanup on plugin stop. BleApiDiscovery.stop() only runs the unsubscribe returned from onAdvertisement. Per the server PR's description, the plugin must also call app.bleApi.unRegister(plugin.id) on stop to release GATT claims and close sessions, and app.bleApi.releaseGATTDevice(mac, pluginId) for individual claim release in BleApiDevice._disconnectGATT(). Without these, GATT slot accounting on the server side leaks across plugin restarts.

  2. BleApiGattServer.js null guards (still relevant from earlier review). readValue, writeValue, writeValueWithoutResponse, startNotifications, stopNotifications all dereference this._service._server._device._gattConnection with no null check. After BleApiDevice._disconnectGATT() sets it to null, in-flight calls throw TypeError: Cannot read properties of null. Sibling _discoverServices already has the right guard.

  3. _state event rename reverts a main-branch fix. Commit 42ec35e on main changed BTSensor.setState to emit("_state", ...) to avoid colliding with Victron device events. This branch sets it back to "state" and updates the listeners in index.js to match. When you rebase, keep _state.

  4. Version regression. package.json is set to 1.3.8-beta4. Main is at 1.3.8-beta6.

  5. webpack.config.js singleton revert (still relevant from earlier review). Commit 93728be set singleton: false, strictVersion: true to isolate the plugin's React from the host's. This branch sets it back to singleton: true, requiredVersion: '^19.0.0'. On a host shipping React 18 the plugin would silently run against the host's React rather than fail. If intentional because the host is confirmed on React 19, worth calling out in the PR description.

  6. Bundles two orthogonal changes. The React 16 → 19 / MUI / react-bootstrap modernization does not depend on the server-side BLE API landing. Splitting it into its own PR against main would let the modernization land now and leave this PR scoped to the BLE work alone, which is also easier to review once #2588 is in.

A few smaller things I noticed but would not gate on: BleApiDevice.updateFromAdvertisement only emits RSSI / ManufacturerData / ServiceData in PropertiesChanged (Name / UUIDs / Connectable changes are stored but not emitted, so anything that listens for those on the live stream will not see updates), and BleApiDiscovery._devices grows unboundedly with no eviction.

Happy to send a fresh PR splitting the React/MUI upgrade out of this one if it would help; attributed however you prefer.

@dirkwa

dirkwa commented May 5, 2026

Copy link
Copy Markdown
Author

Awesome to see that you use AI now. great. So you will be able to figure this out yourself once BLE lands.
Good luck.

@dirkwa dirkwa closed this May 5, 2026
@dirkwa dirkwa reopened this Jul 6, 2026
@dirkwa

dirkwa commented Jul 6, 2026

Copy link
Copy Markdown
Author

The dirkwa image has "BLE Manager" merged and I am using it in production.

When the SignalK server's BLE API (app.bleApi) is available, the plugin
can now use it instead of requiring a local USB Bluetooth adapter via
D-Bus/BlueZ. This enables BLE sensor data from remote ESP32 gateways
and removes the hard dependency on local Bluetooth hardware.

A new "BLE Source" config option (auto/local/api) controls the mode.
Virtual device objects (BleApiDevice, BleApiGattServer, BleApiDiscovery)
implement the same interface as node-ble, so all 50+ sensor classes
work without modification.

Also upgrades the UI from React 16 to React 19 with matching dependency
updates (@material-ui -> @mui, react-bootstrap v1 -> v2,
react-html-parser -> html-react-parser) and webpack Module Federation
singleton config to match the server admin UI.
@dirkwa dirkwa force-pushed the ble-gateway-api-support branch from f464c81 to 4b1bf3e Compare July 6, 2026 21:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants