Kühlbox: Einstellungsblock in 20-Byte-Stücken schreiben
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 <noreply@anthropic.com>
This commit is contained in:
@@ -96,6 +96,17 @@ enum AlpicoolProtocol {
|
|||||||
/// Gerät sie wieder zusammensetzen kann.
|
/// Gerät sie wieder zusammensetzen kann.
|
||||||
static let chunkDelay: TimeInterval = 0.15
|
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.
|
/// Zerlegt ein Paket in schreibbare Stücke.
|
||||||
///
|
///
|
||||||
/// Ohne ausgehandelte MTU nimmt BLE nur 20 Nutzbytes je Schreibvorgang an.
|
/// Ohne ausgehandelte MTU nimmt BLE nur 20 Nutzbytes je Schreibvorgang an.
|
||||||
|
|||||||
@@ -399,11 +399,7 @@ final class BMSSession: NSObject {
|
|||||||
sentFrameCount += 1
|
sentFrameCount += 1
|
||||||
lastSendAt = Date()
|
lastSendAt = Date()
|
||||||
|
|
||||||
// Was in einen Schreibvorgang passt, hängt an der ausgehandelten MTU.
|
let pieces = AlpicoolProtocol.chunks(data, limit: writeLimit(for: endpoint))
|
||||||
// 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)
|
|
||||||
|
|
||||||
for (index, piece) in pieces.enumerated() {
|
for (index, piece) in pieces.enumerated() {
|
||||||
guard index > 0 else {
|
guard index > 0 else {
|
||||||
@@ -424,6 +420,19 @@ final class BMSSession: NSObject {
|
|||||||
publishDiagnostics()
|
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
|
// MARK: - Steuern
|
||||||
|
|
||||||
/// Schickt einen Stellbefehl und fragt kurz darauf den Zustand ab, damit
|
/// Schickt einen Stellbefehl und fragt kurz darauf den Zustand ab, damit
|
||||||
|
|||||||
@@ -544,6 +544,49 @@ check("aus dem langen Datensatz wird er zu lang für einen Schreibvorgang",
|
|||||||
longPacket.count > 20, "-> \(longPacket.count) Byte")
|
longPacket.count > 20, "-> \(longPacket.count) Byte")
|
||||||
let pieces = AlpicoolProtocol.chunks(longPacket, limit: 20)
|
let pieces = AlpicoolProtocol.chunks(longPacket, limit: 20)
|
||||||
checkEqual("wird in zwei Stücke geteilt", pieces.count, 2)
|
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("kein Stück überschreitet die Grenze", pieces.allSatisfy { $0.count <= 20 }, true)
|
||||||
checkEqual("zusammengesetzt ergibt sich das Original",
|
checkEqual("zusammengesetzt ergibt sich das Original",
|
||||||
[UInt8](pieces.reduce(Data(), +)), [UInt8](longPacket))
|
[UInt8](pieces.reduce(Data(), +)), [UInt8](longPacket))
|
||||||
|
|||||||
Reference in New Issue
Block a user