From 58de005fa7f26f0321a08c2b2dbe187c7fab5917 Mon Sep 17 00:00:00 2001 From: BiasF Date: Sun, 30 Aug 2026 23:58:47 +0200 Subject: [PATCH] =?UTF-8?q?K=C3=BChlbox:=20Einstellungsblock=20in=2020-Byt?= =?UTF-8?q?e-St=C3=BCcken=20schreiben?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Ursache, belegt statt vermutet. Im Referenzprojekt gibt es einen Bericht von genau dieser Box - einer Maentum/Plug-in Festival IceCube Dual (Gruni22/alpicool_ha_ble#20). Dort galt: Solltemperatur ändern geht, Ein- und Ausschalten nicht. Genau unser Bild. Der Grund ist nicht das Paket, sondern der Schreibvorgang. Die Box nimmt nur die 20 Nutzbytes der Standard-MTU je Schreibvorgang an. Der kurze Temperaturbefehl hat sieben Byte und geht durch, der Einstellungsblock hat 31 und wird abgelehnt. Wir haben bisher nach der ausgehandelten MTU aufgeteilt - und die ist unter iOS weit grösser als 20, also wurde nie aufgeteilt. Jetzt gilt für die Kühlbox die Grenze der Box, nicht die der Verbindung: 31 Byte gehen als 20 und 11 raus, mit der üblichen Pause dazwischen. Das mitgeschnittene Paket aus dem Bericht steht als Prüfung im Testlauf. Unsere Erzeugung trifft es byteweise - Längenbyte, Datenblock, rechte Zone, Prüfsumme -, es scheiterte also wirklich nur am Schreiben. Co-Authored-By: Claude Opus 5 --- .../Bluetooth/AlpicoolProtocol.swift | 11 +++++ CamperMonitor/Bluetooth/BMSSession.swift | 19 +++++--- Tests/main.swift | 43 +++++++++++++++++++ 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/CamperMonitor/Bluetooth/AlpicoolProtocol.swift b/CamperMonitor/Bluetooth/AlpicoolProtocol.swift index c8580ea..8a2d930 100644 --- a/CamperMonitor/Bluetooth/AlpicoolProtocol.swift +++ b/CamperMonitor/Bluetooth/AlpicoolProtocol.swift @@ -96,6 +96,17 @@ enum AlpicoolProtocol { /// Gerät sie wieder zusammensetzen kann. static let chunkDelay: TimeInterval = 0.15 + /// Wieviel die Box je Schreibvorgang annimmt. + /// + /// Das sind die 20 Nutzbytes der Standard-MTU – unabhängig davon, was auf + /// der Verbindung ausgehandelt wurde. Ein längerer Schreibvorgang wird von + /// diesen Boxen abgelehnt; belegt an einer Maentum/Plug-in Festival + /// IceCube Dual, bei der genau deshalb das Ein- und Ausschalten scheiterte, + /// während der kurze Temperaturbefehl durchging + /// (Gruni22/alpicool_ha_ble#20). Das Ändern der Solltemperatur geht mit + /// sieben Byte durch, der Einstellungsblock mit 31 nicht. + static let maxWriteSize = 20 + /// Zerlegt ein Paket in schreibbare Stücke. /// /// Ohne ausgehandelte MTU nimmt BLE nur 20 Nutzbytes je Schreibvorgang an. diff --git a/CamperMonitor/Bluetooth/BMSSession.swift b/CamperMonitor/Bluetooth/BMSSession.swift index 1665e21..ee8343d 100644 --- a/CamperMonitor/Bluetooth/BMSSession.swift +++ b/CamperMonitor/Bluetooth/BMSSession.swift @@ -399,11 +399,7 @@ final class BMSSession: NSObject { sentFrameCount += 1 lastSendAt = Date() - // Was in einen Schreibvorgang passt, hängt an der ausgehandelten MTU. - // Längeres muss aufgeteilt werden, sonst verwirft das Gerät es - // wortlos – der Einstellungsblock einer Kühlbox ist so ein Fall. - let limit = peripheral.maximumWriteValueLength(for: endpoint.writeType) - let pieces = AlpicoolProtocol.chunks(data, limit: limit) + let pieces = AlpicoolProtocol.chunks(data, limit: writeLimit(for: endpoint)) for (index, piece) in pieces.enumerated() { guard index > 0 else { @@ -424,6 +420,19 @@ final class BMSSession: NSObject { publishDiagnostics() } + /// Wieviel je Schreibvorgang rausgeht. + /// + /// Grundsätzlich das, was die Verbindung hergibt. Die Kühlboxen nehmen + /// aber nur die 20 Byte der Standard-MTU an, auch wenn iOS eine grössere + /// aushandelt und damit weit mehr erlauben würde. Ohne diese Grenze ginge + /// der Einstellungsblock als ein Schreibvorgang raus – und die Box würde + /// ihn ablehnen, während die kurzen Befehle durchgehen. + private func writeLimit(for endpoint: Endpoint) -> Int { + let negotiated = peripheral.maximumWriteValueLength(for: endpoint.writeType) + guard dialect == .alpicool else { return negotiated } + return min(negotiated, AlpicoolProtocol.maxWriteSize) + } + // MARK: - Steuern /// Schickt einen Stellbefehl und fragt kurz darauf den Zustand ab, damit diff --git a/Tests/main.swift b/Tests/main.swift index 001a1fc..6cccacc 100644 --- a/Tests/main.swift +++ b/Tests/main.swift @@ -544,6 +544,49 @@ check("aus dem langen Datensatz wird er zu lang für einen Schreibvorgang", longPacket.count > 20, "-> \(longPacket.count) Byte") let pieces = AlpicoolProtocol.chunks(longPacket, limit: 20) checkEqual("wird in zwei Stücke geteilt", pieces.count, 2) + +// Ein echtes Paket der Maentum/Plug-in Festival IceCube Dual, mitgeschnitten +// beim Ausschalten (Gruni22/alpicool_ha_ble#20). Es muss byteweise +// herauskommen - und in zwei Schreibvorgänge zerfallen, weil die Box längere +// nicht annimmt. Genau daran scheiterte das Ein- und Ausschalten. +let iceCubePayload: [UInt8] = [ + 0x00, // Bedienfeld frei + 0x01, // eingeschaltet + 0x01, // Eco + 0x02, // Batteriewächter hoch + 0x14, // Soll links 20 + 0x14, 0xEC, // Grenzen 20 / -20 + 0x02, // Rückschaltdifferenz + 0x00, // Anlaufverzögerung + 0x00, // Celsius + 0xFD, 0xFD, 0xFD, 0x00, // Kompressordrehzahlen + 0x0A, // Ist links 10 + 0x57, // 87 % + 0x0C, 0x06, // 12,6 V + 0x14, // Soll rechts 20 + 0x00, 0x00, + 0x02, // Rückschaltdifferenz rechts + 0xFD, 0xFD, 0xFD, 0x00, // Kompressordrehzahlen rechts + 0x0A, // Ist rechts 10 + 0x01, // Kompressor läuft +] +var iceCube = AlpicoolState() +for frame in AlpicoolProtocol.extractFrames(from: + alpicoolResponse(command: 0x01, payload: iceCubePayload)).frames { + iceCube.apply(frame) +} +let expectedOff = Data([ + 0xFE, 0xFE, 0x1C, 0x02, + 0x00, 0x00, 0x01, 0x02, 0x14, 0x14, 0xEC, 0x02, 0x00, 0x00, + 0xFD, 0xFD, 0xFD, 0x00, + 0x14, 0x00, 0x00, 0x02, 0xFD, 0xFD, 0xFD, 0x00, 0x00, 0x00, 0x00, + 0x09, 0x37, +]) +checkEqual("Ausschaltbefehl der IceCube Dual stimmt byteweise", + iceCube.settingsCommand(poweredOn: false), expectedOff) +checkEqual("er passt nicht in einen Schreibvorgang", + AlpicoolProtocol.chunks(expectedOff, limit: AlpicoolProtocol.maxWriteSize) + .map(\.count), [20, 11]) checkEqual("kein Stück überschreitet die Grenze", pieces.allSatisfy { $0.count <= 20 }, true) checkEqual("zusammengesetzt ergibt sich das Original", [UInt8](pieces.reduce(Data(), +)), [UInt8](longPacket))