Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f60b5c4
Group logging: log on a whole fleet at once, MetaBase style
lkasso Jul 19, 2026
6146b6f
Board picker rows carry identity: live RSSI + MAC (or iOS identifier)
lkasso Jul 20, 2026
a8951f4
Settle the MMS's async page flush before trusting LOG_LENGTH; center …
lkasso Jul 20, 2026
b24903f
Merge branch 'main' into group-logging
lkasso Jul 20, 2026
295c877
Board picker names render in label black, not the button tint
lkasso Jul 20, 2026
40be971
Flush the MMS page while logging is live; wait out a zero that can't …
lkasso Jul 20, 2026
9e22051
Group boards blink a slow red heartbeat while recording
lkasso Jul 20, 2026
e66dcb6
Retry failed group downloads in place; drop Add To Group for now
lkasso Jul 20, 2026
6c0c753
Group start clears stale boards; partial saves stop posing as failures
lkasso Jul 20, 2026
0fb66d6
Download paths narrate their decode outcomes to the debug console
lkasso Jul 20, 2026
6159abb
Wait for the firmware's Drop Entries completion before arming loggers
lkasso Jul 20, 2026
d8d1dd2
Each sensor picks its variant from ITS OWN module byte; clear waits s…
lkasso Jul 20, 2026
06dbf1a
Saved sessions are one tap from the group screen; empty records close…
lkasso Jul 20, 2026
ef245e8
Settings shows 0/0 after a clear instead of re-reading a lying board
lkasso Jul 20, 2026
ad919fa
Disconnect returns to the scan list, not a blank detail pane
lkasso Jul 20, 2026
4fd171c
Group start verifies entries are LANDING before trusting a board
lkasso Jul 20, 2026
5ddbec5
Group Logging becomes a navigation value; View Saved Sessions works
lkasso Jul 20, 2026
33281ed
History section titles show the MAC, not the DIS serial
lkasso Jul 20, 2026
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
13 changes: 13 additions & 0 deletions Apps/MetaWear/MetaWear/App/AppStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ final class AppStore {

// MARK: - Demo device

/// Orchestrates group logging (start/stop-and-download across several
/// boards). Owned here — never by a view — so an in-flight fleet walk
/// survives navigation. Created lazily; solo sessions never pay for it.
private var _groupCapture: GroupCaptureCoordinator?
var groupCapture: GroupCaptureCoordinator {
if let coordinator = _groupCapture { return coordinator }
let coordinator = GroupCaptureCoordinator(
containers: containers, persistence: persistence, appStore: self
)
_groupCapture = coordinator
return coordinator
}

/// The fully simulated MetaWear fleet (see `DemoBLETransport.Identity`).
/// Created on first access so non-demo sessions never pay for it; each
/// board is a stable instance reused across connect/disconnect cycles
Expand Down
11 changes: 11 additions & 0 deletions Apps/MetaWear/MetaWear/App/DemoMode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ enum DemoMode {
identities.contains { $0.identifier == id }
}

/// Display MAC for a demo board (from its transport identity, wire
/// order reversed to the usual big-endian colon form); nil otherwise.
static func macAddress(for id: UUID) -> String? {
guard let identity = identities.first(where: { $0.identifier == id }) else {
return nil
}
return identity.macLSBFirst.reversed()
.map { String(format: "%02X", $0) }
.joined(separator: ":")
}

/// Display name for a demo board ("Simulated MetaWear", "Simulated
/// MetaWear 2", …); nil for non-demo IDs.
static func name(for id: UUID) -> String? {
Expand Down
33 changes: 32 additions & 1 deletion Apps/MetaWear/MetaWear/App/RootView.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import SwiftUI
import MetaWear
import MetaWearPersistence

struct RootView: View {
@Environment(AppStore.self) private var appStore
Expand All @@ -22,6 +23,13 @@ struct RootView: View {
.navigationDestination(for: DeviceFeaturePane.self) { pane in
pane.destination()
}
// Declared ONCE at the stack root: SessionHistoryView can
// appear more than once per stack (scan-root link + the
// group screen's link), and per-instance declarations
// triggered SwiftUI's duplicate-destination warning.
.navigationDestination(for: MWSessionSnapshot.self) {
SessionDetailView(snapshot: $0)
}
}
} detail: {
NavigationStack(path: $path) {
Expand All @@ -37,12 +45,21 @@ struct RootView: View {
// transition. In regular (iPad) the sidebar is
// already showing alongside, so the detail just
// sits empty rather than nagging the user.
// Safety net: if this pane ever DOES become the
// visible column in compact (the onChange's flip
// can be swallowed mid-transition), push focus
// back to the sidebar. Harmless in regular width
// — preferredCompactColumn only affects compact.
Color.clear
.onAppear { preferredColumn = .sidebar }
}
}
.navigationDestination(for: DeviceFeaturePane.self) { pane in
pane.destination()
}
.navigationDestination(for: MWSessionSnapshot.self) {
SessionDetailView(snapshot: $0)
}
}
}
.background {
Expand All @@ -62,7 +79,18 @@ struct RootView: View {
// (e.g. a foreignDownload pushed for the previous board) must
// not resolve against the next one.
path = NavigationPath()
preferredColumn = newID == nil ? .sidebar : .detail
if newID == nil {
// Deferred one tick: flipping the compact column in the
// SAME transaction as the path reset + the detail content
// swapping to the blank pane gets swallowed by
// NavigationSplitView — compact users ended up stranded on
// an empty detail page with only a Back button (field bug:
// tap X to disconnect → blank screen instead of the scan
// list).
Task { @MainActor in preferredColumn = .sidebar }
} else {
preferredColumn = .detail
}
}
.overlay {
if isConnecting {
Expand Down Expand Up @@ -159,6 +187,8 @@ enum DeviceFeaturePane: Hashable {
case sensorConfig
case liveStream([SensorSelection])
case logSession
/// MetaBase-style multi-board logging — lives on the sidebar stack.
case groupLogging
case download
/// The Download screen in foreign-session mode: drains a board whose
/// logging was started on another device via the anonymous-logger path.
Expand All @@ -174,6 +204,7 @@ enum DeviceFeaturePane: Hashable {
case .sensorConfig: SensorConfigView()
case .liveStream(let sels): LiveStreamView(selections: sels)
case .logSession: LogSessionView()
case .groupLogging: GroupLoggingView()
case .download: DownloadView()
case .foreignDownload(let s): DownloadView(foreign: s)
case .sessionHistory: SessionHistoryView()
Expand Down
Loading
Loading