diff --git a/CamperMonitor/Bluetooth/BMSSession.swift b/CamperMonitor/Bluetooth/BMSSession.swift index 1e81340..582dfb6 100644 --- a/CamperMonitor/Bluetooth/BMSSession.swift +++ b/CamperMonitor/Bluetooth/BMSSession.swift @@ -95,6 +95,9 @@ final class BMSSession: NSObject { /// teils auch unangemeldet an, Stellbefehle nicht – deshalb wird vor /// einem Befehl notfalls noch einmal angemeldet. private(set) var bindAcknowledged = false + /// Ob vor einem Stellbefehl schon einmal nachgemeldet wurde. Jedes Mal + /// anzumelden lässt die Box bei jedem Tastendruck erneut piepen. + private var didRebindForControl = false private var buffer: [UInt8] = [] private var pollTimer: DispatchSourceTimer? private var lastResponse: Data? @@ -257,6 +260,7 @@ final class BMSSession: NSObject { didUnlock = false didBind = false bindAcknowledged = false + didRebindForControl = false buffer.removeAll() activationToken += 1 let token = activationToken @@ -486,9 +490,12 @@ final class BMSSession: NSObject { return } - // Hat die Box die Anmeldung nie beantwortet, wird sie hier nachgeholt. - // Ein Stellbefehl an eine unangemeldete Box wird sonst verworfen. - guard bindAcknowledged else { + // Hat die Box die Anmeldung nie beantwortet, wird sie einmal je + // Verbindung nachgeholt. Ein Stellbefehl an eine unangemeldete Box + // wird sonst womöglich verworfen - jedes Mal anzumelden lässt die Box + // aber bei jedem Tastendruck zusätzlich piepen. + guard bindAcknowledged || didRebindForControl else { + didRebindForControl = true send(AlpicoolProtocol.packet(.bind)) queue.asyncAfter(deadline: .now() + 0.4) { [weak self] in self?.deliverControl(packet, attempt: 0) @@ -671,6 +678,8 @@ final class BMSSession: NSObject { isBound: dialect == .alpicool ? bindAcknowledged : nil, confirmedWrites: confirmedWrites, lastWriteError: lastWriteError, + fridgePayloadHex: alpicoolState.lastPayload.isEmpty ? nil + : alpicoolState.lastPayload.map { String(format: "%02X", $0) }.joined(separator: " "), gattSummary: gattSummary, sentFrames: sentFrameCount, receivedBytes: receivedByteCount, diff --git a/CamperMonitor/Bluetooth/BluetoothManager.swift b/CamperMonitor/Bluetooth/BluetoothManager.swift index 8dc2f50..e8c2ee3 100644 --- a/CamperMonitor/Bluetooth/BluetoothManager.swift +++ b/CamperMonitor/Bluetooth/BluetoothManager.swift @@ -86,6 +86,10 @@ struct BMSDiagnostics: Hashable { var confirmedWrites: Int /// Fehler des letzten bestätigten Schreibvorgangs. var lastWriteError: String? + /// Nur bei Kühlboxen: die vollständige Nutzlast der letzten Statusantwort. + /// Daran hängt die Länge des Stellbefehls, deshalb gehört sie in die + /// Diagnose. + var fridgePayloadHex: String? /// Vollständiger Dienst-/Merkmalsbaum des Geräts. var gattSummary: [String] var sentFrames: Int diff --git a/CamperMonitor/Views/DeviceDetailView.swift b/CamperMonitor/Views/DeviceDetailView.swift index 397a4dd..b20a0d7 100644 --- a/CamperMonitor/Views/DeviceDetailView.swift +++ b/CamperMonitor/Views/DeviceDetailView.swift @@ -11,6 +11,7 @@ struct DeviceDetailView: View { @State private var editedName = "" @State private var keyInput = "" @State private var showDeleteConfirmation = false + @State private var didCopyReport = false @AppStorage(AppSettings.showDiagnosticsKey) private var showDiagnostics = false /// Ob die technischen Angaben eingeblendet werden. @@ -272,6 +273,33 @@ struct DeviceDetailView: View { return String(format: "0x%02X", byte) } + /// Alles auf einmal, zum Weitergeben. Einzeln abzutippen ist zuviel + /// verlangt, und gerade der Merkmalsbaum ist zu lang dafür. + private func report(_ info: BMSDiagnostics) -> String { + var lines = [ + "Gerät: \(currentDevice.name) (\(currentDevice.role.title))", + "Protokoll: \(info.dialect)", + "Verbunden: \(info.isConnected ? "ja" : "nein")", + "Empfang abonniert: \(info.isNotifyActive ? "ja" : "nein")", + ] + if let position = info.endpointPosition { + lines.append("Weg: \(position.index) von \(position.total)") + } + if let endpoint = info.endpointLabel { lines.append("Merkmal: \(endpoint)") } + if let isBound = info.isBound { lines.append("Angemeldet: \(isBound ? "ja" : "nein")") } + lines.append("Gesendet: \(info.sentFrames) · empfangen: \(info.receivedBytes) Byte" + + " · bestätigt: \(info.confirmedWrites)") + if let writeError = info.lastWriteError { lines.append("Schreibfehler: \(writeError)") } + if let command = info.lastCommandHex { lines.append("Letzter Stellbefehl: \(command)") } + if let hex = info.lastResponseHex { lines.append("Letzte Antwort: \(hex)") } + if let payload = info.fridgePayloadHex { lines.append("Statusdaten: \(payload)") } + if !info.gattSummary.isEmpty { + lines.append("Merkmale:") + lines.append(contentsOf: info.gattSummary) + } + return lines.joined(separator: "\n") + } + /// Welches Protokoll das BMS spricht und was zuletzt ankam. @ViewBuilder private var bmsDiagnosticsSection: some View { @@ -350,6 +378,22 @@ struct DeviceDetailView: View { .textSelection(.enabled) } } + if let payload = info.fridgePayloadHex { + VStack(alignment: .leading, spacing: 4) { + Text("Statusdaten der Box (\(payload.split(separator: " ").count) Byte)") + Text(payload) + .font(.caption.monospaced()) + .foregroundStyle(.secondary) + .textSelection(.enabled) + } + } + Button { + UIPasteboard.general.string = report(info) + didCopyReport = true + } label: { + Label(didCopyReport ? "Diagnose kopiert" : "Diagnose kopieren", + systemImage: didCopyReport ? "checkmark" : "doc.on.doc") + } } header: { Text("Diagnose") } footer: {