MetaWear app: App Store readiness (v10.0) + firmware update, persistence & performance work#7
Merged
Conversation
Make Apps/MetaWear submittable as an update to the existing App Store record (id1547334547): - App icon: reuse the existing MetaWear marketing icon (1024, no alpha) - Privacy manifest: add PrivacyInfo.xcprivacy (no tracking/collection; first-party required-reason audit was clean) - Info.plist: version 10.0, ITSAppUsesNonExemptEncryption=false, UIRequiredDeviceCapabilities=[bluetooth-le], UIBackgroundModes=[remote-notification] - Deployment target: standardize on iOS 26.0 (Liquid Glass floor) - CloudKit: enable private-DB sync (cloudKitDatabase .automatic); make all @model types CloudKit-compatible (drop @Attribute(.unique), add defaults); add aps-environment entitlement - Add App Store collateral drafts under Apps/MetaWear/AppStore/ Verified: SDK + 42 persistence tests pass; app builds for iOS 26.0 simulator and bundle validates; app unit tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CloudKit integration requires every relationship — including to-many — to be optional, not just defaulted. The non-optional `samples` relationship crashed the app at container load (NSCocoaErrorDomain 134060: 'all relationships be optional'). Make it [MWSampleRecord]? and coalesce nil to [] in the store and snapshot accessors. Verified: app launches on the simulator, the CloudKit-backed store loads and default.store is created; 42 persistence tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
NSPersistentCloudKitContainer on the app's main ModelContext caused continuous WAL checkpoints and failed background-task scheduling (com.apple.coredata.cloudkit.activity.export, BGSystemTaskSchedulerErrorDomain Code=3), starving the @mainactor view models. Single sensor reads, the Live-Stream Add-sensor menu, and logging start all stalled, while raw BLE streaming (which never touches SwiftData) kept working — the tell. CloudKit sync is not required for v1. Set cloudKitDatabase: .none. Models stay CloudKit-compatible (optional relationships, no .unique, defaults) so it can be re-enabled later with proper setup: register BGTaskSchedulerPermittedIdentifiers for com.apple.coredata.cloudkit.activity, route writes through a dedicated background context, and deploy the schema to Production. Verified: app launches with zero CloudKit-activity / WAL-checkpoint churn in the console; local store loads cleanly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The per-packet debug logs fire dozens of times/sec while streaming, for every BLE advertisement while scanning, and thousands of times during a log download. Each is a synchronous fputs to stderr; with the Xcode debugger attached that write is slow, so DEBUG builds feel laggy across streaming, scanning, and download. (Release was already unaffected: mwLog is #if DEBUG.) Add mwLogVerbose — off by default even in DEBUG, enabled via -MWLogVerbose or MW_LOG_VERBOSE=1 — and move the four hot-path sites (proto read-and-route, BLE handleValueUpdate, central didDiscover, scanner discovered) onto it. Lifecycle logs (connect, scan start/stop, startLogging, command read/write) stay on. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The live chart plotted the full 600-sample ring per channel, and SwiftUI Charts redrew every LineMark on each 33 ms throttle tick. Charts can't sustain 600 x N marks at 30 fps, so as the ring filled each redraw blew past the frame budget and the visible trace fell progressively behind the live data — the 'values take a long time to show up on the graph' symptom. Decimate the plotted series to <=180 points (uniform integer index mapping; first and last preserved). A live card is only a few hundred px wide, so the trace is visually identical while Charts' per-tick work drops ~3x. The full-resolution ring is untouched: archive-to-history still saves every sample, and the numeric readout still shows the true latest value. Adds DownsampleChartTests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The chart card stacked header -> x/y/z readout -> graph with one uniform VStack spacing, leaving the live numbers cramped against the top of the graph. Add extra top padding to just the chart so the readout has room, without changing the header spacing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous per-frame downsample re-picked points from the whole ring every 33 ms, so as new samples arrived the index mapping shifted and every plotted point — including old ones — changed value: the trace looked like it was going crazy. Decimate once, at ingest. Channel.ingest stores every sample full-resolution in `ring` (for archive / latest / true-rate readout) and mirrors 1-of-`displayStride` into a capped `displayRing` that is only ever appended to. The throttle snapshots `displayRing`, so plotted points are real samples that scroll FIFO and are never recomputed — smooth and stable. Stride scales with the configured rate (~30 plotted Hz; e.g. 200 Hz accel → stride 7); low-rate sensors keep every sample. Replaces DownsampleChartTests with ChannelDecimationTests (stride scaling, full-res preservation, and the stability property). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The README was SDK-only; the app was three one-line mentions. Add a 'The MetaWear App' section (features by screen, how to run it incl. Demo Mode, how it's built, and a code map), a Table of Contents entry, and a link from the intro. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the single SwiftData container with two. A CloudKit-backed store holds only RememberedDevice, so the small paired-board list syncs across the user's devices via private iCloud (with a local fallback if CloudKit can't initialize, so a sync hiccup never blocks launch). A separate local-only store owns the high-volume sessions/samples/logs, which must never enter iCloud. This resolves the earlier CloudKit-thrash problem: cross-device board recognition without syncing thousands of sample rows per session. Adds AppModelContainerTests verifying the two stores are distinct and that RememberedDevice vs MWSessionRecord land in their respective containers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Apps/MetaWear/AppStore/ drafts (store listing, review notes, privacy policy, screenshots, submission checklist) are internal submission material. Stripped from history and git-ignored so they stay local-only and never reach the remote. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-light leak From the pre-submission review (all low-severity, verified): - A1: StreamSessionViewModel.stop() now archives the captured buffers to Session History (idempotent via hasArchived), so Stop-then-background no longer silently loses the session. - A3: the RememberedDevices CloudKit fallback reuses the same ModelConfiguration name with cloudKitDatabase: .none, so a failed-CloudKit launch reopens the same store file instead of orphaning synced devices in a divergent SQLite file. - A5: ControlsViewModel.readAmbientLight() tears the stream down on both success and error paths, so a mid-read failure no longer leaves the LTR329 enabled and self-blocking subsequent reads. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pressure and Ambient Light reads were shown unconditionally. MetaMotion R/RL and S carry no barometer or ambient-light sensor, so those reads timed out / failed. ControlsViewModel.loadModules() now reads the device's discovered module set, and the view shows the Pressure / Ambient Light rows only when the board reports those modules present — matching the gating in SensorConfigView / LogSessionView (isPresent + SensorKey.module). Temperature (NRF die, present on every board) is unchanged. Also folds in a PrivacyInfo.xcprivacy comment update describing the split-container privacy story (no change to declared keys). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Render Quick Reads from a presence-driven spec list instead of hardcoded rows: each one-shot-readable sensor (temperature, pressure, ambient light) appears only when its module is discovered present, so a board shows exactly the reads it supports — and adding another one-shot read is a one-line spec. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A new Firmware section in Device Settings surfaces the board's installed revision, checks MbientLab's release catalog for a newer build, and runs an over-the-air DFU update with a live progress readout. FirmwareUpdateViewModel wraps the MetaWearFirmware API: - checkForFirmwareUpdate() -> Up to date / Update available - updateFirmwareToLatest() streamed into a Phase enum the section renders - idle guard up front so a busy board fails before the bootloader handoff - only a .completed event counts as success (an empty stream = already latest) - reconnects through AppStore afterwards to refresh the now-stale deviceInfo The section is gated to real hardware (device.identifier != DemoBLETransport.deviceIdentifier), so Demo Mode never hits the live catalog with a synthetic firmware revision. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rework the placeholder board into a more hardware-accurate MetaMotion-style model: a rounded outer shell with an inset front-panel highlight, LED/button discs, and a USB edge detail, built from reusable addRoundedBox / addDisc / material helpers. Preserve the entity's own scale during the orientation animation (was forcing scale .one) and refresh the loader scale/offsets and the doc comments for the still-optional real USDZ. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Gets the next-generation MetaWear iOS app (
Apps/MetaWear/) ready to ship as an update (v10.0) to the existing App Store record (Apple ID 1547334547, bundlecom.mbientlab.MetaWear), plus the fixes and features found along the way.App Store readiness (v10.0)
CFBundleShortVersionString = 10.0,ITSAppUsesNonExemptEncryption = false,UIRequiredDeviceCapabilities = [bluetooth-le].PrivacyInfo.xcprivacy(no tracking, no collected data types).UIBackgroundModes = [remote-notification]+aps-environmentsolely for background CloudKit sync of remembered devices (documented in the review notes).Persistence / CloudKit
MWSessionRecord.samples).RememberedDevice→ CloudKit private DB; sessions/samples/log records → local-only (cloudKitDatabase: .none). High-volume telemetry never enters iCloud — syncing thousands of sample rows thrashed the main actor and bloated iCloud storage.Performance
-MWLogVerboseflag (the log flood was a real source of lag).Features
DFUProgressreadout, and reconnects afterward. Idle-guarded; gated to real hardware so Demo Mode never hits the live catalog. (Untested on hardware — validate via TestFlight.)Other
AppModelContainerTests(container separation),ChannelDecimationTests.🤖 Generated with Claude Code