diff --git a/Apps/MetaWear/MetaWear/Features/Settings/DeviceSettingsView.swift b/Apps/MetaWear/MetaWear/Features/Settings/DeviceSettingsView.swift index 0ddd140..46584cb 100644 --- a/Apps/MetaWear/MetaWear/Features/Settings/DeviceSettingsView.swift +++ b/Apps/MetaWear/MetaWear/Features/Settings/DeviceSettingsView.swift @@ -19,6 +19,10 @@ struct DeviceSettingsView: View { @State private var activeLoggerCount: Int? @State private var isClearing = false @State private var clearLogError: AppError? + @State private var showClearMacrosConfirm = false + @State private var showClearEventsConfirm = false + @State private var showRestartConfirm = false + @State private var maintenanceError: AppError? var body: some View { Form { @@ -69,6 +73,25 @@ struct DeviceSettingsView: View { Text("Stops any active logging, drops every entry from flash, and removes all logger subscriptions. Local pending sessions for this device are also deleted (their backing data on the board is gone).") } + Section { + Button("Reset LED", systemImage: "lightbulb.slash") { + Task { await resetLED() } + } + Button("Clear Macros", systemImage: "memorychip", role: .destructive) { + showClearMacrosConfirm = true + } + Button("Clear Events & Timers", systemImage: "calendar.badge.minus", role: .destructive) { + showClearEventsConfirm = true + } + Button("Restart Board", systemImage: "arrow.counterclockwise") { + showRestartConfirm = true + } + } header: { + Text("Maintenance") + } footer: { + Text("Reset LED stops and clears the light immediately — if it relights after a disconnect, an on-board event is re-arming it; use Clear Events & Timers. Restart reboots the board without erasing logs or macros.") + } + Section { Button("Factory Reset", systemImage: "exclamationmark.triangle", role: .destructive) { showFactoryResetConfirm = true @@ -120,6 +143,38 @@ struct DeviceSettingsView: View { Text("Any logger subscriptions and pending entries will be removed from the board.") } } + .alert("Clear all macros on this board?", + isPresented: $showClearMacrosConfirm) { + Button("Clear Macros", role: .destructive) { + Task { await clearMacros() } + } + Button("Cancel", role: .cancel) {} + } message: { + Text("Removes every on-boot macro — including the identity broadcast this app configures. The app re-applies its broadcast automatically on the next connect.") + } + .alert("Clear all events and timers?", + isPresented: $showClearEventsConfirm) { + Button("Clear Events & Timers", role: .destructive) { + Task { await clearEventsAndTimers() } + } + Button("Cancel", role: .cancel) {} + } message: { + Text("Removes every on-board event binding and timer — from ALL apps, including this one's recording heartbeat. Use this if the LED keeps relighting after disconnects.") + } + .alert("Restart this MetaWear?", + isPresented: $showRestartConfirm) { + Button("Restart") { + Task { await restartBoard() } + } + Button("Cancel", role: .cancel) {} + } message: { + Text("Reboots the board without erasing anything — logs and macros survive. The board will disconnect.") + } + .alert(item: $maintenanceError) { err in + Alert(title: Text("Maintenance failed"), + message: Text(err.message), + dismissButton: .default(Text("OK"))) + } .alert(item: $clearLogError) { err in Alert(title: Text("Clear logs failed"), message: Text(err.message), @@ -170,6 +225,52 @@ struct DeviceSettingsView: View { } } + // MARK: - Maintenance actions + + /// Stop playback and clear all configured patterns. If the LED comes + /// back after a disconnect, an on-board disconnect EVENT is re-arming + /// it — that's what Clear Events & Timers is for. + private func resetLED() async { + guard let device = appStore.activeDevice else { return } + do { + try await device.stopLED(clearPattern: true) + } catch { + maintenanceError = AppError(error: error) + } + } + + private func clearMacros() async { + guard let device = appStore.activeDevice else { return } + do { + try await device.eraseAllMacros() + } catch { + maintenanceError = AppError(error: error) + } + } + + private func clearEventsAndTimers() async { + guard let device = appStore.activeDevice else { return } + do { + try await device.removeAllEvents() + try await device.removeAllTimers() + } catch { + maintenanceError = AppError(error: error) + } + } + + /// Soft reboot — the board disconnects itself; the same intentional- + /// disconnect handling as Factory Reset takes the user back to the + /// scan list. + private func restartBoard() async { + guard let device = appStore.activeDevice else { return } + do { + try await device.restart() + await appStore.disconnect() + } catch { + maintenanceError = AppError(error: error) + } + } + private func deleteLocalPendingRecords(for deviceID: UUID) { let context = appStore.containers.local.mainContext let descriptor = FetchDescriptor( diff --git a/Sources/MetaWear/MetaWearDevice.swift b/Sources/MetaWear/MetaWearDevice.swift index 958c144..78dbd51 100644 --- a/Sources/MetaWear/MetaWearDevice.swift +++ b/Sources/MetaWear/MetaWearDevice.swift @@ -311,6 +311,31 @@ public actor MetaWearDevice { mwLog("[Device] factoryReset: done") } + /// Reboot the board WITHOUT erasing anything. Flash-resident data — + /// log entries, macros — survives; volatile state (timers, events, + /// sensor enables, a wedged NAND housekeeping pass) is cleared, which + /// is the point: this is the unwedge for a misbehaving board when a + /// factory reset would cost real data. The link drops intentionally; + /// reconnect after ~1 s brings the board back. + public func restart() async throws { + mwLog("[Device] restart: \(identifier)") + guard state != .disconnected else { + throw MWError.invalidState("Cannot restart a disconnected device") + } + // Intentional link drop — same suppression + reset pair (and the + // same MMS fw-1.5.0 ResetAfterGC-no-op fallback) as factoryReset. + await proto.clearDisconnectHandler() + try await send(MWDebug.ResetAfterGC()) + try? await send(MWDebug.Reset()) + await proto.stop() + state = .disconnected + activeStreamKeys.removeAll() + activeFusionConfig = nil + loggerRegistry.removeAll() + logReferenceDate = nil + mwLog("[Device] restart: done") + } + // MARK: - Streaming /// Stream a sensor signal continuously.