Kühlbox: fehlenden zweiten Fühler erkennen

Deine Diagnose hat es gezeigt. Die Box sendet 30 Byte Nutzlast, also den
langen Datensatz - hat aber nur einen Fühler. Für den fehlenden zweiten
meldet sie -128, den üblichen Platzhalter, und die Einstellungen der
rechten Zone stehen auf lauter Nullen.

Meine Erkennung hat diesen Platzhalter für einen Messwert gehalten und die
Box deshalb für zweizonig gehalten. Damit ging der lange Stellbefehl raus,
31 Byte statt 20 - und den verwirft sie wortlos. Die Solltemperatur ging
weiter, weil sie ein eigenes, kurzes Kommando hat und von der Zonenfrage
nicht berührt wird. Genau dieses Bild stand im Bericht.

Als zweite Zone gilt jetzt nur noch, wo der zweite Fühler auch wirklich
misst. Für diese Box heisst das: 20 Byte, ein Schreibvorgang.

Nebenbei fiel auf, dass die Anzeige eine rechte Zone mit -128 Grad geführt
hat. Auch das ist damit weg.

Die echte Antwort der Box steht als Prüfung im Testlauf, mitsamt der
Möglichkeit, die Erkennung in den Geräteeinstellungen zu übersteuern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
BiasF
2026-08-31 09:17:28 +02:00
co-authored by Claude Opus 5
parent 8f8edb137b
commit c4e444dd27
2 changed files with 68 additions and 17 deletions
+22 -11
View File
@@ -92,6 +92,9 @@ enum AlpicoolProtocol {
static func signed(_ byte: UInt8) -> Int { Int(Int8(bitPattern: byte)) } static func signed(_ byte: UInt8) -> Int { Int(Int8(bitPattern: byte)) }
/// Womit diese Boxen einen nicht vorhandenen Fühler melden.
static let missingSensorReading = -128
/// Pause zwischen den Teilstücken eines aufgeteilten Pakets, damit das /// Pause zwischen den Teilstücken eines aufgeteilten Pakets, damit das
/// Gerät sie wieder zusammensetzen kann. /// Gerät sie wieder zusammensetzen kann.
static let chunkDelay: TimeInterval = 0.15 static let chunkDelay: TimeInterval = 0.15
@@ -154,16 +157,24 @@ struct AlpicoolState: Equatable {
/// Übersteuerung aus den Geräteeinstellungen. /// Übersteuerung aus den Geräteeinstellungen.
var zoneMode: FridgeZoneMode = .automatic var zoneMode: FridgeZoneMode = .automatic
/// Rohe Bytes der rechten Zone, für die Erkennung und die Diagnose. /// Die Einstellungsbytes der rechten Zone, für die Erkennung und die
/// Diagnose. Ohne den Messwert der wird getrennt beurteilt.
var rightZoneBytes: [UInt8] = [] var rightZoneBytes: [UInt8] = []
/// Ob die Box wirklich eine zweite Zone hat. /// Ob die Box wirklich eine zweite Zone hat.
/// ///
/// Die Nutzlastlänge allein taugt nicht: Einzonen-Boxen senden den langen /// Die Nutzlastlänge allein taugt nicht: Einzonen-Boxen senden den langen
/// Datensatz teils mit, füllen den zweiten Block aber mit Nullen. Als /// Datensatz teils mit und füllen den zweiten Block auf. Zwei Anzeichen
/// zweizonig gilt deshalb nur, wo dieser Block überhaupt etwas enthält. /// verraten das. Erstens meldet die Box für den fehlenden zweiten Fühler
/// -128, den üblichen Platzhalter. Zweitens stehen die Einstellungen der
/// rechten Zone dann auf lauter Nullen oder lauter 0xFF.
///
/// Das ist keine Frage der Anzeige allein: der Stellbefehl fällt für eine
/// Box mit zwei Zonen länger aus, und die falsche Länge wird verworfen.
var detectedDualZone: Bool { var detectedDualZone: Bool {
guard rightCurrent != nil, !rightZoneBytes.isEmpty else { return false } guard let current = rightCurrent,
current != AlpicoolProtocol.missingSensorReading else { return false }
guard !rightZoneBytes.isEmpty else { return false }
return rightZoneBytes.contains { $0 != 0x00 } && rightZoneBytes.contains { $0 != 0xFF } return rightZoneBytes.contains { $0 != 0x00 } && rightZoneBytes.contains { $0 != 0xFF }
} }
@@ -213,7 +224,7 @@ struct AlpicoolState: Equatable {
rightTarget = AlpicoolProtocol.signed(p[18]) rightTarget = AlpicoolProtocol.signed(p[18])
rightCurrent = AlpicoolProtocol.signed(p[26]) rightCurrent = AlpicoolProtocol.signed(p[26])
runningStatus = Int(p[27]) runningStatus = Int(p[27])
rightZoneBytes = Array(p[18...26]) rightZoneBytes = Array(p[18...25])
} else { } else {
rightTarget = nil rightTarget = nil
rightCurrent = nil rightCurrent = nil
@@ -263,12 +274,12 @@ struct AlpicoolState: Equatable {
p[10], p[11], p[12], p[13], // Kompressordrehzahlen p[10], p[11], p[12], p[13], // Kompressordrehzahlen
] ]
// Der zweite Block gehört an den Befehl, sobald die Box ihn auch // Der zweite Block gehört nur an den Befehl, wenn die Box wirklich
// gemeldet hat unabhängig davon, ob die Anzeige eine zweite Zone // zwei Zonen hat. Eine Einzonen-Box sendet den langen Datensatz teils
// zeigt. Die Box erwartet den Befehl in der Länge, in der sie selbst // trotzdem nimmt aber nur den kurzen Befehl an. Stimmt die Erkennung
// antwortet; ein kürzerer wird verworfen, auch wenn der zweite Block // im Einzelfall nicht, lässt sie sich in den Geräteeinstellungen von
// nur Nullen enthält. // Hand festlegen.
if p.count >= 28 { if isDualZone, p.count >= 28 {
data += [ data += [
p[18], // Sollwert rechts p[18], // Sollwert rechts
0, 0, 0, 0,
+46 -6
View File
@@ -530,13 +530,12 @@ checkEqual("leerer zweiter Block gilt nicht als zweite Zone", padded.isDualZone,
// 20 Byte noch in einen Schreibvorgang, aus dem langen nicht mehr. Ohne // 20 Byte noch in einen Schreibvorgang, aus dem langen nicht mehr. Ohne
// Aufteilen verwirft die Box ihn wortlos genau das war der Fehler. // Aufteilen verwirft die Box ihn wortlos genau das war der Fehler.
checkEqual("Stellbefehl aus dem kurzen Datensatz", offBytes.count, 20) checkEqual("Stellbefehl aus dem kurzen Datensatz", offBytes.count, 20)
// Der Befehl folgt der Länge, in der die Box selbst antwortet nicht dem, // Eine Einzonen-Box, die den langen Datensatz nur auffüllt, bekommt den
// was die Anzeige aus dem leeren zweiten Block macht. Eine Box, die den // kurzen Befehl den langen nimmt sie nicht an.
// langen Datensatz sendet, nimmt nur den langen Befehl an. checkEqual("aufgefüllter Datensatz heisst trotzdem kurzer Stellbefehl",
checkEqual("langer Datensatz heisst langer Stellbefehl", padded.settingsCommand(poweredOn: false)?.count, 20)
padded.settingsCommand(poweredOn: false)?.count, 31)
checkEqual("die Anzeige bleibt trotzdem einzonig", padded.isDualZone, false)
padded.zoneMode = .dual
guard let longPacket = padded.settingsCommand(poweredOn: true) else { guard let longPacket = padded.settingsCommand(poweredOn: true) else {
check("Stellbefehl aus dem langen Datensatz wird gebaut", false); exit(1) check("Stellbefehl aus dem langen Datensatz wird gebaut", false); exit(1)
} }
@@ -584,6 +583,47 @@ let expectedOff = Data([
]) ])
checkEqual("Ausschaltbefehl der IceCube Dual stimmt byteweise", checkEqual("Ausschaltbefehl der IceCube Dual stimmt byteweise",
iceCube.settingsCommand(poweredOn: false), expectedOff) iceCube.settingsCommand(poweredOn: false), expectedOff)
// Eine echte Box aus dem Feld: 30 Byte Nutzlast, aber nur ein Fühler. Sie
// füllt den zweiten Block mit Nullen auf und meldet für den fehlenden
// zweiten Fühler -128. Der Stellbefehl muss trotzdem der kurze sein - der
// lange wurde von ihr wortlos verworfen, und genau daran scheiterte das
// Ein- und Ausschalten.
let paddedIceCubePayload: [UInt8] = [
0x00, // Bedienfeld frei
0x01, // eingeschaltet
0x01, // Eco
0x02, // Batteriewächter hoch
0x09, // Soll 9
0x14, 0xEC, // Grenzen 20 / -20
0x02, 0x00, 0x00, // Rückschaltdifferenz, Verzögerung, Celsius
0x00, 0x00, 0xFD, 0x00, // Kompressordrehzahlen
0x17, // Ist 23
0x64, // 100 %
0x0E, 0x03, // 14,3 V
0x00, 0x00, 0x00, 0x00, // rechte Zone: nur Füllung
0x00, 0x00, 0x00, 0x00,
0x80, // kein zweiter Fühler
0x00, // Kompressor aus
0x01, 0x00, // was darüber hinaus kommt
]
var single = AlpicoolState()
for frame in AlpicoolProtocol.extractFrames(from:
alpicoolResponse(command: 0x01, payload: paddedIceCubePayload)).frames {
single.apply(frame)
}
checkEqual("30 Byte Nutzlast werden gelesen", single.leftCurrent, 23)
checkEqual("fehlender zweiter Fühler heisst eine Zone", single.isDualZone, false)
checkEqual("und damit der kurze Stellbefehl",
single.settingsCommand(poweredOn: false)?.count, 20)
checkEqual("der in einen Schreibvorgang passt",
AlpicoolProtocol.chunks(single.settingsCommand(poweredOn: false)!,
limit: AlpicoolProtocol.maxWriteSize).count, 1)
// Von Hand lässt sich die Erkennung übersteuern, falls sie danebenliegt.
single.zoneMode = .dual
checkEqual("von Hand erzwungen wird er wieder lang",
single.settingsCommand(poweredOn: false)?.count, 31)
single.zoneMode = .automatic
// Ob ein Stellbefehl gewirkt hat, wird an den Einstellungsbytes abgelesen - // Ob ein Stellbefehl gewirkt hat, wird an den Einstellungsbytes abgelesen -
// nicht an den Messwerten, die ohnehin schwanken. // nicht an den Messwerten, die ohnehin schwanken.
let fingerprintBefore = iceCube.settingsFingerprint let fingerprintBefore = iceCube.settingsFingerprint