A simple Web Application that uses Web BLE to connect, read and write settings of a OpenCollar Edge device.
This app is a static web app. There is no build step and no package.json.
Serve the repository root over a local HTTP server instead of opening index.html directly from disk:
cd /home/tim/apps/ble-settings-app
python3 -m http.server 8000Then open:
http://localhost:8000/for the main BLE settings apphttp://localhost:8000/composer.htmlfor the HEX composer
Notes:
- Use a Chromium-based browser such as Chrome or Edge, because the app uses Web Bluetooth / Web BLE.
localhostis required here because the app registers a service worker and fetches local JSON assets. Opening the files with afile://URL will not work correctly.
Adding a new settings.json version:
- upload settings.json file to settings folder
- add settings.json version to functions.js
- add settings.json version to service-worker.js
The app and HEX composer support both the legacy settings protocol and the family-based
protocol released in OpenCollar v8.0.0. The bundled settings/settings-v8.0.0.json is the
unmodified v8.0.0 release asset,
and settings/settings-v8.0.1.json is the unmodified
v8.0.1 release asset.
The v8.0.1 schema is identical in content to v8.0.0; it is bundled so the app selects the
latest patch release and shows its firmware notes. Bundled DFU releases for v8.0.1 live in
assets/dfu/releases/open-collar-v8.0.1/ and assets/dfu/releases/air-quality-v8.0.1/.
- Legacy settings use
id length data; v8 settings usefamily id length data. - Runtime value responses also include a family byte in v8 (currently
0xA0). - Commands retain
id length data. Single-setting (A8) and single-value (A3) requests includefamily idas their two-byte payload in v8. - Bluetooth and satellite payloads prepend the port; LoRaWAN uses the port separately. Values remain little-endian; the address is always sent as family first, then ID.
functions.js normalizes family-based settings and values to a unique 0xFFII address
in memory for DOM IDs and lookups, preserving the original byte ID as wireId. Use
getProtocolAddress, encodeProtocolRecord, and encodeReadRequest for wire data;
do not serialize the normalized ID as a single byte. JSON import/export remains keyed
by setting name, so existing profiles can be imported across the protocol change.
Automatic schema selection matches numeric major/minor versions. A manual selection with the wrong protocol is rejected. Future firmware schemas must still be bundled when settings change; the family-based format alone does not guarantee compatibility. The firmware documentation calls the development transition v7.4; v8.0.0 is the published release that contains it.
Protocol references:
The firmware migrates stored settings itself. Downgrading to older firmware uses the old storage addresses and can restore defaults; export a settings profile before a firmware change. Bundled DFU releases are managed separately from settings schemas.
Run the dependency-free regression tests with Node.js:
node --test tests/settings-protocol.test.cjs tests/mcumgr.test.cjs tests/panel-engine.test.cjsThe settings list is being reworked into guided, task-oriented panels. Eleven panels sit above the settings list: Positioning (GPS), Data sending and storing (a message-type matrix over the LoRaWAN, satellite, LP0 and flash-store flags), Network (LoRaWAN), Device and security, Iridium satellite, VHF beacon, WiFi and BLE scanning, Tracker search (CMDQ), Fence monitor, External switch, and Sensors and diagnostics. The three schedules (GPS, satellite, VHF) share one component: a switch, a schedule type (fixed or day/night), intervals, and a 24-hour bar. A search box above the panels finds settings by label, help or key, and each panel can load the firmware defaults into the pending changes. The older feature cards (CMDQ, FenceEdge, WiFi scan, BLE scan) keep their actions and results; their settings blocks are hidden because the panels edit those settings now.
-
panels/panel-renderer.jsrenders panels from the definitions. A panel is a view over the same state as the settings list: it reads effective values (pending edit, else the device value, else the default) and writes through the list inputs, so validation, the pending draft and "Review and apply" are shared. Fields whose keys the loaded schema lacks are skipped; fields gated by another setting are greyed out with the reason. -
panels/panel-engine.jsholds the draft of pending edits, evaluates panel conditions, orders writes so dependents are written before a switch that enables them (and after one that disables them), and runs write-then-read-back verification. It has no DOM code and is tested intests/panel-engine.test.cjs. -
panels/panel-definitions.jsis the declarative panel format; the Positioning (GPS) panel is the reference definition. Keys absent from the loaded schema are skipped by renderers. -
settings-meta.jsoncarriescategorieswith a title, display order, and the order of settings inside each category; the settings list and the composer follow it instead of alphabetical order. -
Edits in the settings list collect in a draft. A bar offers "Review and apply", which shows a before/after list, writes in dependency order, reads each value back, and reports what the device confirmed. The per-setting Update buttons still work and clear their draft entry.
-
Import uses the same ordered, verified apply path. Export and import refuse to run while edits are pending, so a profile always matches what the device reported.
-
Once the header scrolls out of view, a slim bar pins to the top with the device name, a connection dot, firmware version, battery voltage, a pending-changes chip that jumps to the first edited field, and a small Disconnect button. Tapping the name scrolls back to the top. The bar hides on the DFU page and while disconnected; on narrow screens the firmware version and then the battery are dropped.
-
The HEX composer mounts the same panels above its settings list. Editing a value in a panel ticks that setting for inclusion in the payload; unticking it in the list leaves it out again. Panel values that are not included show the firmware default.
-
Help text in
settings-meta.jsonand the panel definitions only states what the firmware READMEs document (VHF beeps per burst, S-Band send modes, port bitmasks, satellite retry timing, air quality duty cycle, report-empty options). Settings the firmware does not document keep a plain label without claims about behaviour.
Flash log downloads stream one packet per line (base64) into memory. If the connection drops,
the device stops answering, or the page is closed mid-download, the packets received so far are
not lost: the app saves them as raw_logs-<type>-<device>_<timestamp>_PARTIAL-<n>msgs.txt as
soon as the download is interrupted, and the overlay offers "Save partial log" and "Retry". While
a download runs, the packets are also mirrored to IndexedDB every 25 packets and on page unload;
on the next visit the app offers to save or discard an interrupted capture it finds there. The
device keeps all logs until "Erase all logs" is used, so Retry downloads the full set again from
the start. The last record in a partial file may be cut mid-message.
DFU runs over MCUmgr SMP on the same GATT connection as the settings UART. Selecting a
built-in version checks it against the device immediately and, when the check is clean,
"Start DFU upload" is the only further click. The upload keeps up to three SMP packets in
flight (the "Packets in flight" setting in the Advanced card); the device answers each
packet with the offset it expects next, so a lost packet shows up as a repeated offset and
is resent, and the client drops back to one packet in flight after any timeout, loss or
rejected write. After the image is uploaded and marked for test, the app resets the device
and reconnects to the retained BluetoothDevice object automatically; no browser chooser is needed because
gatt.connect() does not require a user gesture, only requestDevice() does. The app
keeps retrying for up to three minutes while MCUboot swaps the image, then reads the
image state over SMP, checks that slot 0 carries the uploaded hash, and returns to the
device screen. The firmware confirms its own image on boot, so the SMP confirm step is a
safety net rather than a requirement. A screen wake lock is held during upload and
reboot. If automatic reconnect fails, the overlay offers a retry and a manual scan.
Adding a new bundled DFU firmware release:
- upload
.binfiles toassets/dfu/releases/<release-id>/... - add release entries to
assets/dfu/manifest.json(release id, firmware version, and file paths) - bump
CACHE_NAMEinservice-worker.jsso clients fetch the new bundle - add firmware release notes to
device-version-notes.jsonif you want notes shown in the UI
App versioning shown in UI:
- the app reads
version.jsonand shows it in the header (bothindex.htmlandcomposer.html) - update
version.jsonon every deployment, preferably from GitHub Actions
Example GitHub Actions step to generate version.json on each deploy:
- name: Generate app version metadata
run: |
if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
APP_VERSION="${GITHUB_REF_NAME}"
else
APP_VERSION="dev-${GITHUB_SHA::7}"
fi
printf '{\n "version": "%s",\n "commit": "%s",\n "built_at": "%s"\n}\n' \
"${APP_VERSION}" \
"${GITHUB_SHA}" \
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" > version.json