196 lines
7.3 KiB
Markdown
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.
|