Files
cardboy/Firmware/cardboy-companion
2025-10-20 00:58:33 +02:00
..
2025-10-20 00:58:33 +02:00
2025-10-19 19:54:24 +02:00
2025-10-19 23:07:20 +02:00

Cardboy Time Sync Companion

This SwiftUI app connects to the Cardboy device over Bluetooth Low Energy and updates its wall clock using the custom time sync service exposed by the firmware. The sources live inside the existing cardboy-companion/cardboy-companion.xcodeproj so you can open and run them directly in Xcode.

Requirements

  • Xcode 15 or newer
  • iOS 16 or newer deployment target (can be lowered to 15 with minor API tweaks)
  • A Cardboy running firmware that includes the BLE time sync service

How it works

  1. The app scans for peripherals exposing service UUID 00000001-CA7B-4EFD-B5A6-10C3F4D3F230.
  2. Once connected it discovers characteristic 00000002-CA7B-4EFD-B5A6-10C3F4D3F231.
  3. Tapping Sync Now writes a 12byte payload containing:
    • 8 bytes Unix epoch seconds (little endian)
    • 2 bytes time zone offset in minutes from UTC (little endian)
    • 1 byte DST flag (1 if daylight saving is active)
    • 1 reserved byte (0)
  4. The firmware applies the timestamp with settimeofday() and updates the TZ environment variable so the clock app renders local time.

Usage

  1. Open cardboy-companion/cardboy-companion.xcodeproj in Xcode.
  2. Ensure the CoreBluetooth capability is enabled for the cardboy-companion target and keep the Uses Bluetooth LE accessories background mode on (preconfigured in this project).
  3. Build & run on a real device (BLE is not available in the simulator).
  4. Allow Bluetooth permissions when prompted. The app keeps scanning in the background, so the Cardboy can request a sync even while the companion is not foregrounded. Tap Sync Now any time you want to trigger a manual refresh.
  5. Switch to the Files tab to browse the LittleFS volume on the Cardboy: you can upload ROMs from the Files picker, create/remove folders, rename entries, delete files, and download items back to the phone for sharing.

BLE File Service Protocol

The ESP firmware exposes a custom GATT service (UUID 00000010-CA7B-4EFD-B5A6-10C3F4D3F230) with two characteristics:

Characteristic UUID Properties Direction Description
File Command 00000011-CA7B-4EFD-B5A6-10C3F4D3F231 Write / Write Without Response iOS → ESP Sends file management requests
File Response 00000012-CA7B-4EFD-B5A6-10C3F4D3F232 Notify ESP → iOS Streams command results (responses or data)

All payloads share the same framing. Commands written to the File Command characteristic use:

Offset | Size | Description
-------+------+------------
0      | 1    | Opcode (see table below)
1      | 1    | Reserved (set to 0)
2      | 2    | Little-endian payload length in bytes (N)
4      | N    | Command payload

Notifications from the File Response characteristic use:

Offset | Size | Description
-------+------+------------
0      | 1    | Opcode (echoed from command)
1      | 1    | Status byte (bit 7 = completion flag; lower 7 bits = error code)
2      | 2    | Little-endian payload length (N)
4      | N    | Response payload (command-specific)

Status byte semantics:

  • Bit 7 (0x80) set → final packet for the current command (no further fragments).
  • Lower 7 bits = error code (0 = success, otherwise errno-style code echoed back).
    • On error the response payload may contain a UTF-8 message.

Opcodes and Payloads

Opcode Name Command Payload Response Payload
0x01 List Directory uint16 path_len + UTF-8 path One or more fragments, each entry encoded as:
- uint8 type (0=file, 1=dir)
- uint8 reserved
- uint16 name_len
- uint32 size (0 for dirs)
- name_len bytes UTF-8 name
Final notification has completion bit set.
0x02 Upload Begin uint16 path_len + UTF-8 path + uint32 file_size Empty payload on success. Starts upload session (expects UploadChunk packets).
0x03 Upload Chunk Raw file bytes Empty payload ack for each chunk.
0x04 Upload End No payload Empty payload confirming completion.
0x05 Download Request uint16 path_len + UTF-8 path First notification: 4-byte little-endian total file size; subsequent notifications stream raw file data fragments. Completion bit marks the final chunk.
0x06 Delete File uint16 path_len + UTF-8 path Empty payload on success.
0x07 Create Directory uint16 path_len + UTF-8 path Empty payload on success.
0x08 Delete Directory uint16 path_len + UTF-8 path Empty payload on success.
0x09 Rename Path uint16 src_len + UTF-8 source path + uint16 dst_len + UTF-8 destination path Empty payload on success.

Notes

  • Paths are absolute within the LittleFS volume; the firmware normalizes them and rejects entries containing ...
  • Large responses (directory lists, downloads) may arrive in multiple notifications; the iOS client aggregates fragments until it sees the completion flag.
  • Uploads are initiated with Upload Begin (including total size), followed by one or more Upload Chunk writes, and Upload End when done.
  • Errors from the firmware propagate via the status byte; when status & 0x7F != 0, the notification payload typically includes a UTF-8 error message (e.g., "stat failed").

This protocol mirrors the implementation in components/backend-esp/src/time_sync_service.cpp and the Swift client in TimeSyncManager.swift. Update both sides if new commands are added.

Optionally bundle this code into your existing app—TimeSyncManager is selfcontained and can be reused.