Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ drawer-bracelet problem can use it, or go dig through the code themselves.
| <img src="screenshots/recap.png" width="230"><br>**Recap** | <img src="screenshots/profile.png" width="230"><br>**Profile** | |

iOS also gets a home-screen widget, a lock-screen/Dynamic Island Live Activity, and a
couple of Siri shortcuts.
couple of Siri shortcuts, including [Sync Data](guides/IOS_SHORTCUTS.md) for
on-demand or scheduled band synchronization without opening the interface.

| | | |
|:--:|:--:|:--:|
Expand Down
36 changes: 36 additions & 0 deletions guides/IOS_SHORTCUTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Syncing with iOS Shortcuts

## Sync Data

On iOS 16 and later, add **Edge → Sync Data** to a shortcut. Pair your band in Edge and allow Bluetooth first. The action syncs the band without opening the app.

**Ignore Connectivity Errors** (off by default): when Bluetooth is off or the band is out of range, the action returns a `Skipped:` result instead of an error. Pairing, permission and other errors are still reported.

For a personal automation, pick a Time of Day trigger and **Run Immediately**. Several triggers a day give several chances to sync; none of them guarantee the band is in range.

The action has about 25 seconds, including app startup. A large backlog can need more than one run, or open Edge to catch up.

### Results

- **Band data synchronized:** the transfer finished and light processing was started.
- **Sync is incomplete:** it ran out of time or the transfer stopped early. Saved data is kept; run it again.
- **A sync request is already active:** another sync owns the band, so nothing new was started.
- **Skipped:** Bluetooth was unavailable or the band was unreachable, with the option on.

## Open Edge and Sync

Brings the app forward and runs the same sync. Use it interactively, not from locked-phone automations.

## Native tests

```sh
flutter build ios --simulator --debug
cd ios
xcodebuild -workspace Runner.xcworkspace -scheme Runner \
-destination "id=$SIMULATOR_ID" -only-testing:RunnerTests \
BUILD_DIR="$PWD/../build/ios" CODE_SIGNING_ALLOWED=NO \
APP_BUNDLE_IDENTIFIER=com.example.openstrapEdge.shortcuttests \
APP_GROUP_IDENTIFIER=group.com.example.openstrap.shortcuttests test
```

The `ShortcutIntents` scheme runs the system-invocation tests. It needs Xcode 27, an iOS 27 simulator, and a development-signed build.
61 changes: 53 additions & 8 deletions ios/OpenStrapIntents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,53 @@ struct SleepIntent: AppIntent {

// MARK: - Action intents (these actually DO something, not just answer)

/// "Start breathing" — unlike the query intents above, this needs the live
/// Flutter engine + BLE stack (a guided session reads live RR from the band),
/// so it must open the app rather than answer standalone. Writes the target
/// route into the App Group; the Dart side picks it up via
/// WidgetService.consumePendingRoute() on launch AND on every foreground
/// resume (see AppState.checkPendingSiriRoute — openAppWhenRun doesn't
/// guarantee a fresh launch, it may just foreground an already-running
/// process, so both call sites matter).
@available(iOS 16.0, *)
struct SyncDataIntent: AppIntent {
static var title: LocalizedStringResource = "Sync Data"
static var description = IntentDescription(
"Sync your paired band into Edge without opening the app. Large backlogs may need more than one run.")
static var openAppWhenRun = false
static var authenticationPolicy: IntentAuthenticationPolicy = .alwaysAllowed

@Parameter(title: "Ignore Connectivity Errors", description:
"Skip quietly when Bluetooth is unavailable or the band cannot be reached. Pairing, permission, and other errors are still reported.", default: false)
var ignoreConnectivityErrors: Bool

static var parameterSummary: some ParameterSummary {
Summary("Sync band data") { \.$ignoreConnectivityErrors }
}

@MainActor
func perform() async throws -> some IntentResult & ReturnsValue<String> {
.result(value: try await syncMessage(using: .shared))
}

@MainActor
func syncMessage(using bridge: ShortcutSyncBridge) async throws -> String {
do {
return try await bridge.sync().message
} catch let error as ShortcutSyncFailure where ignoreConnectivityErrors && error.canIgnore {
return "Skipped: \(error.localizedDescription)"
}
}
}

@available(iOS 16.0, *)
struct OpenEdgeAndSyncIntent: AppIntent {
static var title: LocalizedStringResource = "Open Edge and Sync"
static var description = IntentDescription(
"Open Edge and sync your paired band. Use this interactive action for catch-up that needs the app in front.")
static var openAppWhenRun = true

@MainActor
func perform() async throws -> some IntentResult & ReturnsValue<String> {
let reply = try await ShortcutSyncBridge.shared.sync()
return .result(value: reply.message)
}
}

/// Flutter consumes the breathing route on launch and resume because opening
/// the app does not guarantee a fresh process.
@available(iOS 16.0, *)
struct StartBreathingIntent: AppIntent {
static var title: LocalizedStringResource = "Start Breathing Session"
Expand Down Expand Up @@ -176,6 +215,12 @@ struct EnableTomorrowAlarmIntent: AppIntent {
@available(iOS 16.0, *)
struct OpenStrapShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: SyncDataIntent(),
phrases: ["Sync data in \(.applicationName)", "Sync my band with \(.applicationName)"],
shortTitle: "Sync Data",
systemImageName: "arrow.triangle.2.circlepath")

AppShortcut(
intent: RecoveryIntent(),
phrases: [
Expand Down
Loading