A local, replayable bridge for giving an old Equil Smartpen receiver a useful second life.
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.
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.
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.
| 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.
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 --helpOn 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.
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/notesThe checked-in Test 123 fixture and its rendering are intentionally local,
anonymized, and disposable. They are not a claim about a user's note.
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.7bText 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.
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 webOpen 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 webStart 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-modelThis 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.
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_cacheThe cache holds disposable PaddleX state only, so recreation is safe:
docker compose down
docker volume rm <project>_paddlex_cacheTo 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> /cacheNeither 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-syncIt 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-orphansThe 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 webThen 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.
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.
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.
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:
- FCC user manual record: model identity, receiver modes, and pairing guidance.
- Interoperability reference repository: consulted for historical wire observations; no code or binary is copied here.
- PaddleOCR
- Ollama
- FCC
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.
- 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.
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.

