trainlog/docs/design/sync_bluetooth_transport_v1.md

7.3 KiB

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:

{
  "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.