Files
Marc Billow bb20eea191 laundry: a payload sitting there is not evidence of when it got there
The Download-course candidate came from "Course_ read while a non-sentinel
one-time payload is loaded". On the first rep after any restart that is
indistinguishable from a payload left over from a previous run, so an
appliance holding cloud payloads while sitting on an ordinary course
proposed that ordinary course as the Download one. Accepting the prefill
would then make selecting a downloaded program start, say, a cotton wash.

Only a transition actually watched counts now. "Never observed" is a
distinct state from "observed, nothing loaded" -- absent-then-loaded is a
genuine selection and still counts -- so a restored store deliberately
re-enters the unobserved state, since a restart cannot tell the two apart.

Payloads are still learned from that first rep either way; which programs
exist is device fact regardless of when they were loaded. It is only the
inference about which course means Download that needs the timing.

Both corpus dumps taken off the Download course show the appliance clearing
its one-time token to the FFFF sentinel, so this may never fire on these
boards. That is a reason to expect them to behave, not to depend on it.
2026-08-10 10:16:00 +00:00

14 KiB
Raw Permalink Blame History

Laundry cloud "Download" cycles: solved, with one dead end

registry/capabilities/laundry.py's cycle select offers a washer's downloaded ("Download" / "Downloaded") programs alongside its ordinary courses now, driven by the store in cloudcourse.py (issue #342). This file records the byte-level work behind it, including a decode that fit one device perfectly and collapsed on the second — the reason nothing in the shipped code interprets a program payload at all.

Four devices in the corpus carry these tokens (a survey of every laundry diagnostics dump attached to an issue turned up 14 devices; the other 10 have no cloud tokens at all, so this is a minority feature):

dump model slots advertised payloads seen blob width
washer_ww5000c_cloud WW5000C _B06C, DA_WM_TP1_21_COMMON, Table_02 9 2 20 bytes
washer_wa55a7700av WA55A7700AV, DA_WM_TP1_21_COMMON, Table_02 2 1 16 bytes
dishwasher_dw5000c_cloud DW5000C, DA_DW_TP1_21_COMMON 4 (but see below) 0 —
(not fixtured) WW5000C _B048, issues #259/#343, Table_02 9 1 20 bytes

Three things follow immediately from that table:

  • CloudExtraCourse_ does not mean the same thing on every family. On the DW5000C all four of its bytes (8E 8D 8F 02) are course codes in that dishwasher's own course list, three already translated (Plastic, Pots and pans, Baby Care). There it tags which ordinary courses came from the cloud; they select with a plain Course_ write and need no payload — consistent with it carrying no payload token at all. It also has a DownloadCourseList_8F token the washers lack. On both washers the slots share zero overlap with the course list and a payload is required. Subtracting the course list is what tells the two apart (cloudcourse.cloud_slots), so the feature engages on the washers and correctly does nothing on the dishwasher.
  • A device can advertise a slot it has never loaded. True on the washers too — nothing about a program is learnable until its owner runs it, which is what the Repairs issue exists to explain.
  • Both WW5000C units advertise the byte-identical slot list (0A5C286B2D0C55301A, same nine slots in the same order) despite different firmware builds. Either the set is a factory/regional default rather than something each owner curates, or the two dumps share an owner — unresolved, but worth knowing before assuming a user picked their own programs.

The three tokens

All on /course/vs/0's x.com.samsung.da.options array, same prefix-match/replace merge as every other token there.

  • CloudExtraCourse_<slot><slot>… — the device's own list of downloaded program slots, one byte each. The cloud counterpart of EditCourseList_.
  • CloudCourse_<blob> — the persisted default program.
  • OneTimeCloudCourse_<blob> — a this-run-only override.

CloudExtraCourse_ is an enumeration, and byte 2 of a blob is its slot

The WW5000C reports CloudExtraCourse_0A5C286B2D0C55301A — nine bytes for its nine downloaded programs. Byte 2 of each of the nine blobs its owner captured is exactly one of those nine, no repeats, sets equal:

        blob                                       byte2  program
        00 21 55 04 49 28 4D 13 4A A0 4C 00 …       55    Sports
        00 20 28 04 49 00 4D 00 4A B8 4C 00 …       28    Spin only
        00 02 5C 04 49 28 4D 13 4A B0 4C 00 …       5C    Outdoor
        00 1F 6B 04 49 28 4D 13 4A A0 4C 00 …       6B    Jeans
        00 2E 2D 04 49 30 4D 12 4A A0 4C 00 …       2D    Super quiet
        00 2F 0C 04 49 58 4D 14 4A B8 4C 00 …       0C    Baby care intensive
        00 0D 30 04 49 30 4D 12 4A B8 4C 00 …       30    Cloudy heaven
        00 30 1A 04 49 28 4D 12 4A A0 4C 00 …       1A    Shirts
        00 04 0A 04 49 40 4D 13 4A B8 4C 00 …       0A    Towels

CloudExtraCourse_  0A 5C 28 6B 2D 0C 55 30 1A

Confirmed independently on the WA55A7700AV: CloudExtraCourse_5958, and its CloudCourse blob 00 1C 59 05 … has byte 2 = 59. Its OneTimeCloudCourse is FF FF 01 … — byte 2 = 01, which is not an advertised slot, and the FFFF prefix marks it as "nothing loaded" rather than naming a program. That sentinel is why cloudcourse.is_loaded exists.

This is what makes "3 of 9 discovered" answerable, and it is why no catalog of program ids is hardcoded anywhere: the appliance already knows which programs it has.

Writing: the two-token rule

Confirmed on hardware by the issue #342 reporter. Writing OneTimeCloudCourse_<blob> alone while some other course is selected is accepted at the protocol level (no error) and then silently ignored by the machine. It takes effect only when the same write also switches Course_ to the Download course — which is what laundry._cloud_cycle_write does, and the only two-token options write in the codebase:

x.com.samsung.da.options:
  - Course_87
  - OneTimeCloudCourse_001F6B0449284D134AA04C0035F004F005F0AC00

CloudCourse_ was separately confirmed writable on its own: set while on Download it changes the running program; set from another course it becomes what gets preselected the next time Download is chosen. The integration doesn't write it today — a "default download cycle" control is a possible follow-up, deliberately left out of the first pass.

There is no single "Download" course code

The WW5000C's Download is Course_87; the WA55A7700AV's is 17 ("Downloaded" in washer_cycle_table_02). Same course table, different code. Any per-table lookup of "the Download code" would have been wrong on one of the only two devices available to check it against, which is why the code is learned by observation and confirmed by the user in the options flow instead of tabled.

The observation signal is "whatever Course_ reads at the moment a non-sentinel OneTimeCloudCourse_ appears or changes" — a transition that was actually watched, not a state. Tokens in this array are replaced by prefix and never evicted, so a payload merely sitting there says nothing about when it got there; on the first rep after a restart it is equally consistent with "just loaded" and "left over from last week". Believing it would propose whatever ordinary course the appliance happens to be sitting on, and accepting that prefill starts a real wash cycle. Even a genuine transition is only ever a candidate, confirmed by the user before use.

Both dumps in the corpus taken while off the Download course (washer_wa55a7700av on Course_01, the _B048 washer on Course_1C) show the appliance clearing its one-time token to the FFFF sentinel, so the saved default persists but the one-shot does not. That makes the stale case unlikely on these boards — which is a reason to expect it to behave, not a reason to depend on it.

The dead end: bytes 5/7/9 do not decode portably

With the nine WW5000C programs and their app-reported settings side by side, three of the varying bytes fit perfectly:

byte meaning formula fit
5 wash temperature (b - 0x10) / 0.8 °C, 0x00 = n/a 9/9
7 rinse count b - 0x10, 0x00 = off 9/9
9 spin level (b - 0x90) / 8 9/9

Nine for nine, including the internally consistent case: "Spin only" is the only program with 0x00 in both byte 5 and byte 7, matching a cycle that skips washing entirely while still reporting a spin level.

It does not survive the second device. Against the WA55A7700AV's CloudCourse blob 00 1C 59 05 49 16 4D 11 4A 22 4C 20 37 F0 AC 22:

  • temperature: (0x16 - 0x10) / 0.8 = 7.5 °C
  • spin: (0x22 - 0x90) / 8 = negative
  • rinse: 0x11 - 0x10 = 1 — the only plausible one

What does survive is the grammar

The two boards' payloads are different lengths (20 vs 16 bytes) but not a different format — same header, same leading fields, two fewer optional trailing ones:

WW5000C  00 | 2155 | 04 | 49:28  4D:13  4A:A0  4C:00 | 35:F0  04:F0  05:F0 | AC:00
WA55     00 | 1C59 | 05 | 49:16  4D:11  4A:22  4C:20 | 37:F0               | AC:22
  • 00, then the 2-byte program id, then one byte (04 vs 05) — identical layout on both.
  • Then a tag/value stream whose first four tags are the same, in the same order, at the same offsets: 49, 4D, 4A, 4C. These are exactly the four whose values vary per program.
  • Then a fixed tail, terminated on both by an AC:<value> pair. The entire width difference is two trailing pairs the WA55 doesn't carry.

The tail is not program data. Across all nine WW5000C programs bytes 12–19 are byte-identical — every trailing pair carries value F0 except the AC terminator, and 35 vs 37 looks like a board or profile marker rather than a field. (It is not a field count either: the board with the higher leading byte has fewer pairs.)

Byte 3 is not part of a program's identity

Worth its own heading, because it is the single strongest argument against ever shipping a table of payloads. The second WW5000C (issues #259/#343, firmware _B048) has its saved CloudCourse set to the same program as the first one's "Towels" capture — and the two payloads differ at exactly one byte:

_B06C  "Towels"      00 04 0A 04 49 40 4D 13 4A B8 4C 00 35 F0 04 F0 05 F0 AC 00
_B048  CloudCourse   00 04 0A 06 49 40 4D 13 4A B8 4C 00 35 F0 04 F0 05 F0 AC 00
                              ^^

Same program id, same slot, same values on all four varying tags, same tail. Only byte 3 moves, 04 → 06. So it is neither a per-board constant (both are WW5000C) nor a property of the program (identical in every other respect) — most likely a download revision or sequence counter.

A hardcoded catalog keyed on program id would therefore have shipped one unit's byte 3 to the other unit. Whether the appliance would reject that, or accept it and do something unintended, is untested and does not need to be: every payload is learned from the device it will be replayed to.

Sentinels

The FFFF "nothing loaded" payload takes its board's own width (16 bytes on the WA55, 20 on the WW5000C _B048) and always carries byte 3 = 00. Its byte 2 is not reliably meaningful: it equals the currently selected course on the WA55 (01, on Course_01) and does not on the _B048 (1B, on Course_1C). Nothing keys off it — a sentinel is rejected on its FFFF prefix, and its byte 2 is not an advertised slot in either dump anyway.

So the payload is tag/value, not fixed offsets — but knowing the grammar doesn't recover the values. The same four tags carry non-overlapping ranges between the two boards:

tag WW5000C (9 programs) WA55
49 00, 28, 30, 40, 58 16
4D 00, 12, 13, 14 11
4A A0, B0, B8 22
4C 00 (all nine) 20

Same field, board-specific encoding. Decoding it properly needs a third device; one device's fit is a coincidence-shaped hypothesis, not a format.

A trap for whoever picks this up: the WA55's /washer/vs/0 reads Warm / High / 1, which looks like it could confirm a decode of that unit's CloudCourse. It can't — that appliance is sitting on Course_01 (Normal), not on its cloud course, so those values describe the local cycle it has selected, not the saved cloud program. A cross-check like this is only evidence when the machine is actually loaded with the program being decoded.

So the shipped code never interprets a payload: a blob is recorded whole and replayed byte-for-byte, exactly as the device reported it, and never decomposed or rebuilt. The read-only "loaded program's temperature/spin" sensors this decode would have enabled were dropped for the same reason.

What is deliberately not done

  • No hardcoded program catalog. Blobs are cloud-assigned per account/region. One owner's captured payload is not evidence about anyone else's appliance, and a table of them would offer options that write another household's wash settings.
  • No invented names. The appliance reports an opaque slot id and nothing else. Names come from the user in the options flow, the same rule that stops an unrecognized local course code from getting a made-up English label (PR #251 review).
  • No blob synthesis. Even with the byte 5/7/9 decode in hand, nothing builds a payload from parts — the device was never tested with one, and a fabricated blob is an untested write to a wash cycle.

Open questions for the next dump

  1. Does OneTimeCloudCourse_ clear itself when a cycle finishes, or when the course changes? Behavior suggests the appliance falls back to CloudCourse_ when Download is re-entered, but the token's own lifecycle is unconfirmed. laundry.cloud_current is written to be correct either way.
  2. What does byte 1 mean? It is distinct per program and sometimes coincides with a plausible local course code for that program (2E Baby Care for "Baby care intensive", 30 Cloudy Day for "Cloudy heaven") and sometimes doesn't (21 Colors for "Sports"). Probably a base-course reference; not reliable enough to use.
  3. What does byte 3 count? It moves between two units holding the identical program (04 vs 06) and is 00 on every sentinel. A revision or download counter is the obvious guess; a dump taken before and after re-downloading the same program would confirm it.
  4. The value encoding is still open, and a third washer won't necessarily settle it. The survey found one, but its saved program is a duplicate of one already captured, so it adds no new tag values. What is actually needed is a dump from a board whose /washer/vs/0 speaks in named levels (Cold/Warm/Hot, Low/High) while that unit is sitting on a downloaded program — then the payload's 49/4A values can be read against settings that describe the same program. The WA55 is such a board but was captured on a local course, which is why it can't be used (see the trap above).
  5. The DW5000C's four slots (8E 8D 8F 02) are in the same numeric range as dishwasher course codes (its selected course is 86), unlike the washers' slots. One payload from that machine would show whether slot ids are drawn from the course-code space on some boards.