Impulse Mobile App — Specification¶
Version: 0.3 Scope: Flutter mobile companion app for the Impulse Watch and Anchor devices. Covers current architecture and all implemented features. Intended as a complete reference for a coding agent implementing or modifying the app.
Table of Contents¶
- Overview & Architecture
- Models
- Services
- Screens
- Widgets
- Utilities
- Theme
- BLE Constants & Wire Formats
- Local Storage
- Device Settings & Anchor Servo Control
1. Overview & Architecture¶
The Impulse app is a Flutter mobile control center for managing watch enforcement automations and BLE-connected anchor devices.
Core responsibilities: 1. Scan for and manage BLE devices (watches and anchors). 2. Create, edit, and delete time-based automations with location/WiFi triggers. 3. Push schedules and settings to the watch over BLE. 4. Monitor live watch status (state, battery, worn, WiFi). 5. Identify and control anchors over BLE. 6. Provide a real-time debug packet inspector.
Key dependencies (pubspec.yaml):
- flutter_blue_plus 1.32.12 — BLE scanning and GATT
- shared_preferences 2.3.4 — Key-value persistence
- permission_handler 11.3.1 — Bluetooth and location permissions
Navigation: 4-tab BottomNavigationBar (Devices, Automations, Settings, Debug).
1.1 Architectural Patterns¶
Services layer: Three singleton services own all state and side effects. UI reads from them and calls their methods.
| Service | Responsibility |
|---|---|
BluetoothService |
Device history, BLE scanning, anchor IP management |
WatchService |
Active GATT connection to one watch, characteristic I/O |
AutomationService |
CRUD for automations, schedule encoding |
DebugLogService |
Broadcast stream of BLE packet log entries |
UI layer: StatefulWidget screens. State updates via setState(). No Provider/Riverpod/Bloc. Services are accessed as singletons directly inside widgets.
Streams: WatchService exposes two broadcast StreamController streams (statusStream, seenAnchorsStream) so multiple screens can subscribe without interference.
Persistence: All persistent state lives in SharedPreferences (JSON-encoded). No SQLite.
2. Models¶
2.1 BluetoothDeviceModel (lib/models/bluetooth_device_model.dart)¶
Represents a known BLE device (watch or anchor).
enum DeviceType { watch, anchor, unknown }
class BluetoothDeviceModel {
final String id; // Anchor: UUID from iBeacon data. Watch: platform remote ID.
final String name; // Display name. Editable by the user (app-only).
final bool isConnected; // Current connection status.
final int rssi; // Signal strength in dBm.
final DateTime lastSeen; // Timestamp of last BLE advertisement.
final DeviceType deviceType; // Inferred from advertisement data.
final String? ipAddress; // IPv4 address (anchors only).
final DateTime? ipLastUpdated;
final String? bleRemoteId; // Platform remote ID if device appeared in a direct scan.
// Null for anchors known only via watch's SeenAnchors.
}
id semantics:
- For anchors: the 16-byte UUID extracted from iBeacon manufacturer data (formatted as UUID string).
- For watches: the platform-assigned BLE remote ID (MAC on Android, UUID on iOS).
bleRemoteId: Only set when the device appeared in a direct BLE scan by the app. Anchors known only from the watch's SeenAnchors characteristic will have bleRemoteId = null and cannot be connected to directly until scanned.
name: Initially populated from the BLE advertisement name. Can be overwritten by the user through the Device Settings modal (Section 10). Changes are persisted to SharedPreferences immediately.
Persistence: Serialized to JSON, stored in SharedPreferences under "paired_devices".
2.2 Automation (lib/models/automation_model.dart)¶
Represents a single scheduled enforcement event.
enum RecurrenceType { once, daily, weekly, monthly }
enum Criteria { getAway, stayNear, getOffWifi, getOnWifi }
enum EnforcementProfile {
strictSilent, normalSilent, looseSilent,
strictBoth, normalBoth, looseBoth,
strictBuzz, normalBuzz, looseBuzz
}
enum AnchorEnforcementProfile { light, medium, hard }
class Automation {
final String id; // UUID v4
final DateTime referenceDate; // Anchor/origin date (UTC)
final TimeOfDay startTime;
final TimeOfDay endTime;
final RecurrenceType recurrenceType;
final int? dayOfWeek; // 1–7 (Mon–Sun); only if weekly
final int? dayOfMonth; // 1–31; only if monthly
final Criteria criteria;
final EnforcementProfile profile;
final bool negate; // If true, cancels the matching recurring event on referenceDate
final String? anchorId; // UUID of target anchor (for getAway/stayNear)
final String? wifiSSID; // Target network name (for getOnWifi/getOffWifi)
final List<String> beepAnchors; // Anchor UUIDs to beep if watch removed
final AnchorEnforcementProfile? anchorProfile; // Required if beepAnchors is non-empty
final Color color; // UI-only calendar block color
}
appearsOnDate(date): Returns true if this automation is active on the given date:
- once: date == referenceDate
- daily: date >= referenceDate
- weekly: date >= referenceDate AND day_of_week(date) == dayOfWeek
- monthly: date >= referenceDate AND day_of_month(date) == dayOfMonth
Validation (enforced in AddAutomationModal before save):
- If WiFi criteria: wifiSSID must be non-empty.
- If anchor criteria: anchorId must be non-null.
- If beepAnchors is non-empty: anchorProfile must be non-null.
- endTime must be after startTime.
Persistence: JSON-encoded, stored in SharedPreferences under "automations_v2".
3. Services¶
3.1 BluetoothService (lib/services/bluetooth_service.dart)¶
Singleton. Owns the device history (watches + anchors) and BLE scan management.
State:
- _deviceHistory: List<BluetoothDeviceModel> — all known devices.
Initialization: initialize() loads _deviceHistory from SharedPreferences on app start.
Key methods:
| Method | Behavior |
|---|---|
deviceHistory |
Getter: all devices |
watches |
Getter: filtered by deviceType == watch |
anchors |
Getter: filtered by deviceType == anchor |
startScan() |
Opens a 15-second BLE scan; returns Stream<List<ScanResult>> |
stopScan() |
Terminates the active scan |
classifyDevice(result) |
Returns DeviceType based on service UUIDs or iBeacon manufacturer data (company ID 0xFFFF) |
extractAnchorUuid(result) |
Parses iBeacon manufacturer data to return the anchor's UUID string, or null |
addOrUpdateDevice(device) |
Upserts by id, persists to SharedPreferences |
updateDeviceStatus(id, connected, rssi) |
Updates connection and signal fields, persists |
updateAnchorIp(anchorId, ipAddress) |
Sets IPv4 on an anchor record, persists |
getSignalStrength(rssi) |
Returns label string: "Excellent" / "Good" / "Fair" / "Weak" |
iBeacon detection:
manufacturerData[0xFFFF] exists
AND data.length >= 23
AND data[0] == 0x02 // iBeacon type
AND data[1] == 0x15 // iBeacon length
→ Extract UUID from bytes [2:18]
3.2 WatchService (lib/services/watch_service.dart)¶
Singleton. Manages the active GATT connection to a single watch.
Connection state:
- _device: BluetoothDevice?
- _gattService: BluetoothService?
- 7 characteristic references (bound on connect):
| Field | UUID | Mode |
|---|---|---|
_wifiCredChar |
watchWifiCredCharUuid (0x0011) |
Write |
_schedCtrlChar |
watchSchedCtrlCharUuid (0x0012) |
Write + Notify |
_schedDataChar |
watchSchedDataCharUuid (0x0013) |
Write |
_settingsChar |
watchSettingsCharUuid (0x0014) |
Write + Notify |
_seenAnchorsChar |
watchSeenAnchorsCharUuid (0x0015) |
Notify |
_statusChar |
watchStatusCharUuid (0x0016) |
Notify |
_anchorIpChar |
watchAnchorIpCharUuid (0x0017) |
Write + Notify |
Streams (broadcast):
- statusStream: Stream<WatchStatus> — emits on every status notification.
- seenAnchorsStream: Stream<List<SeenAnchorInfo>> — emits on every seen-anchors notification.
Models:
class WatchStatus {
final int activityState; // 0=dormant, 1=enforcement, 2=dormant_sleep
final bool btConnected;
final bool wifiConnected;
final bool worn;
final int batteryPct; // 0xFF if unavailable
final String? activeEventId; // UUID of active event, or null
}
class SeenAnchorInfo {
final String uuid;
final int rssi; // dBm (raw byte − 128)
final DateTime lastSeen;
}
Connect lifecycle:
connect(device):
device.connect(timeout: 10s)
device.discoverServices()
_bindCharacteristics()
_subscribeNotifications():
Enable notify on _statusChar and _seenAnchorsChar
Read current values immediately
Stream updates through _statusController and _seenAnchorsController
disconnect():
Cancel all subscriptions
device.disconnect()
Clear all characteristic references
Key methods:
| Method | Payload | Response |
|---|---|---|
pushWifiCredentials(ssid, password) |
JSON {"ssid":"...","password":"..."} written to _wifiCredChar |
None |
pushSettings(disconnectedIsDormant, awayIsDormant, tzOffsetMinutes) |
[flag(1B), flag(1B), tzOffset(2B LE)] written to _settingsChar |
0x01 ack (5s timeout) |
pushSchedule(automations) |
3-phase protocol (see below) | 0x01 on success, 0x00 on failure |
pushAnchorIpTable(anchors) |
[count(1B)] + [uuid(16B) + ipv4(4B) + ts(4B)] × N written to _anchorIpChar |
0x01 ack (5s timeout) |
Schedule transfer (3-phase):
Phase 1 — BEGIN:
Write to _schedCtrlChar: [0x01, blob_length(u32 LE)]
Await notify: [0x01] = ready (10s timeout)
Phase 2 — DATA:
For each 20-byte chunk of blob:
Write to _schedDataChar (no response)
Wait 10ms
Phase 3 — END:
Write to _schedCtrlChar: [0x02, crc32(u32 LE)]
Await notify: [0x01] = success, [0x00] = CRC mismatch (10s timeout)
Return: true if phase 1 and phase 3 both ACK with 0x01.
3.3 AutomationService (lib/services/automation_service.dart)¶
Singleton. CRUD and persistence for automations.
State: _automations: List<Automation>
Persistence: SharedPreferences key "automations_v2" (JSON array).
Key methods:
| Method | Behavior |
|---|---|
initialize() |
Loads automations from SharedPreferences |
automations |
Getter |
addAutomation(a) |
Appends and persists |
updateAutomation(a) |
Replaces by id and persists |
deleteAutomation(id) |
Removes by id and persists |
getAutomationsForDate(date) |
Filters by appearsOnDate(date) |
calculateAutomationLayouts(automations) |
Returns Map<String, AutomationLayout> for non-overlapping column placement in the calendar grid |
Layout algorithm: Sorts by startMinutes (asc) then durationMinutes (desc). Greedily assigns each automation to the first non-overlapping column.
3.4 DebugLogService (lib/services/debug_log_service.dart)¶
Singleton. Real-time BLE packet log with a max of 300 entries.
class DebugLogEntry {
final DateTime timestamp;
final String source; // e.g. "status", "seen_anchors", "schedule"
final String decoded; // Human-readable interpretation
final String hex; // Space-separated hex bytes
}
Methods: log(source, decoded, rawBytes), clear(), stream (broadcast).
Logging points: Watch connection events, schedule transfer phases, status/seen-anchors notifications, anchor IP pushes.
4. Screens¶
4.1 Main (lib/main.dart)¶
App entry point. Initializes services, builds the 4-tab scaffold.
On init:
- Calls BluetoothService.initialize() and AutomationService.initialize().
- Subscribes to WatchService.seenAnchorsStream:
- For each SeenAnchorInfo, creates or updates a BluetoothDeviceModel (type: anchor, isConnected: false, bleRemoteId: null).
- Calls BluetoothService.addOrUpdateDevice().
4.2 Devices Screen (lib/screens/devices_screen.dart)¶
Purpose: Scan for BLE devices, manage watch connection, push schedules, identify anchors.
State:
- _isScanning: bool
- _isPushing: bool
- _statusMsg: String?
- _watchStatus: WatchStatus?
- _statusSub: StreamSubscription<WatchStatus>?
Layout:
1. Optional status banner (operation feedback).
2. Watches section: One card per known watch.
- Shows: device icon, name, connection state.
- When connected: live status panel (activity state, WiFi, worn, battery, active event UUID).
- Buttons: "Connect" / "Disconnect", and when connected: "Push Schedule Now".
- Settings icon (gear button) on each card → opens DeviceSettingsModal (see Section 10).
3. Anchors section: One card per known anchor.
- Shows: device icon, name, UUID prefix, RSSI + signal label, IPv4 if known.
- "Beep" icon button → identify anchor over BLE.
- Settings icon (gear button) on each card → opens DeviceSettingsModal (see Section 10).
Actions:
_startScan()
1. Call BluetoothService.startScan().
2. For each ScanResult: classify device type, extract anchor UUID if applicable, create BluetoothDeviceModel with bleRemoteId populated, call addOrUpdateDevice().
3. Scan auto-stops after 15 seconds.
_connectWatch(model)
1. Retrieve BluetoothDevice from flutter_blue_plus using model.bleRemoteId.
2. Call WatchService.connect(fbpDevice).
3. Call BluetoothService.updateDeviceStatus(model.id, true, model.rssi).
4. Subscribe to WatchService.statusStream → update _watchStatus via setState().
_disconnectWatch()
1. Call WatchService.disconnect().
2. Call BluetoothService.updateDeviceStatus(model.id, false, model.rssi).
_pushSchedule()
1. Call WatchService.pushSchedule(AutomationService().automations).
2. Show success or failure message in status banner.
_identifyAnchor(model)
1. Requires model.bleRemoteId (device must have been directly scanned).
2. Connect to anchor BLE device.
3. Discover services, find anchorServiceUuid.
4. Write 0x01 to anchorIdentifyCharUuid (write no-response).
5. Disconnect.
4.3 Automations Screen (lib/screens/automations_screen.dart)¶
Purpose: Visual day-based automation editor.
Layout:
1. Date navigator (previous / today / next day).
2. 24-hour calendar grid:
- Left column: hour labels (12 AM–11 PM, 60px per hour).
- Main area: AutomationBlock widgets positioned by start time and duration.
3. FAB (orange): open AddAutomationModal.
Actions:
_addAutomation()
- Show AddAutomationModal(initialDate: _selectedDate).
- If result is non-null: call AutomationService.addAutomation(), setState().
_editAutomation(automation)
- Show AddAutomationModal(existingAutomation: automation).
- If result is non-null: call AutomationService.updateAutomation(), setState().
Rendering blocks:
- Call getAutomationsForDate(_selectedDate) → list.
- Call calculateAutomationLayouts(list) → layout map.
- Render one AutomationBlock per automation using layout for column positioning.
4.4 Settings Screen (lib/screens/settings_screen.dart)¶
Purpose: Watch configuration and IP table management.
Sections:
-
Connection Status: Shows "Watch connected" or "Not connected" (read-only).
-
Push WiFi Credentials: - Text fields: SSID, password (toggleable visibility). - "Send to Watch" →
WatchService.pushWifiCredentials(ssid, password). -
Watch Behaviour: - Toggle: "Enforce when disconnected from phone" (
DISCONNECTED_IS_DORMANT). - Toggle: "Enforce when on WiFi but no BLE" (AWAY_IS_DORMANT). - Timezone slider: UTC−12 to UTC+14 in 15-minute increments. - "Save & Push to Watch" →WatchService.pushSettings(...)+ persist to SharedPreferences. -
Schedule: - Shows event count. - "Push Schedule to Watch" →
WatchService.pushSchedule(automations). -
Anchor IP Addresses: - If no anchors: "No anchors discovered". - Otherwise: text field per anchor for IPv4 input. - "Push IPs to Watch" → for each non-empty IP field:
BluetoothService.updateAnchorIp(), thenWatchService.pushAnchorIpTable().
Persistence keys: "setting_disc_dorm" (bool), "setting_away_dorm" (bool), "setting_tz" (int).
4.5 Debug Screen (lib/screens/debug_screen.dart)¶
Purpose: Live BLE packet inspector.
Layout: Scrollable list of DebugLogEntry tiles. Each tile: timestamp + source badge + decoded text + hex bytes.
Actions: Auto-scroll toggle, copy all to clipboard, clear log.
5. Widgets¶
5.1 AddAutomationModal (lib/widgets/add_automation_modal.dart)¶
Full-screen dialog for creating or editing an automation.
Props:
- initialDate: DateTime
- existingAutomation: Automation? (if provided, pre-fills form)
Sections (in order):
1. Date navigator header.
2. Start/end time pickers (tap to open system showTimePicker()).
3. Recurrence selector: Once / Daily / Weekly / Monthly.
4. Criteria dropdown: Stay Near / Get Away / Get on WiFi / Get off WiFi.
5. Target input: SSID text field (WiFi) or anchor dropdown (anchor-based).
6. Enforcement profile dropdown (9 options).
7. Beep anchors: checkbox list of known anchors; if any selected, shows anchor profile dropdown.
8. Color picker: 9 swatches.
9. Negate toggle with explanation.
10. Cancel and Save/Update buttons.
Validation (_canSave): WiFi criteria requires non-empty SSID; anchor criteria requires non-null anchor ID.
On save: Builds Automation object, pops with result.
5.2 AutomationBlock (lib/widgets/automation_block.dart)¶
A Positioned colored block in the calendar grid.
Props: automation, layout, onTap.
Positioning:
- top = startMinutes px
- height = durationMinutes px
- left and width derived from layout.columnIndex / layout.totalColumns
Content (adaptive to height): 1. Target label (SSID, anchor name, or UUID prefix) — always shown. 2. Time range string — if height > 30px. 3. Criteria label — if height > 60px.
6. Utilities¶
6.1 BLE Constants (lib/utils/ble_constants.dart)¶
// Anchor GATT service
anchorServiceUuid = "4a0f0001-f8ce-11ee-8001-020304050607"
anchorIdentifyCharUuid = "4a0f0002-f8ce-11ee-8001-020304050607" // Write no-response
anchorWifiCredCharUuid = "4a0f0003-f8ce-11ee-8001-020304050607" // Write with response
anchorSettingsCharUuid = "4a0f0004-f8ce-11ee-8001-020304050607" // Write with response
anchorSchedCtrlCharUuid= "4a0f0005-f8ce-11ee-8001-020304050607" // Write with response
anchorSchedDataCharUuid= "4a0f0006-f8ce-11ee-8001-020304050607" // Write no-response
anchorToggleCharUuid = "4a0f0007-f8ce-11ee-8001-020304050607" // Read + Write with response
// Watch GATT service
watchServiceUuid = "4a0f0010-f8ce-11ee-8001-020304050607"
watchWifiCredCharUuid = "4a0f0011-f8ce-11ee-8001-020304050607" // Write no-response
watchSchedCtrlCharUuid = "4a0f0012-f8ce-11ee-8001-020304050607" // Write + Notify
watchSchedDataCharUuid = "4a0f0013-f8ce-11ee-8001-020304050607" // Write no-response
watchSettingsCharUuid = "4a0f0014-f8ce-11ee-8001-020304050607" // Write + Notify
watchSeenAnchorsCharUuid = "4a0f0015-f8ce-11ee-8001-020304050607" // Notify
watchStatusCharUuid = "4a0f0016-f8ce-11ee-8001-020304050607" // Notify
watchAnchorIpCharUuid = "4a0f0017-f8ce-11ee-8001-020304050607" // Write + Notify
// iBeacon manufacturer data
impulseCompanyId = 0xFFFF
iBeaconType = 0x02
iBeaconLength = 0x15
6.2 ScheduleEncoder (lib/utils/schedule_encoder.dart)¶
Handles binary serialization of automations and CRC32 computation.
Functions:
| Function | Returns | Notes |
|---|---|---|
encodeBlob(automations) |
Uint8List |
Serializes automations (no CRC) |
encodeWithCrc(automations) |
Uint8List |
encodeBlob() + 4-byte CRC32 appended |
crc32(data) |
int |
ISO 3309 polynomial 0xEDB88320 |
uuidToBytes(uuid) |
Uint8List |
Parses UUID string → 16 bytes |
generateUuid() |
String |
Cryptographically random UUID v4 |
Blob encoding per automation:
[event count: u16 LE]
for each automation:
[id: 16 bytes]
[referenceDate: i64 LE, Unix seconds]
[startMinutes: u16 LE]
[endMinutes: u16 LE]
[recurrenceType: u8]
[dayOfWeek: u8, 0 if N/A]
[dayOfMonth: u8, 0 if N/A]
[criteria: u8]
[profile: u8]
[anchorProfile: u8, 0xFF if null]
[negate: u8]
[hasAnchor: u8, 0 or 1]
[anchorId: 16 bytes, zero-filled if absent]
[wifiSSID_len: u8]
[wifiSSID: UTF-8 bytes, omitted if len=0]
[beepAnchorCount: u8]
[beepAnchor UUIDs: 16 bytes each]
7. Theme (lib/theme/app_theme.dart)¶
Dark theme with orange accent.
| Token | Hex | Usage |
|---|---|---|
darkGrey |
#2B2B2B |
AppBar, BottomNav background |
lightOrange |
#FF9D5C |
Primary accent, FAB, buttons, highlights |
backgroundGrey |
#1A1A1A |
Scaffold background |
cardGrey |
#3A3A3A |
Card and input field background |
textWhite |
#FFFFFF |
Primary text |
textGrey |
#B0B0B0 |
Secondary text, hints |
Cards: rounded 12px, elevation 2. Text scale: headlineLarge 24px bold, headlineMedium 20px bold, bodyLarge 16px, bodyMedium 14px.
8. BLE Wire Formats¶
Watch GATT (app → watch)¶
| Characteristic | Write Payload | Notify Response |
|---|---|---|
| WiFi Credentials | JSON {"ssid":"...","password":"..."} |
— |
| Settings | [disc_dorm(1B), away_dorm(1B), tz_offset(2B LE signed)] |
0x01 success |
| Schedule Control | [0x01, length(4B LE)] (BEGIN) or [0x02, crc(4B LE)] (END) |
0x01 success |
| Schedule Data | Raw blob chunks (≤20 bytes each) | — |
| Anchor IP Table | [count(1B)] + ([uuid(16B)] + [ipv4(4B)] + [ts(4B LE)])×N |
0x01 success |
Watch GATT (watch → app, notify)¶
| Characteristic | Payload |
|---|---|
| Seen Anchors | [count(1B)] + ([uuid(16B)] + [rssi(1B, actual+128)] + [ts(4B LE)])×N |
| Watch Status | [state(1B)] + [bt(1B)] + [wifi(1B)] + [worn(1B)] + [battery(1B)] + [event_uuid(16B)] + optional unreachable anchor list |
Anchor GATT (app → anchor)¶
| Characteristic | Write Payload | Response |
|---|---|---|
| Identify | Any (ignored) | — (no response) |
| WiFi Credentials | JSON {"ssid":"...","password":"..."} |
0x01 success / 0x00 failure |
| Settings | [max_beep_minutes(2B LE)] |
0x01 |
| Schedule Control | Same 3-phase protocol as watch | 0x01 / 0x00 |
| Toggle | 0x00 (close) or 0x01 (open) |
0x01 success / 0x02 rejected |
Anchor iBeacon Advertisement¶
Manufacturer data (company 0xFFFF):
[type: 0x02]
[length: 0x15]
[uuid: 16 bytes]
[major: 2 bytes BE = 0x4A0F]
[minor: 2 bytes BE = 0x0000]
[txPower: 1 byte]
9. Local Storage¶
| SharedPreferences Key | Type | Content |
|---|---|---|
"paired_devices" |
JSON string | Array of BluetoothDeviceModel (watches + anchors) |
"automations_v2" |
JSON string | Array of Automation |
"setting_disc_dorm" |
bool | Enforce when disconnected from phone |
"setting_away_dorm" |
bool | Enforce when on WiFi only |
"setting_tz" |
int | Timezone offset in minutes (−720 to +840) |
10. Device Settings & Anchor Servo Control¶
This section documents the per-device settings (rename, anchor toggle) accessible from the Devices screen.
10.1 Overview of Changes¶
| Area | Change |
|---|---|
lib/utils/ble_constants.dart |
Add anchorToggleCharUuid |
lib/services/anchor_service.dart |
New singleton service for anchor BLE control operations |
lib/screens/devices_screen.dart |
Add settings gear icon to each device card |
lib/widgets/device_settings_modal.dart |
New modal: display name editor + anchor servo control |
10.2 New BLE Constant¶
Add to lib/utils/ble_constants.dart:
This UUID matches ANCHOR_TOGGLE_CHAR_UUID defined in the firmware spec (Section 4.4).
10.3 New Service: AnchorService (lib/services/anchor_service.dart)¶
Singleton. Handles direct BLE connections to anchor devices for control operations (separate from the passive iBeacon scanning managed by BluetoothService).
Enum:
enum AnchorToggleResult {
success, // Firmware responded 0x01
rejected, // Firmware responded 0x02 (enforcement active)
connectionError, // Could not connect or discover service/characteristic
}
Method: sendToggle(bleRemoteId, value)
sendToggle(bleRemoteId: String, value: int) -> Future<AnchorToggleResult>
value: 0 = close, 1 = open
1. Obtain a BluetoothDevice from flutter_blue_plus using bleRemoteId.
2. Connect to the device (timeout: 10s).
On failure: return AnchorToggleResult.connectionError.
3. Discover services.
On failure or if anchorServiceUuid not found: disconnect, return connectionError.
4. Find the anchorToggleCharUuid characteristic.
If not found: disconnect, return connectionError.
5. Write [value] (1 byte) to the characteristic (write with response).
Await the response byte:
0x01 → success
0x02 → rejected
Other or timeout → connectionError
6. Disconnect.
7. Return the appropriate AnchorToggleResult.
10.4 Updated: Devices Screen (lib/screens/devices_screen.dart)¶
Change: Add a settings gear IconButton to each device card (both watch cards and anchor cards).
Placement: In the card's trailing area or action row, alongside the existing "Connect"/"Beep" buttons.
On tap:
After the modal closes, call setState() so that any name change is reflected immediately in the card.
10.5 New Widget: DeviceSettingsModal (lib/widgets/device_settings_modal.dart)¶
A ModalBottomSheet for per-device settings. Displayed when the user taps the gear icon on any device card.
Props:
State:
String _nameInput; // Initialized from device.name
bool _isBusy; // True while a BLE operation is in progress
String? _statusMsg; // Feedback message to display
Layout (in order):
-
Header: Title "Device Settings", styled to match app theme.
-
Display Name section: - Label: "Display Name" -
TextFieldpre-filled withdevice.name. - "Save" button (enabled when_nameInputdiffers fromdevice.nameand is non-empty). - On save:- Create updated
BluetoothDeviceModelwithname = _nameInput. - Call
BluetoothService().addOrUpdateDevice(updatedModel). - Show brief confirmation message.
- Create updated
-
Servo Control section (anchors only — rendered if
device.deviceType == DeviceType.anchor): - Section header: "Servo Lock" - Descriptive text: "Open the anchor strap lock to reposition the anchor. The lock cannot be opened during an active enforcement event." - Guard: ifdevice.bleRemoteId == null, show a warning instead: "This anchor has not been directly scanned. Tap 'Scan' on the Devices tab to discover it before using servo control." Do not render the control buttons. - IfbleRemoteIdis available, render two buttons side by side:- "Open" button: outlined, sends
value = 1. - "Close" button: filled, sends
value = 0. - Both buttons disabled while
_isBusy == true. - On either button tap:
_sendToggle(value): setState: _isBusy = true, _statusMsg = null result = await AnchorService().sendToggle(device.bleRemoteId!, value) setState: _isBusy = false switch result: success: _statusMsg = value == 1 ? "Anchor opened." : "Anchor closed." rejected: _statusMsg = "Cannot open: anchor is in an active enforcement event." connectionError: _statusMsg = "Could not connect to anchor. Make sure it is nearby and powered on." - "Open" button: outlined, sends
- Display
_statusMsgbelow the buttons (using the app's secondary text style). - A
CircularProgressIndicatorreplaces button content while_isBusy == true.
10.6 Data Flow for Anchor Toggle¶
User taps "Open" on DeviceSettingsModal
→ DeviceSettingsModal._sendToggle(1)
→ AnchorService().sendToggle(bleRemoteId, 1)
→ flutter_blue_plus.connect(bleRemoteId, timeout: 10s)
→ discoverServices()
→ find anchorServiceUuid → find anchorToggleCharUuid
→ write([0x01]) with response
← firmware responds:
0x01 → servo opens (160°), firmware detaches servo
0x02 → enforcement active, servo unchanged
→ disconnect()
← returns AnchorToggleResult
← DeviceSettingsModal updates _statusMsg
10.7 Behavioral Notes¶
- Close is always allowed: The "Close" button should never be disabled on enforcement grounds (the firmware always accepts a close command). The rejection message is written to only apply when
value == 1(open). - bleRemoteId requirement: Anchors discovered only through the watch's SeenAnchors stream will not have a
bleRemoteId. The settings modal must guard against this and prompt the user to scan first. - Name persistence is immediate: Saving a display name writes through to
BluetoothServiceand SharedPreferences before the modal closes. The Devices screen must refresh its device list after the modal closes viasetState(). - No persistent servo state in app: The app does not track or cache whether the servo is open or closed. The Toggle characteristic is readable per the firmware spec, but reading it is deferred to a future spec version. For now, the UI is stateless with respect to servo position.
- One connection at a time:
AnchorService.sendToggleconnects, operates, and disconnects in a single call. It does not maintain a persistent connection to the anchor.
End of specification v0.2