From 3e505c236e51950acfa9ee9798011be4abcd37d7 Mon Sep 17 00:00:00 2001 From: lkasso Date: Sat, 13 Jun 2026 21:55:45 -0700 Subject: [PATCH] Remove AI-assistant docs from repo; expand .gitignore Untrack CLAUDE.md and AGENTS.md (kept locally, now gitignored) and add .claude/.cursor AI tooling, common Xcode/SPM build artifacts, and secret/ code-signing patterns to .gitignore ahead of going public. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 35 ++++- AGENTS.md | 98 ------------ CLAUDE.md | 432 ----------------------------------------------------- 3 files changed, 34 insertions(+), 531 deletions(-) delete mode 100644 AGENTS.md delete mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index 671d4f4..5633e45 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,40 @@ +# macOS .DS_Store + +# Xcode / build artifacts /.build /Packages -xcuserdata/ +build/ DerivedData/ +xcuserdata/ +*.moved-aside +*.hmap +*.dSYM +*.dSYM.zip +*.ipa +timeline.xctimeline + +# Swift Package Manager .swiftpm/ + +# Secrets & code-signing (never commit these) .netrc +.env +*.env +Secrets.swift +*.pem +*.p12 +*.p8 +*.cer +*.mobileprovision + +# AI assistant tooling (kept local, not published) +CLAUDE.md +AGENTS.md +.claude/ +.cursor/ + +# Editors / IDEs +.vscode/ +.idea/ +*.swp diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index f569ba0..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,98 +0,0 @@ -# Agent guide for Swift and SwiftUI - -This repository contains an Xcode project written with Swift and SwiftUI. Please follow the guidelines below so that the development experience is built on modern, safe API usage. - - -## Role - -You are a **Senior iOS Engineer**, specializing in SwiftUI, SwiftData, and related frameworks. Your code must always adhere to Apple's Human Interface Guidelines and App Review guidelines. - - -## Core instructions - -- Target iOS 26.0 or later. (Yes, it definitely exists.) -- Swift 6.2 or later, using modern Swift concurrency. Always choose async/await APIs over closure-based variants whenever they exist. -- SwiftUI backed up by `@Observable` classes for shared data. -- Do not introduce third-party frameworks without asking first. -- Avoid UIKit unless requested. - - -## Swift instructions - -- `@Observable` classes must be marked `@MainActor` unless the project has Main Actor default actor isolation. Flag any `@Observable` class missing this annotation. -- All shared data should use `@Observable` classes with `@State` (for ownership) and `@Bindable` / `@Environment` (for passing). -- Strongly prefer not to use `ObservableObject`, `@Published`, `@StateObject`, `@ObservedObject`, or `@EnvironmentObject` unless they are unavoidable, or if they exist in legacy/integration contexts when changing architecture would be complicated. -- Assume strict Swift concurrency rules are being applied. -- Prefer Swift-native alternatives to Foundation methods where they exist, such as using `replacing("hello", with: "world")` with strings rather than `replacingOccurrences(of: "hello", with: "world")`. -- Prefer modern Foundation API, for example `URL.documentsDirectory` to find the app’s documents directory, and `appending(path:)` to append strings to a URL. -- Never use C-style number formatting such as `Text(String(format: "%.2f", abs(myNumber)))`; always use `Text(abs(change), format: .number.precision(.fractionLength(2)))` instead. -- Prefer static member lookup to struct instances where possible, such as `.circle` rather than `Circle()`, and `.borderedProminent` rather than `BorderedProminentButtonStyle()`. -- Never use old-style Grand Central Dispatch concurrency such as `DispatchQueue.main.async()`. If behavior like this is needed, always use modern Swift concurrency. -- Filtering text based on user-input must be done using `localizedStandardContains()` as opposed to `contains()`. -- Avoid force unwraps and force `try` unless it is unrecoverable. -- Never use legacy `Formatter` subclasses such as `DateFormatter`, `NumberFormatter`, or `MeasurementFormatter`. Always use the modern `FormatStyle` API instead. For example, to format a date, use `myDate.formatted(date: .abbreviated, time: .shortened)`. To parse a date from a string, use `Date(inputString, strategy: .iso8601)`. For numbers, use `myNumber.formatted(.number)` or custom format styles. - -## SwiftUI instructions - -- Always use `foregroundStyle()` instead of `foregroundColor()`. -- Always use `clipShape(.rect(cornerRadius:))` instead of `cornerRadius()`. -- Always use the `Tab` API instead of `tabItem()`. -- Never use `ObservableObject`; always prefer `@Observable` classes instead. -- Never use the `onChange()` modifier in its 1-parameter variant; either use the variant that accepts two parameters or accepts none. -- Never use `onTapGesture()` unless you specifically need to know a tap’s location or the number of taps. All other usages should use `Button`. -- Never use `Task.sleep(nanoseconds:)`; always use `Task.sleep(for:)` instead. -- Never use `UIScreen.main.bounds` to read the size of the available space. -- Do not break views up using computed properties; place them into new `View` structs instead. -- Do not force specific font sizes; prefer using Dynamic Type instead. -- Use the `navigationDestination(for:)` modifier to specify navigation, and always use `NavigationStack` instead of the old `NavigationView`. -- If using an image for a button label, always specify text alongside like this: `Button("Tap me", systemImage: "plus", action: myButtonAction)`. -- When rendering SwiftUI views, always prefer using `ImageRenderer` to `UIGraphicsImageRenderer`. -- Don’t apply the `fontWeight()` modifier unless there is good reason. If you want to make some text bold, always use `bold()` instead of `fontWeight(.bold)`. -- Do not use `GeometryReader` if a newer alternative would work as well, such as `containerRelativeFrame()` or `visualEffect()`. -- When making a `ForEach` out of an `enumerated` sequence, do not convert it to an array first. So, prefer `ForEach(x.enumerated(), id: \.element.id)` instead of `ForEach(Array(x.enumerated()), id: \.element.id)`. -- When hiding scroll view indicators, use the `.scrollIndicators(.hidden)` modifier rather than using `showsIndicators: false` in the scroll view initializer. -- Use the newest ScrollView APIs for item scrolling and positioning (e.g. `ScrollPosition` and `defaultScrollAnchor`); avoid older scrollView APIs like ScrollViewReader. -- Place view logic into view models or similar, so it can be tested. -- Avoid `AnyView` unless it is absolutely required. -- Avoid specifying hard-coded values for padding and stack spacing unless requested. -- Avoid using UIKit colors in SwiftUI code. - - -## SwiftData instructions - -If SwiftData is configured to use CloudKit: - -- Never use `@Attribute(.unique)`. -- Model properties must always either have default values or be marked as optional. -- All relationships must be marked optional. - - -## Project structure - -- Use a consistent project structure, with folder layout determined by app features. -- Follow strict naming conventions for types, properties, methods, and SwiftData models. -- Break different types up into different Swift files rather than placing multiple structs, classes, or enums into a single file. -- Write unit tests for core application logic. -- Only write UI tests if unit tests are not possible. -- Add code comments and documentation comments as needed. -- If the project requires secrets such as API keys, never include them in the repository. -- If the project uses Localizable.xcstrings, prefer to add user-facing strings using symbol keys (e.g. helloWorld) in the string catalog with `extractionState` set to "manual", accessing them via generated symbols such as `Text(.helloWorld)`. Offer to translate new keys into all languages supported by the project. - - -## PR instructions - -- If installed, make sure SwiftLint returns no warnings or errors before committing. - - -## Xcode MCP - -If the Xcode MCP is configured, prefer its tools over generic alternatives when working on this project: - -- `DocumentationSearch` — verify API availability and correct usage before writing code -- `BuildProject` — build the project after making changes to confirm compilation succeeds -- `GetBuildLog` — inspect build errors and warnings -- `RenderPreview` — visually verify SwiftUI views using Xcode Previews -- `XcodeListNavigatorIssues` — check for issues visible in the Xcode Issue Navigator -- `ExecuteSnippet` — test a code snippet in the context of a source file -- `XcodeRead`, `XcodeWrite`, `XcodeUpdate` — prefer these over generic file tools when working with Xcode project files - diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index d349373..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,432 +0,0 @@ -# MetaWear Swift SDK - -A clean-room Swift implementation of the MetaWear BLE protocol. -This file captures all architecture decisions made so far. - ---- - -## Goals - -- Native iOS app + reusable Swift SDK -- No dependency on MbientLab's existing SDKs -- Protocol spec sourced from: `/Users/kasso/Documents/MetaWear-API/docs/api-specification.md` (the former `protocol-reference.md` was merged into it 2026-06; its math-op table was wrong — firmware ops are Add=1…Subtract=9, Abs=10, Constant=11) - ---- - -## Platform - -- **iOS only** -- **iOS 17+** (for SwiftData) -- **Swift 6** strict concurrency (`-strict-concurrency=complete`) -- **SwiftUI** for UI (no UIKit, no cross-platform frameworks) - ---- - -## Key Technology Decisions - -| Concern | Choice | Rejected | Reason | -|---------|---|---|---| -| UI | SwiftUI + `@Observable` | UIKit, Flutter | Native, modern, less boilerplate | -| Observation | `@Observable` | `ObservableObject` + `@Published` | Swift 5.9+, less boilerplate | -| Async sequences | `AsyncThrowingStream` | Combine, `AsyncStream` | Errors propagate (BLE drops), no Combine dependency | -| Thread safety | `actor` | `DispatchQueue` + locks | Compiler-enforced, Swift 6 native | -| BLE wrapper | Custom `CoreBluetoothTransport` | AsyncBluetooth | AsyncBluetooth uses Combine for notifications | -| Persistence | SwiftData | CoreData, SQLite | iOS 17+, Swift-native | -| Live graphing | Swift Charts | Third-party | Native iOS 16+, integrates with SwiftUI | - ---- - -## Architecture - -``` -┌─────────────────────────────────────┐ -│ SwiftUI Views │ -│ (@Observable ViewModels) │ -├─────────────────────────────────────┤ -│ MetaWearClient (actor) │ -│ DeviceState machine │ -│ Module access (accel, gyro, etc.) │ -├─────────────────────────────────────┤ -│ Protocol Layer │ -│ Command builders │ -│ Response parsers │ -│ Module registry │ -├─────────────────────────────────────┤ -│ BLETransport (protocol) │ -├───────────────┬─────────────────────┤ -│ CoreBluetooth │ MockBLETransport │ -│ Transport │ (for tests) │ -└───────────────┴─────────────────────┘ -``` - ---- - -## BLETransport Protocol - -The entire CoreBluetooth surface the protocol layer needs. -Swap implementations for testing without touching anything above. - -```swift -protocol BLETransport: Actor { - func connect(to identifier: UUID) async throws - func disconnect() async throws - func write(_ data: Data, to characteristic: CBUUID, type: CBCharacteristicWriteType) async throws - func read(from characteristic: CBUUID) async throws -> Data - func notify(from characteristic: CBUUID) -> AsyncThrowingStream - func scan(for services: [CBUUID]?) -> AsyncStream -} -``` - ---- - -## CoreBluetoothTransport - -Custom wrapper around CoreBluetooth. No third-party BLE libraries. - -### Bridging strategy - -CoreBluetooth is delegate/callback based. Bridge to async/await: - -| CoreBluetooth callback | Bridged to | -|---|---| -| `didConnect` | `CheckedContinuation` | -| `didFailToConnect` | same continuation, throw | -| `didDisconnect` | terminate notification stream with error | -| `didUpdateValueFor` (read response) | `CheckedContinuation` | -| `didUpdateValueFor` (notification) | `AsyncThrowingStream.Continuation.yield()` | -| `didWriteValueFor` | `CheckedContinuation` | -| `didDiscover peripheral` | `AsyncStream.Continuation.yield()` | - -### Swift 6 pattern - -`CBCentralManager` and `CBPeripheral` are not `Sendable`. -Delegate methods are `nonisolated`, hop back into the actor via `Task`: - -```swift -actor CoreBluetoothTransport: NSObject, BLETransport { - private var connectContinuation: CheckedContinuation? - private var notificationContinuation: AsyncThrowingStream.Continuation? - - nonisolated func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { - Task { await self.handleConnect() } - } -} -``` - ---- - -## MetaWear UUIDs - -```swift -// Service -let metaWearService = CBUUID(string: "326A9000-85CB-9195-D9DD-464CFBBAE75A") - -// Characteristics -let commandChar = CBUUID(string: "326A9001-85CB-9195-D9DD-464CFBBAE75A") // write -let notifyChar = CBUUID(string: "326A9006-85CB-9195-D9DD-464CFBBAE75A") // notify - -// Standard BLE Device Info (service 0x180A) -let firmwareRevision = CBUUID(string: "2A26") -let modelNumber = CBUUID(string: "2A24") -let hardwareRevision = CBUUID(string: "2A27") -let manufacturerName = CBUUID(string: "2A29") -let serialNumber = CBUUID(string: "2A25") -``` - ---- - -## Packet Format - -Every command and notification: - -``` -Byte 0: module_id -Byte 1: register_id (| 0x80 for READ requests) -Byte 2: data_id (only for signals with an ID) -Bytes 3+: payload (little-endian) -``` - -Macros are the only commands written **with** response. -Everything else uses write-without-response. - -```swift -func buildCommand(_ module: Module, _ register: UInt8, _ payload: UInt8...) -> Data { - Data([module.rawValue, register] + payload) -} - -func buildReadCommand(_ module: Module, _ register: UInt8, _ payload: UInt8...) -> Data { - Data([module.rawValue, register | 0x80] + payload) -} -``` - ---- - -## Device State Machine - -The MetaWear cannot stream and log simultaneously. -State is enforced at the actor level — invalid transitions throw. - -``` -Disconnected → Idle → Streaming - → Logging - → Downloading (only when Idle, after Logging) -``` - -```swift -enum DeviceState: Equatable { - case disconnected - case idle - case streaming(config: StreamConfig) - case logging(config: LogConfig) - case downloading(progress: Double) -} -``` - -State guards in `MetaWearClient`: -```swift -func startStreaming(config: StreamConfig) async throws { - guard case .idle = state else { throw MetaWearError.invalidState } - ... -} -``` - ---- - -## Data Modes - -### Streaming (live) - -- BLE delivers packed data: 3 samples per notification -- Effective max ~100Hz (BLE bottleneck) -- Unpacked in protocol layer → `AsyncThrowingStream` -- Fed into `@Observable` ViewModel → Swift Charts -- No persistence — display only - -``` -MetaWear → BLE notifications (packed ~33/sec) - → unpack 3 samples - → AsyncThrowingStream - → @Observable ViewModel (rolling buffer) - → Swift Charts (live graph) -``` - -### Logging (on-device) - -- Sensor data written to MetaWear flash at up to 800Hz+ -- Separate explicit download step when stopped -- Download delivers progress via callback -- Parsed log entries persisted to SwiftData - -``` -MetaWear flash → BLE download (burst) - → parse log entries (tick → epoch conversion) - → SwiftData - → display / export -``` - -**MMS-specific:** must call `flush_page` before downloading. -**Tick math:** `(48.0 / 32768.0) * 1000.0 = 1.4648 ms/tick` - ---- - -## Module IDs - -```swift -enum Module: UInt8 { - case switch_ = 0x01 - case led = 0x02 - case accelerometer = 0x03 - case temperature = 0x04 - case gpio = 0x05 - case haptic = 0x08 - case dataProcessor = 0x09 - case event = 0x0A - case logging = 0x0B - case timer = 0x0C - case serial = 0x0D - case macro = 0x0F - case settings = 0x11 - case barometer = 0x12 - case gyro = 0x13 - case magnetometer = 0x15 - case sensorFusion = 0x19 - case debug = 0xFE -} -``` - ---- - -## Board Initialization Sequence - -1. Subscribe to notify characteristic (`326A9006`) -2. Read Device Info characteristics (firmware, model, hardware, manufacturer, serial) -3. Discover available modules — send `[module_id, 0x80]` for each known opcode -4. Board responds with `[module_id, 0x80, impl_id, revision]` -5. Build module registry from responses -6. Read logging time reference (`[0x0B, 0x84]`) and set epoch - -Module discovery can be parallelised with `ThrowingTaskGroup` — fire all queries simultaneously rather than sequentially. - ---- - -## Sensor Data Types - -```swift -struct CartesianFloat: Sendable { - let x: Float // axes in physical units (g, dps, µT) - let y: Float - let z: Float -} - -struct Quaternion: Sendable { - let w: Float - let x: Float - let y: Float - let z: Float -} - -struct EulerAngles: Sendable { - let heading: Float // degrees - let pitch: Float - let roll: Float - let yaw: Float -} - -struct SensorSample: Sendable { - let epoch: Date - let value: SensorValue -} - -enum SensorValue: Sendable { - case acceleration(CartesianFloat) // g - case rotation(CartesianFloat) // dps - case magneticField(CartesianFloat) // µT - case quaternion(Quaternion) - case eulerAngles(EulerAngles) - case pressure(Float) // Pa - case altitude(Float) // m - case temperature(Float) // °C -} -``` - -All types are `Sendable` — safe to pass across actor boundaries. - ---- - -## Scale Factors (for parsing raw int16 responses) - -### Accelerometer - -| Range | BMI160 byte | BMI270 byte | Scale (LSB/g) | -|---|---|---|---| -| ±2g | 0x03 | 0x00 | 16384 | -| ±4g | 0x05 | 0x01 | 8192 | -| ±8g | 0x08 | 0x02 | 4096 | -| ±16g | 0x0C | 0x03 | 2048 | - -### Gyroscope - -| Range | Scale (LSB/dps) | -|---|---| -| ±2000 dps | 16.4 | -| ±1000 dps | 32.8 | -| ±500 dps | 65.6 | -| ±250 dps | 131.2 | -| ±125 dps | 262.4 | - -### Other - -| Sensor | Scale | -|---|---| -| Magnetometer | 16.0 LSB/µT | -| Pressure | raw ÷ 256 → Pa | -| Altitude | raw ÷ 256 → m | -| Temperature | raw ÷ 8 → °C | -| Humidity | raw ÷ 1024 → % | -| Quaternion / Euler | raw ÷ 65536 (Q16.16) | - ---- - -## Reference Implementation — MetaWear-Swift-Combine-SDK - -Source: `https://github.com/mbientlab/MetaWear-Swift-Combine-SDK` - -A fully working Combine-based Swift SDK. Our rewrite replaces Combine with Swift 6 concurrency -but preserves the protocol taxonomy and data model. - -### What to keep (port directly) - -| File / Concept | Why | -|---|---| -| `MWActions.swift` — `MWLoggable`, `MWStreamable`, `MWPollable`, `MWReadable`, `MWCommand` | Clean orthogonal protocol taxonomy, no Combine dependency | -| All `SensorModules/` structs | Sensor config structs, scale factors, signal lookups — no Combine | -| `MWData.swift` + `copy()` pattern | C++ data pointer is only valid during callback — `copy()` on line 1 is essential | -| `MWDataConvertible` + `asColumns` | CSV/table export, zero per-sensor code at consumer level | -| `MWNamedSignal` + `DownloadUtilities` | Typed logger identifier registry, bundles stop/decode/columns per signal | -| `MWFrequency` | Bidirectional Hz↔ms value type | -| `MWError` | 4-case error enum, fits `throws` directly | -| `MWDataTable` | CSV table from streamed/downloaded data | -| `Bridging.swift` — `bridge(obj:)` / `bridge(ptr:)` | Still needed to pass Swift objects into C++ callbacks | -| `MWModules.swift` | Module detection/lookup logic | -| `cbindings.swift` | C++ integer constants re-exported as Swift | -| Sensor conflict detection | Accel/gyro/mag cannot run while SensorFusion is active — hardware constraint | -| Write queue + `peripheralIsReady` drain | BLE write throttling is real; keep as actor-isolated queue | - -### What to replace entirely - -| Old (Combine) | New (Swift Concurrency) | -|---|---| -| `MetaWear.swift` (class + bleQueue) | `MetaWearDevice` (actor) | -| `MetaWearScanner.swift` (class + bleQueue) | `MetaWearScanner` (actor) | -| `Combine/Stream.swift` | `func stream(_ s: S) -> AsyncThrowingStream` | -| `Combine/Log.swift` | `func log(_ l: L) async throws` | -| `Combine/Download.swift` | `func downloadLogs() -> AsyncThrowingStream, Error>` | -| `Combine/Read.swift` | `func read(_ r: R) async throws -> Timestamped` | -| `Combine/Command.swift` | `func command(_ c: C) async throws` | -| `Combine/Timer.swift` | `func createTimer(...) async throws -> MWTimer` | -| `Combine/RecordEvents.swift` | `func recordEvents(for:_:) async throws` | -| `Helpers/Combine+Internal.swift` | Removed | -| `Helpers/Combine+Public.swift` | Removed | -| `PassthroughSubject` one-shot | `withCheckedThrowingContinuation` | -| `PassthroughSubject` multi-value | `AsyncThrowingStream { continuation in }` | -| `CurrentValueSubject` accumulator | `var accumulated: [T] = []` inside actor | -| `bleQueue` + `handleOutputOnBleQueue` | `actor` (serialises all access automatically) | -| `AnyCancellable` lifetime management | `Task` + `withTaskCancellationHandler` | -| `flatMap` download pipeline | Straightforward `async` function with a loop | - -### Key lessons from their pain points - -- **`AnyCancellable` retention** — silent failure when forgotten. `Task` lifetime is explicit. -- **`PassthroughSubject` as one-shot** — no guarantee of single send. `CheckedContinuation` enforces it. -- **`flatMap` chains** — opaque stack traces. `async/await` is linear and debuggable. -- **No backpressure** on `PassthroughSubject`. `AsyncThrowingStream` supports it. -- **`eraseToAnyPublisher()` everywhere** — ~150 calls, heap-boxes every boundary. Async sequences compose without this. -- **Macro recording type break** — changing `Output` mid-chain is awkward in Combine. `async throws -> MacroID` is clean. - -### Their C++ bridge pattern (we keep this) - -```swift -// Pass a Swift object into a C++ callback as UnsafeMutableRawPointer -let continuation = /* CheckedContinuation or AsyncThrowingStream.Continuation */ -mbl_mw_some_async_function(board, bridgeRetained(obj: continuation)) { context, result in - let c = bridge(ptr: context!) // recover the Swift object - c.resume(returning: result) // or c.yield(value) -} -``` - -`MblMwData*` is only valid during the C++ callback — always call `data.pointee.copy()` immediately. - ---- - -## Open Questions - -- What sensors does the user configure in the app, or is sensor config user-facing? -- Export format for logged data (CSV, JSON, custom binary)? -- Single device or multi-device support? -- Does the app need a scan/discovery screen or is the MAC address known ahead of time? - ---- - -## Reference - -Full byte-level protocol: `/Users/kasso/Documents/MetaWear-API/docs/api-specification.md`