Skip to content

EquilBridge

A local, replayable bridge for giving an old Equil Smartpen receiver a useful second life.

EquilBridge local notes viewer

EquilBridge captures receiver bytes over Bluetooth Classic RFCOMM, keeps the raw stream, decodes the verified model-3 live format, and exports traceable SVG pages. It also has a safe, read-only offline-memory importer and an optional local OCR/text-cleanup path. The core package has no runtime dependencies beyond Python's standard library.

What came back

EquilBridge did not restore the old app or cloud service. It restored a local path from a model-3 receiver's stored notes to evidence controlled by the user: raw note and wire bytes, replayable decoding, traceable SVG pages, and a local viewer/library.

Dedication

More than ten years ago, I received an Equil Smartpen as a graduation gift. This project is dedicated to the friend behind that gift, and to a friendship that has lasted longer than the service built around the pen.

Current status

Area Status
Software and replay path Ready: tests, fixtures, replay, and SVG export are local and repeatable
Hardware handshake Observed: an actual model-3 handshake was captured; receiver identity is intentionally anonymized
Offline note Observed: one 912-byte note decoded as 9 strokes / 126 points spelling Test 123
Offline-memory sync Live-validated on one receiver: an intentionally empty library imported 13 notes without duplicates, producing 13 pages and 39 local artifacts
Viewer sync path Live-validated on that receiver: the local viewer's Sync notes control completed the offline-memory import
Live pen-event suite Pending: dot, line, pen-up, page-button, disconnect, and recovery checks still need a receiver run

This is an interoperability bridge, not a replacement for the discontinued service. The offline-memory and local viewer sync paths have been validated on one paired model-3 receiver. Live pen-event acceptance remains pending until a receiver run verifies the measurable dot, line, pen-up, page-button, disconnect, and recovery checks.

Install and inspect

Python 3.11 or newer is supported. Development tools are kept in the dev dependency group; the base package remains dependency-free.

uv sync --all-groups
uv run equilbridge doctor --json
uv run equilbridge --help

On Linux, install and enable BlueZ using your distribution's package manager, then pair the receiver through the operating system. The receiver manual's desktop path uses the Others position; the manual also describes an Android mode for phone pairing. EquilBridge does not ship the historical Android application: once a receiver is paired, use the explicit-address Linux path.

bluetoothctl devices
uv run equilbridge capture --address <RECEIVER_MAC> --output captures/session.jsonl
# Ctrl-C is safe; complete chunks remain usable.
uv run equilbridge replay captures/session.jsonl
uv run equilbridge export captures/session.jsonl --output notes/

Replace the example address with the receiver address shown by your host. Do not paste a real address into public issues or documentation.

doctor is read-only. capture uses an explicit address and sends only the non-destructive model-3 session-start/session-close records after a validated handshake. replay regenerates frames and events from authoritative chunks; it never trusts derived records and never writes to hardware.

Offline memory

The sync command imports model-3 offline memory using observed, read-only directory/open requests. It writes note bytes, wire evidence, and SVG output locally as an all-or-nothing group. It does not erase or mutate pen memory.

uv run equilbridge sync --address <RECEIVER_MAC> --output data/notes

The checked-in Test 123 fixture and its rendering are intentionally local, anonymized, and disposable. They are not a claim about a user's note.

Offline replay sample

Offline replay SVG

Optional local OCR and text cleanup

OCR is opt-in. The core install does not pull PaddleOCR. The exact selectable profiles and model IDs are:

Profile Detection Recognition
balanced PP-OCRv6_small_det PP-OCRv6_small_rec
fast PP-OCRv6_tiny_det PP-OCRv6_tiny_rec
quality PP-OCRv6_medium_det PP-OCRv6_medium_rec
english-handwriting PP-OCRv5_mobile_det en_PP-OCRv5_mobile_rec
uv run equilbridge ocr docs/assets/offline-20260830-185424.png --profile english-handwriting --json
uv run equilbridge clean notes/page.txt --model qwen3:1.7b

Text cleanup is a proposal, not an overwrite: retain raw OCR and accepted text as separate provenance when building an application around this CLI. The Compose AI profile uses local Ollama with qwen3:1.7b and OLLAMA_KEEP_ALIVE=0; the model is started only when the ai profile is requested. The CLI accepts raster PNG or JPEG input for OCR. A synced SVG is provenance output; the web client rasterizes it locally before sending pixels to the OCR path.

On-demand Docker viewer

The default web service stores notes in the host bind mount and binds its viewer to 127.0.0.1. Compose uses host networking for this service because Docker's default bridge network does not expose the host Bluetooth AF_BLUETOOTH/RFCOMM namespace; host networking lets the viewer's Sync notes action open the receiver channel. Host networking also means Docker cannot remap the web container port with a ports entry: EQUIL_PORT sets the actual web process port (default 8765), so use it when the host port is busy. Its normal image build includes the optional OCR layer:

docker compose up --build web

Open http://127.0.0.1:8765/ while it runs. To choose another available host port, set EQUIL_PORT (for example, EQUIL_PORT=8876 docker compose up --build web) and open the corresponding loopback URL. For a smaller image without OCR dependencies, build the same service with:

EQUILBRIDGE_WITH_OCR=0 docker compose up --build web

Start the optional local AI profile and pull its default model only when you need text-cleanup proposals:

docker compose --profile ai up --build web ollama-model

This starts Ollama locally and pulls qwen3:1.7b; set EQUIL_MODEL to choose another local model. The services have restart: "no", and Ollama uses OLLAMA_KEEP_ALIVE=0, so neither the viewer nor the model stays running by itself. Notes persist in ./data/notes; OCR cache state persists in the named paddlex_cache volume, and downloaded Ollama model weights persist in the named ollama_models volume. Ollama is published on loopback only, at 127.0.0.1:${EQUIL_OLLAMA_PORT:-11434}; set EQUIL_OLLAMA_PORT if that host port is busy. The host-networked web service uses that loopback endpoint, while ollama-model stays on the Compose network and reaches the server at http://ollama:11434.

PaddleX cache ownership

The named paddlex_cache volume is seeded with the ownership of the build-time EQUIL_UID/EQUIL_GID pair (default 1000:1000) when it is first created: Docker copies the image's already-owned .paddlex directory into an empty volume. Docker never re-owns an existing named volume, and the non-root web service cannot repair a volume created under an older pair. Changing EQUIL_UID or EQUIL_GID after a first run therefore requires recreating or explicitly re-owning paddlex_cache; OCR otherwise fails once the container can no longer write its cache. This is an operational contract: keep the pair stable across runs, or apply one of the exact remediations below before the next start. A freshly created volume is seeded correctly for whatever pair the next build uses.

Find the exact volume name, which compose prefixes with the project name:

docker volume ls --filter name=paddlex_cache

The cache holds disposable PaddleX state only, so recreation is safe:

docker compose down
docker volume rm <project>_paddlex_cache

To keep the cached state instead, re-own it from a one-shot root helper container before starting the stack:

docker run --rm -u 0 -v <project>_paddlex_cache:/cache alpine \
  chown -R <new-uid>:<new-gid> /cache

Neither the notes in ./data/notes nor the ollama_models volume is affected by either remediation.

Bluetooth sync is a deliberate one-shot profile. Pair the receiver with BlueZ first, then provide its address only for that run:

EQUIL_ADDRESS=<RECEIVER_MAC> docker compose --profile bluetooth run --rm bluetooth-sync

It requires host BlueZ access. When you are done, stop the default and either optional profile services, while preserving notes and model weights:

docker compose --profile ai --profile bluetooth down --remove-orphans

Sync notes from the viewer

The viewer can also import receiver notes itself through the compact Sync notes control in the library. Start the Bluetooth-enabled viewer with the receiver address, after pairing the receiver with BlueZ:

EQUIL_ADDRESS=<RECEIVER_MAC> docker compose up --build web

Then open http://127.0.0.1:${EQUIL_PORT:-8765}/ (or the port selected with EQUIL_PORT) and press Sync notes. The receiver must be awake and switched to Others mode for the whole transfer, so keep it that way until the control reports a result. The viewer container receives the same Bluetooth-specific device, D-Bus, and capability grants as the one-shot bluetooth-sync profile. Its required host networking additionally gives it the host network namespace, though the viewer process itself still binds only to loopback. Without EQUIL_ADDRESS, the viewer starts normally and the control reports that receiver sync is not configured.

Import is incremental: identical note bytes for the same timestamp are skipped instead of being written again, while different content with the same timestamp is kept as the next numbered copy. If a sync fails, the page says what happened and asks you to keep the receiver awake in Others mode and retry; notes already imported stay untouched.

Remove a note locally

The selected page has a Remove from Library action. Confirming it moves the page's SVG, transcription sidecar, and any note/wire evidence into the hidden, recoverable data/notes/.equilbridge-trash/ directory; it never permanently erases them. The receiver is not contacted or changed. A later Sync notes operation may re-import the note, and any unsaved transcription draft for the selected page is removed from the viewer along with the page.

How it fits together

BlueZ / RFCOMM channel 1
          -> lossless chunks -> 16-byte framer -> model-3 decoder
          -> traceable stroke/page events -> JSONL replay or SVG export
                                      \-> optional local OCR / Ollama proposal

Raw chunks are the source of truth. Derived frames, events, strokes, and text remain linked to their source records. Malformed data becomes diagnostics; it does not become a fabricated mark. The web viewer binds to loopback by default. RFCOMM and the local HTTP viewer are not application-layer encrypted, so keep captures private and use --allow-remote only with a network boundary you understand.

Provenance and clean-room boundary

The implementation is an independent expression of observed interoperability facts: frame boundaries, field packing, command shapes, and coordinate equations. It does not include a historical APK, JAR, plugin source, or decompiled source. Raw local captures are evidence, not redistributed vendor software.

Primary references:

Equil and Smartpen are third-party marks used only to describe compatibility. EquilBridge is independent and is not affiliated with, endorsed by, or sponsored by any hardware manufacturer or former service operator.

Limitations

  • Linux with BlueZ is the supported live path; pairing assistance and a GUI are intentionally outside the release-critical recovery path.
  • Coordinates are replayable inferred receiver units, not a promise of factory calibration.
  • The live pen-event suite is still pending. The one-receiver offline-memory sync result does not cover dot, line, pen-up, page-button, disconnect, or recovery events.
  • Receiver memory deletion is unsupported because no compatible destructive command has been observed. Local page removal is recoverable and leaves the receiver unchanged.
  • No cloud sync, analytics, trackers, remote assets, or hosted fonts are used by the static project site.
  • Offline import is deliberately read-only and limited to the observed model-3 memory format.

Contributing and licence

See CONTRIBUTING.md, SECURITY.md, and CODE_OF_CONDUCT.md. EquilBridge is released under the Apache License 2.0; see NOTICE for attribution and the trademark boundary.

About

A local, replayable bridge for recovering notes from Equil Smartpen receivers

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages