Skip to content
Merged
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
25 changes: 25 additions & 0 deletions .github/workflows/swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: SwiftLint

on:
pull_request:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: read

jobs:
swiftlint:
runs-on: macos-15

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Install SwiftLint
run: brew install swiftlint

- name: Run SwiftLint
run: swiftlint --strict
34 changes: 34 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
excluded:
- .claude
- .build
- .swiftpm
- .git
- DerivedData
- "**/xcuserdata"
- "**/SourcePackages"

# Start with low-noise rules that are useful immediately on the current codebase.
# Broaden this list gradually as existing style drift is cleaned up.
only_rules:
- blanket_disable_command
- closing_brace
- comment_spacing
- compiler_protocol_init
- duplicate_imports
- duplicated_key_in_dictionary_literal
- dynamic_inline
- empty_enum_arguments
- empty_parameters
- empty_parentheses_with_trailing_closure
- invalid_swiftlint_command
- leading_whitespace
- no_space_in_method_call
- orphaned_doc_comment
- protocol_property_accessors_order
- redundant_discardable_let
- redundant_objc_attribute
- redundant_void_return
- trailing_newline
- trailing_semicolon

reporter: xcode
6 changes: 6 additions & 0 deletions Apps/MetaWear/MetaWear/App/AppStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import SwiftData
import MetaWear
import MetaWearPersistence

/// Main app coordinator shared by the SwiftUI scene.
///
/// Owns long-lived services (`MetaWearScanner`, SwiftData containers, and the
/// persistence actor), tracks the active device, and handles cross-feature
/// flows that do not belong to a single screen: remembered devices, orphan log
/// recovery, connect/disconnect, and unexpected-disconnect handling.
@Observable
@MainActor
final class AppStore {
Expand Down
6 changes: 3 additions & 3 deletions Apps/MetaWear/MetaWear/App/RingBuffer.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import Foundation

// `nonisolated`: this is a pure value type with no actor affinity — the app
// target's default MainActor isolation would otherwise make every member
// main-actor-only, which blocks nonisolated tests and any future off-main use.
/// Fixed-capacity FIFO buffer backed by a circular array.
///
/// `append` is O(1) — once full, the oldest element is overwritten in place
/// via a wrapping head index. (An earlier version used `Array.removeFirst()`,
/// which shifts every remaining element and made each append O(capacity);
/// at 100 Hz × several channels that put hundreds of thousands of element
/// moves per second on the main actor.)
// `nonisolated`: this is a pure value type with no actor affinity — the app
// target's default MainActor isolation would otherwise make every member
// main-actor-only, which blocks nonisolated tests and any future off-main use.
nonisolated struct RingBuffer<Element> {
private var storage: [Element] = []
/// Index of the oldest element once the buffer has wrapped.
Expand Down
6 changes: 6 additions & 0 deletions Apps/MetaWear/MetaWear/App/SensorSelection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import Foundation
import SwiftUI
import MetaWear

/// Stable identity for each sensor family or fusion output the demo app can
/// stream, log, display, and persist.
enum SensorKey: Hashable, Sendable {
case accelerometer
case gyroscope
Expand Down Expand Up @@ -75,6 +77,8 @@ enum SensorKey: Hashable, Sendable {
}
}

/// Sensor-fusion payloads exposed in the app. Each case maps to a concrete
/// `MWLoggable` / `MWStreamable` type in the SDK.
enum SensorFusionOutput: String, CaseIterable, Sendable, Identifiable {
case quaternion
case eulerAngles
Expand All @@ -99,6 +103,7 @@ enum SensorFusionOutput: String, CaseIterable, Sendable, Identifiable {
}
}

/// Firmware sensor-fusion algorithm modes surfaced in the UI.
enum SensorFusionMode: String, CaseIterable, Sendable, Identifiable {
case ndof = "NDoF"
case imuPlus = "IMU Plus"
Expand All @@ -108,6 +113,7 @@ enum SensorFusionMode: String, CaseIterable, Sendable, Identifiable {
var id: String { rawValue }
}

/// User-selected sensor configuration for one live or logged channel.
struct SensorSelection: Identifiable, Sendable, Hashable {
let id: SensorKey
var hz: Double
Expand Down
5 changes: 5 additions & 0 deletions Apps/MetaWear/MetaWear/ViewModels/Channel.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import Foundation
import Observation

/// One charted sensor stream in a live session.
///
/// Separates high-frequency sample ingestion from observed UI state: raw
/// samples accumulate in an ignored ring buffer, then the stream view model
/// snapshots into observed fields at a fixed UI cadence.
@Observable
@MainActor
final class Channel: Identifiable {
Expand Down
4 changes: 4 additions & 0 deletions Apps/MetaWear/MetaWear/ViewModels/ControlsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import Foundation
import Observation
import MetaWear

/// Presentation model for quick device controls.
///
/// Keeps UI-editable command settings for LED and haptic actions, plus the
/// latest one-shot read values for temperature, pressure, and ambient light.
@Observable
@MainActor
final class ControlsViewModel {
Expand Down
5 changes: 5 additions & 0 deletions Apps/MetaWear/MetaWear/ViewModels/DeviceViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import Foundation
import Observation
import MetaWear

/// Presentation model for the connected-device overview.
///
/// Mirrors immutable device facts, live battery state, and module discovery
/// into main-actor state that SwiftUI can render without repeatedly crossing
/// the `MetaWearDevice` actor boundary.
@Observable
@MainActor
final class DeviceViewModel {
Expand Down
5 changes: 5 additions & 0 deletions Apps/MetaWear/MetaWear/ViewModels/DownloadViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import SwiftData
import MetaWear
import MetaWearPersistence

/// Coordinates download and persistence for stopped on-device logging sessions.
///
/// Drains the board once into raw entries, decodes those entries per pending
/// `LogSessionRecord`, saves typed snapshots through `MWPersistenceStore`, and
/// updates download progress for the UI.
@Observable
@MainActor
final class DownloadViewModel {
Expand Down
6 changes: 6 additions & 0 deletions Apps/MetaWear/MetaWear/ViewModels/LogSessionViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import Observation
import SwiftData
import MetaWear

/// Drives on-device logging setup and teardown.
///
/// Converts selected UI sensors into SDK loggers, persists pending
/// `LogSessionRecord` rows so interrupted sessions can be recovered, and
/// keeps elapsed-time state for the logging UI while the board records to
/// flash independently of the app.
@Observable
@MainActor
final class LogSessionViewModel {
Expand Down
4 changes: 4 additions & 0 deletions Apps/MetaWear/MetaWear/ViewModels/ScannerViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import Foundation
import Observation
import MetaWear

/// Presentation model for scan results.
///
/// Wraps `MetaWearScanner` with sorted device lists and optional RSSI polling
/// for connected devices while keeping CoreBluetooth state owned by the SDK.
@Observable
@MainActor
final class ScannerViewModel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import Foundation
import Observation
import MetaWearPersistence

/// Minimal view model for the session-history screen.
///
/// Listing is driven directly by SwiftData queries in the view; this type owns
/// actions that need async persistence calls and error presentation.
@Observable
@MainActor
final class SessionHistoryViewModel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import Observation
import MetaWear
import MetaWearPersistence

/// Drives the live-streaming screen.
///
/// Owns the active sensor streams, per-sensor chart channels, pause/resume
/// lifecycle, and optional archive-to-history flow for the in-memory ring
/// buffers. Each stream consumes `MetaWearDevice.startStream(_:)` on a
/// background task and throttles UI updates through `Channel.displayBuffer`.
@Observable
@MainActor
final class StreamSessionViewModel {
Expand Down
93 changes: 68 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
# MetaWear Swift SDK

A clean-room Swift 6 implementation of the MetaWear protocol.
A Swift 6 implementation of the [MetaWear protocol](https://github.com/mbientlab/MetaWear-API).

This repository contains both the reusable Swift Package products and the MetaWear app:

- Use `MetaWear` when you need the scanner, device actor, BLE transport, protocol layer, and sensor/module APIs.
- Add `MetaWearPersistence` when you want SwiftData-backed session storage and CSV export helpers.
- Add `MetaWearFirmware` only when your app needs over-the-air DFU firmware updates.
- Open `Apps/MetaWear/MetaWearApp.xcodeproj` for the full MetaWear app.

## Table of Contents

| Start here | Use it for |
|------------|------------|
| [Quick Start](#quick-start) | Adding the package, scanning, connecting, streaming, and sending simple commands |
| [Architecture](#architecture) | Understanding the scanner/device/protocol/transport layering |
| [Supported sensors and modules](#supported-sensors-and-modules) | Finding the Swift type and configuration shape for each MetaWear module |
| [Logging](#logging) | On-device flash logging, typed downloads, anonymous logger recovery, and CSV export |
| [Persistence (SwiftData)](#persistence-swiftdata) | Saving downloaded sessions and reconstructing typed samples |
| [Testing](#testing) | Running unit tests, hardware integration tests, and the macOS CLI demo |

## Requirements

Expand All @@ -17,10 +35,10 @@ A clean-room Swift 6 implementation of the MetaWear protocol.

This SDK targets MbientLab MetaMotion boards only. Anything else (legacy MetaWear R / RG / RPro / C / CPro, MetaMotion C, MetaEnvironment, MetaTracker, MetaHealth) is treated as `MWModel.unknown` — connect / read may still work but module-level behaviour is not validated.

| Board | Model number (`0x2A24`) | Hardware revisions (`0x2A27`) | Acc / Gyro chip |
|----------------------|:-----------------------:|:---------------------------------------------|:----------------|
| MetaMotion R / RL | `5` | `r0.1`, `r0.2`, `r0.3`, `r0.4`, `r0.5` | BMI160 |
| MetaMotion S | `8` | `r0.1` | BMI270 |
| Board | Model number (`0x2A24`) | Hardware revisions (`0x2A27`) |
|----------------------|:-----------------------:|:---------------------------------------------|
| MetaMotion R / RL | `5` | `r0.1`, `r0.2`, `r0.3`, `r0.4`, `r0.5` |
| MetaMotion S | `8` | `r0.1` |

`MWModel` decodes the Model Number characteristic into a typed case; `MWDeviceInformation.isHardwareRevisionSupported` cross-checks the revision against the table above:

Expand All @@ -39,15 +57,51 @@ The validator is forgiving about formatting — `"r0.4"`, `"R0.4"`, and `"0.4"`

## What ships in this repository

| Product / target | Kind | What it is |
|-----------------------------------|-------------|--------------------------------------------------------------------------------------------------|
| `MetaWear` | library | Core SDK — scanner, device actor, BLE transport, protocol layer, every sensor module |
| `MetaWearPersistence` | library | SwiftData session storage, depends on `MetaWear` |
| `MetaWearFirmware` | library | Over-the-air firmware update, wraps NordicDFU 4.16.0 (`@preconcurrency`) in an actor-isolated `DFUSession` |
| `MetaWearDemo` | executable | macOS CLI that exercises the core SDK against a real board |
| `Apps/MetaWear/MetaWearApp.xcodeproj` | iOS app | SwiftUI demo — scan / connect / live-stream / log / download / export, SwiftData-backed sessions |
| Product / target | Kind | What it is |
|---------------------------------------|-------------|--------------------------------------------------------------------------------------------------|
| `MetaWear` | library | Core SDK — scanner, device actor, BLE transport, protocol layer, every sensor module |
| `MetaWearPersistence` | library | SwiftData session storage, depends on `MetaWear` |
| `MetaWearFirmware` | library | Over-the-air firmware update, wraps NordicDFU 4.16.0 (`@preconcurrency`) in an actor-isolated `DFUSession` |
| `MetaWearDemo` | executable | macOS CLI that exercises the core SDK against a real board |
| `Apps/MetaWear/MetaWearApp.xcodeproj` | iOS app | MetaWear App — scan / connect / live-stream / log / download / export, SwiftData-backed sessions |

The four SwiftPM products are intentionally split so an app can take just `MetaWear` without pulling NordicDFU or SwiftData.
Hardware integration tests (`MetaWearHardwareTests`) live in `Tests/IntegrationTests/MetaWearTestHost.xcodeproj` — see [Hardware integration tests](#hardware-integration-tests).

---

## Development workflow

### Repository layout

| Path | Purpose |
|------|---------|
| `Sources/MetaWear` | Core SDK: public API, protocol layer, module implementations, CoreBluetooth transport, and mocks |
| `Sources/MetaWearPersistence` | SwiftData session storage and sample reconstruction |
| `Sources/MetaWearFirmware` | Firmware catalog lookup, downloads, and Nordic DFU orchestration |
| `Sources/MetaWearDemo` | macOS CLI smoke test against real hardware |
| `Apps/MetaWear/MetaWear` | SwiftUI iOS demo app, organized by app state, view models, features, and design components |
| `Tests/MetaWearTests` | Unit tests for protocol, parsing, modules, device state, and mock transport behavior |
| `Tests/MetaWearPersistenceTests` | In-memory SwiftData persistence tests |
| `Tests/MetaWearFirmwareTests` | Firmware catalog, version, and server behavior tests |
| `Tests/MetaWearHardwareTests` | Real-device integration tests hosted by `Tests/IntegrationTests/MetaWearTestHost.xcodeproj` |

### Common commands

```bash
# Run all SwiftPM tests that do not require Bluetooth hardware.
swift test

# Run one suite while developing a module.
swift test --filter MWLEDTests

# Exercise the SDK against a nearby board from the command line.
swift run MetaWearDemo
```

The four SwiftPM products are intentionally split so an app can take just `MetaWear` without pulling NordicDFU or SwiftData. Hardware integration tests (`MetaWearHardwareTests`) live in `Tests/IntegrationTests/MetaWearTestHost.xcodeproj` — see [Hardware integration tests](#hardware-integration-tests).
### Documentation standards

Public SDK types should have `///` documentation because they surface in Xcode Quick Help and generated symbol docs. Implementation comments should explain protocol quirks, firmware ordering constraints, or concurrency reasoning; avoid comments that merely restate a line of Swift. Markdown docs should prefer small, runnable snippets and should call out whether hardware is required.

---

Expand Down Expand Up @@ -191,6 +245,7 @@ Enforces the device state machine at compile time; invalid transitions throw `MW
```

Key methods:

```swift
public func connect() async throws
public func disconnect() async throws
Expand Down Expand Up @@ -1352,18 +1407,6 @@ On first run macOS will prompt for Bluetooth permission — grant it once and it

---

## Protocol reference

Full byte-level specification — every module opcode, register, command byte layout, config bit-field, response format, and scale factor:

```
/Users/kasso/Documents/MetaWear-API/docs/protocol-reference.md
```

Run `mkdocs serve` in that directory to browse as a formatted site.

---

## What's not yet implemented

### Known small SDK gaps (deferred from legacy parity)
Expand Down
Loading
Loading