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))