trainlog/docs/design/sync_bluetooth_transport_v1.md

196 lines
7.3 KiB
Markdown

# Bluetooth synchronization transport V1
## Status
`TRAINLOG_SYNC_BLUETOOTH_TRANSPORT_V1=PASS/FROZEN`
This transport is additive. It does not change generation manifests, artifact
formats, causal deletion, revision ancestry, acknowledgement semantics, SQLite
schemas, or the frozen Trainlog session format.
## Product behavior
Normal synchronization is automatic when the authorized Android phone and
desktop are within Bluetooth range.
1. Android's already user-enabled sync foreground service maintains a bonded
Bluetooth Classic RFCOMM connection to the selected desktop.
2. The desktop advertises one Trainlog-specific RFCOMM profile.
3. On a fresh Bluetooth connection Android publishes one bounded
full-generation request. Reconnect noise is rate-limited.
4. The existing full-generation orchestrator performs the same bidirectional
generation/ACK conversation through the Bluetooth byte transport.
5. If Bluetooth is unavailable, the existing MTP transport remains the wired
fallback.
6. Google Drive is not a synchronization transport in the target configuration.
After convergence, the desktop AI export is transferred to Android and
Android publishes it to the user-selected `Trainlog/AI` Drive folder.
Drive publication failure never rolls back a completed sync.
The manual Web and Android Synchroniser actions remain as recovery/forced-sync
controls.
## Private live-device evidence — 2026-09-22
The authorized Samsung Android peer and ArchASP completed a physical Bluetooth
Classic/RFCOMM synchronization without MTP or Drive as a sync transport.
- request: `sr_86ccc07f-cc5b-4506-86c1-06c00bd8b06a`;
- run: `sy_df8f87fb-e4c1-44f5-b3f7-8fe87942d19e`;
- inbound Android generation:
`gen_9609d163-4a3a-4187-a806-84379a1bcf4c`;
- outbound desktop generation:
`gen_bc1d5e4b-4b16-49bc-b228-c3ff1f1a1705`;
- final transport: `bt`;
- final Bluetooth state: `success`;
- final phase/result: `completed/completed`;
- desktop SQLite integrity: `ok`;
- post-sync AI export: `delivered_to_android`;
- Android copy:
`Documents/Trainlog/trainlog_ai_export_v1.json`, 66,971 bytes,
SHA-256 `49f7f02fdeebd15699655e43fdb475bb20d9dd2d1bc59d2bdb4cd099ae50ecda`.
The initial legacy bond contained mismatched link-key state. The private device
was explicitly re-paired; Android then reported the bond ready and established
the Trainlog RFCOMM service. Android now also repairs a missing bond through the
normal platform pairing flow before attempting RFCOMM.
The Android SAF destination `Trainlog/AI` was then selected explicitly by
the user. A Web-triggered Bluetooth-only run completed immediately afterward:
- request: `sy_ab1869d3-7f25-497f-a338-97213ec08370`;
- run: `sy_f3ab061f-b4f8-44e9-9911-c0ad3283b457`;
- inbound Android generation:
`gen_c74705f9-04dc-4ffe-9e67-6ae5bf08d1a2`;
- outbound desktop generation:
`gen_0b33d209-35f1-41c0-9f98-6adf843b2a5f`;
- `transport=bt`, `bluetooth_state=success`;
- both directions are durable `sqlite-commit-full`;
- `ai_post_sync=delivered_to_android`;
- desktop SQLite integrity: `ok`.
The resulting local AI export was 66,971 bytes with SHA-256
`73ce8d3ca6cef3080ed2da77d58ef600bb79e6d2089f3472632b59aea76a8b90`.
The exact Google Drive object in `Trainlog/AI/trainlog_ai_export_v1.json` was
modified at `2026-09-22T14:50:11.875Z`, had the same 66,971-byte size, and
the downloaded Drive object had the exact same SHA-256. This closes the
Android-SAF publication proof without rclone.
## Trust and pairing
Bluetooth device names are presentation only and are never authority.
Desktop trusted configuration pins:
- Android sync `peer_id`;
- bonded Android Bluetooth device address;
- Trainlog RFCOMM service UUID.
Android persists the selected bonded desktop address after an explicit one-time
selection. The RFCOMM service uses normal bonded-device authentication.
Current private deployment values are installation configuration, not protocol
constants.
## RFCOMM service
Trainlog service UUID:
`f0d1c0de-7a11-4f62-9b7c-545241494e4c`
The desktop registers an `org.bluez.Profile1` server with BlueZ. Android is
the RFCOMM client. This direction lets Android resolve the advertised service
through SDP without hidden Android channel APIs.
The desktop Bluetooth agent is persistent and owns the BlueZ profile. Local
sync workers never talk to BlueZ directly; a bounded local adapter talks to the
agent over a mode-0600 Unix socket.
## Bluetooth byte envelope
Each Bluetooth frame is:
- 4-byte unsigned big-endian JSON header length;
- UTF-8 JSON header (maximum 64 KiB);
- optional payload whose length and SHA-256 are declared in the header.
Payload limit is 256 MiB and individual exchange files remain subject to their
existing generation bounds. A receiver validates length and SHA-256 before
making received bytes visible.
Initial frame:
```json
{
"format": "trainlog-bt-frame",
"version": 1,
"type": "hello",
"peer_id": "peer_<uuid>",
"protocol": "trainlog-bt-files-v1"
}
```
The local adapter exposes only `pull` and `push`, matching the existing MTP
adapter boundary. Transfers use a bounded ZIP archive containing relative
exchange paths. Extraction rejects absolute paths, `..`, symbolic links,
duplicates and excess file/byte counts. Each destination is replaced from a
same-directory temporary after complete bytes are durable.
## Pull ownership
Android pull snapshots include only Android-owned/current coordination objects:
- `android-peer-v1.json`;
- `trainlog-sync-full-generation-request-v1.json`;
- `trainlog-sync-request-v1.json`;
- `android-generation-v1.json`;
- `android-consumption-ack-v1.json`;
- `android-generation-error-v1.json`;
- the immutable generation referenced by `android-generation-v1.json`.
Desktop-owned coordination files are never treated as Android source
authority.
## Push ownership
Desktop workers already construct phase-specific disposable outboxes. Bluetooth
push accepts exactly that bounded outbox and atomically publishes those files
under Android's canonical `Documents/Trainlog` directory.
## Concurrency
Bluetooth and MTP have independent physical transport locks. The common
business `sync.lock` still serializes synchronization conversations.
A background discovery probe must never own `sync.lock`. A Web-triggered
conversation must never fail merely because an idle transport probe is in
progress.
## Automatic arrival trigger
A successful RFCOMM connection is an arrival event. Android publishes at most
one automatic request per connection and persists a short reconnect cooldown.
The same connection is not a repeating timer; no connection means no automatic
request.
A later optimization may compare explicit dirty revisions before generating a
request, but V1 correctness does not depend on such an optimization.
## MTP fallback
MTP remains supported and keeps its current fail-closed semantics. Automatic
desktop selection is:
`Bluetooth -> MTP -> device_unavailable`
Drive is never an implicit fallback in the target configuration.
## AI export publication
The desktop remains the canonical producer of
`trainlog_ai_export_v1.json`. After a completed bidirectional sync it sends
the exact export bytes to Android. Android publishes/replaces the export in the
persisted SAF tree selected for `Trainlog/AI`.
The export has its own pending/success state. A Drive provider error is not a
sync error and is retried later by Android.