Skip to content

← Firmware Downloads

Post-install setup · Reborn (Y2Linux) · Y2

y2-platform contract inventory (Phase 0 of the Rust port)

Debian-based Linux with a fast clickwheel interface for the Innioasis Y2.

Instructions published by the project, shown here word for word.

y2-platform contract inventory (Phase 0 of the Rust port)

What y2-platform promises to its callers. Written down from the Python platform (Linux 4684423, Beta 8.44, test build 165, 2026-10-28) before it was ported (plan, §3 principle 2: the contract does not change); since 2026-10-29 the Rust crates in this directory are the only implementation and the Python platform is deleted (the goldens below were recorded from it).

The entries were read from the source by five readers (A observation/boot, B power/sleep/time, C connectivity, USB, SSH, D service/media/update/firmware, E reports/diagnostics/maintenance) and spot-checked against the code and the player; treat an entry as a map to the code, and re-read the code it cites before porting it.

Rules every subcommand shares

Entry and errors. /usr/sbin/y2-platform is the Rust binary (it was `exec /usr/bin/python3 -I /usr/lib/y2-platform/main.py "$@"` until test build 169). An error no command turns into its own record gives {"schema": "org.y2linux.error/v1", "state": "Failed", "reason": <message>, "type": <class name>} and exit 1; since the refactor (test build 171) that includes input of an unexpected type, which the Python platform (and the 1:1 port) ended with a traceback on stderr and empty stdout. Argument errors exit 2 with usage on stderr (Arguments). Several commands catch their own errors first and print a different shape: {"state": "Failed", "failure": str(error)} (export-state, reset, firmware, update, time-set, time-remember), {"failure": "report_failed", "reason": <class name>} (report), `{"state": "Failed", "failure": "diagnostic_export_failed"}(export-diagnostics),{"schema": "org.y2linux.codec-settings/v1", "failure": …}`. str(error) of an OSError is Python's text ([Errno 11] Resource temporarily unavailable, with the path for file errors): callers only show or log it, but a port must decide per command whether to reproduce it (the goldens record it).

Reborn's reading of exit codes (crates/reborn-platform/src/client.rs run): stdout must be one JSON object (≤ 1 MiB); a non-zero exit is an error and Reborn shows value["failure"] or operation_failed, except health, whose exit 1 is evidence. So the org.y2linux.error/v1 shape (it has reason, no failure) always reaches the owner as operation_failed, e.g. every busy media.lock/internal-media.lock and sleep_operation_in_progress (D). Reborn checks schema for status, capabilities, health and sleep.

stdout. One line of compact serde JSON (since 2026-10-30, docs/planning/PLATFORM-RUST-IDIOMATIC-PLAN.md P1; it was Python's json.dumps with ", "/": " and ensure_ascii=True before): UTF-8 as it is, serde/ryu float spelling. Sorted keys for the generic commands (status, health, capabilities, media, internal-media, space, bench-*, boot-stage) and for sleep, boot-evidence, cpu-idle-policy, report, update, firmware, transfer, network-check, export-*, reset; struct order for the others. codec-runtime prints shell tokens (consumed by usr/libexec/y2/connectivity under set -e); collect streams lines; daemons print nothing. JSON callers are order- and spacing-insensitive; shell callers that grep JSON accept both spacings ('"state": ?"Online"'). Since the idiomatic-Rust plan the contract is that plan's §1 (Reborn's commands, fields, types and reason texts; the files it and the shell read; /data records an older build must still read; the report bundle; the hardware behaviour). Byte identity is no longer part of it, except the pinned digests: the SD media identity, the reports crash signature and the reset plan confirmation hash Python's sorted json.dumps bytes (y2p_core::json::python_dumps_sorted, checked against CPython's output), the reset settings digest hashes record::encode's bytes.

Files. common.atomic_json: temp .<name>.<pid>.<monotonic_ns>.tmp in the same directory (no randomness: the CRNG may not be ready at boot, test_platform_records_do_not_need_the_random_pool), `O_EXCL|O_NOFOLLOW| O_CLOEXEC, mode set byfchmod(default 0600), compact(',', ':')` separators, sorted keys, newline, os.replace; durable=True adds fsync of the file and then of the directory. boot.private_directory creates each missing parent 0700, refuses a non-directory and chmods the target 0700 on every call. ctx.read strips and returns None above its limit (128 KiB); ctx.json returns the default when the type differs; ctx.glob sorts and stops at 256 and, unlike most Rust globs, matches dot-files. Journals (power-journal.jsonl, sleep-diagnostics.jsonl, wifi-journal.jsonl) are bounded appends with one .previous rotation under their lock.

Subprocesses. common.command(argv, timeout=1.0, limit=65536): no shell, stdin and stderr to /dev/null, own session, env exactly PATH=/usr/sbin:/usr/bin:/sbin:/bin LC_ALL=C; on overrun the whole tree is SIGKILLed (kill_tree, V1 code review B5) and a D-state child is left; result {ok, reason, output} with reasons command_unavailable, command_timeout, command_output_limit, command_failed. Exceptions to the rule (direct subprocess.Popen): service-daemon's children (media reconcile|verify, update auto-check, report work, firmware scan), ssh-daemon's dropbear (inherits the environment), the shutdown steps in power.py.

Clocks. CLOCK_MONOTONIC for most deadlines, records (monotonic_ns, monotonic_s) and write windows; CLOCK_BOOTTIME for battery state, the sleep retry gate and "did a sleep happen" (boottime − monotonic grew: F49 verify). Wall clock for report names (UTC queue, local time when saved), the daily update check, space cleanup ages, and record.wall_timestamp. Monotonic floats are compared across processes (power.json freshness ≤ 10 s for sleep admission, Wi-Fi events).

Locks (flock; a port must take the same paths in the same mode)

LockTaken by
/run/y2/activity.lock (shared lease; Reborn holds it while playing; sleep takes it exclusively)export-state, media repair/write, network-check, report send/upload/work, reset purge, transfer commit, update apply/rollback/probe-recovery, product-sleep, y2-suspend
/run/y2/suspend.locksleep request, product-sleep, y2-suspend
/run/y2/media.lockmedia (all but status), firmware scan, report save
/run/y2/internal-media.lockinternal-media mount/unmount/provision/setup
/run/y2/dhcp.lock (blocking, no timeout)wifi-event, dhcp-event
/run/y2/time.lock (blocking)time-set, time-remember, ntp-event, service-daemon
/run/y2/{power,service,ssh,connectivity-monitor}.lock (single instance)the four daemons
/run/y2/{codec-settings,cpu-idle-policy,rtc-alarm}.lockcodec-settings, cpu-idle-policy, rtc-alarm
/run/y2/radiod.lock (single instance)y2-radiod, the radio daemon (Bluetooth and both radios' power; its clients use /run/y2/radiod.sock, y2p_contract::radiod)
/data/system/platform/.boot.lock (blocking)boot-stage, update health-ack
/data/system/platform/.power-journal.lock, .sleep-diagnostics.lockpower-daemon, sleep, product-sleep, suspend-record
/data/system/platform/.maintenance.lock, .wifi-journal.lockreset + product-sleep; connectivity-daemon
/data/updates/.lock, /data/updates/.stage.lockupdate (all writers); product-sleep checks .stage.lock
/data/system/platform/reports/.upload.lock, /data/exports/.diagnostics.lockreport send/upload/work; export-diagnostics
/data/transfer-staging/<id>/lock, /data/reset-quarantine/<id> (directory fd)transfer commit/discard; reset purge

Most are LOCK_EX|LOCK_NB with a named refusal (*_in_progress, busy); the blocking ones are marked.

Callers (who must notice nothing)

What a port must not miss (collected from the entries)

Ported to Rust (platform/crates/y2-platform/src/commands)

The dispatcher parses the arguments first (Arguments), then runs every subcommand from its canonical form. There is no Python platform any more (phase 6, 2026-10-29): every subcommand and action has a route (main.rs's test every_command_line_the_parser_takes_has_a_route), the image has no Python, and y2-status, y2-health, y2-suspend-record, y2-power-audit, /sbin/poweroff and /sbin/reboot are links the binary answers to by argv[0] (commands::suspend::program). The internal programmes (__product-sleep, __suspend-record, __power-command, __power-audit) read their own arguments, as the Python scripts did (os.fsdecode for bytes that are not UTF-8, y2p_core::os_text); environment values (dhcp-event, ntp-event) too. y2-platform __arguments prints the argument table (for platform-contract.py).

Arguments

Since 2026-10-29 (owner decision "replace, keep exit 2"; it replaced an emulation of CPython 3.15's argparse) the parser is src/cli.rs: a table of every subcommand's arguments (COMMANDS: type str/int/float, choices, default, required, flag or append, help) and a small parser over it.

Where Python raises an exception main.py catches, Rust prints main.py's error record with Python's exact str(error) and class (ipaddress's AddressValueError and NetmaskValueError, int()'s ValueError, UnicodeEncodeError), or the command's own failure shape where the command catches it; where Python crashes (TypeError, AttributeError, KeyError, IndexError), the port ended the same way; since the refactor these are refusal records (see the refactor). Each command has golden cases, recorded from the Python platform before it was removed (see below).

Command (forms)ModulePython
capabilities [--json]capabilities.rs, codec.rscapabilities.status + cli tail
sleep statussleep.rssleep.status/product
timetime.rstimekeeping.status
time-remembertime.rstimekeeping.remember(every=0)
ntp-event step|stratum|periodic|unsynctime.rstimekeeping.ntp_event
boot-stage <stage>boot.rs, power_features.rsboot.mark, power_features.apply
codec-runtime [--record]codec.rsradio_policy.codec_runtime
wifi-event CONNECTED|DISCONNECTED|SERVICE_RESTARTnetwork.rsnetwork.lease_event
dhcp-event deconfig|leasefail|nak|bound|renewnetwork.rsnetwork.dhcp_event
cpu-idle-policy start|stopcpu_idle.rscpu_idle.apply
every other subcommand, the four daemons, the sleep helper and the scripts (plan §5a groups)commands/<group>/the module named in each file's header

Known, accepted differences (none reachable from the platform's own records): JSON input with NaN, Infinity or integers beyond 64 bits reads as unreadable (Python reads them); an OSError while writing a record names the record, not Python's temporary file; str.splitlines separators other than \n/\r\n in sysfs text; a valid UTF-8 argument of an internal programme holding U+10DC80..U+10DCFF reads as an escaped byte; a y2-suspend-record result beyond 64 bits is refused like a non-number (y2-suspend passes exit codes). Compressed report archives differ in bytes from zlib's (their members do not). File calls use the 64-bit interfaces on the 32-bit player (stat64, statvfs64, …), as Python does; time_t stays 32 bits there (dates after 2038, as the rest of the 32-bit userspace).

Refactor (2026-10-29, test build 171)

PLATFORM-RUST-REFACTOR-PLAN.md: one shared core, typed records, split modules, tests through the root's runner and clock. Outputs stay byte-identical (checked against the build before it with tools/development/platform-raw-goldens.py, key order and spacing included) except these owner decisions and fixes, each a re-recorded golden:

Typed records (2026-10-30, test builds 181–185)

PLATFORM-RUST-IDIOMATIC-PLAN.md (P0–P7, Q1–Q5): every record the platform owns is a Rust struct, read leniently where a field may be missing and written in its fields' types; the Python emulation (pyjson, pyops, Failure::Crash, the dict/json.loads error texts) is gone. Owner rules:

Records Reborn reads in the platform's types (2026-10-30, after Beta 9.0)

The records Reborn decides on are now y2p-contract types the platform writes and Reborn reads, so a field renamed or retyped on one side no longer compiles on the other. Each module keeps the platform's own reading rules (lenient where a record of another version may be read, strict where the platform is the only writer); what needs the player (a root, the clock, a failure text) stays in the platform as free functions or a trait (storage::records::OnPlayer).

ModuleTypesPlatformReborn
lenientthe lenient field readers, BootId, Shapedy2p-core re-exports itclient, power, storage
sleepProduct, ProductState, WakeSourcethe suspend receiptsleep state
updateStatus, CoreJournal, Records, Download, LastCheck, DownloadProgress, DownloadPhaseupdate status, download.json, last-check.json, the live download recordSoftware Update page, download progress
powerPowerRecord, BatteryStatus, Estimate, BatterySource, Confidence, BatteryState, Intent, IntentState, ShutdownAction, Acknowledgement, Step, ProcessIdentitypower.json (the daemon writes the shared record), shutdown.jsonbattery level, voltage reuse, shutdown intent
networkPublishednetwork.json's readersWi-Fi readiness
storageMediaRecord, ReadOnly, Check, Identity, Geometry, Y2MediaClaim, SetupStatusthe SD records, internal-media.json, the setup noteSD source checks, internal music folder, Internal Storage setup
usbReceipt, Viewthe media hand-off's view checkUSB screen, library refresh
reportsReportProgress, ReportPhasereport-progress.jsonreport progress
radioFirmwareStatusfirmware statusWi-Fi/Bluetooth firmware state

Still read as JSON on purpose: the status snapshot's other sections (volumes, USB controller states, health, capabilities, the diagnostics page, which shows whatever a field holds), and the platform's own writers that borrow (connectivity::published::Record, service::usb::Transfer), whose readers are the types above. The recorded contract (crates/reborn-platform/tests/platform-contract.expected.json) did not change in these steps except for the removed download_percent (download.json never carried a byte count).

Defects found while inventorying and porting (fixed 2026-10-29)

Ported 1:1 first (owner decision 2026-10-28: port, then fix), fixed in Rust after Phase 6, each with a test.

FindingFix
usr/libexec/sftp-server grepped internal-media.json for "state": "Ready" (with a space); the record is compact, so SFTP always opened /data/music (confirmed on the player)the grep takes both forms (tests/test_sftp_root.py)
media status exited 1 for a Degraded (read-only) or Failed cardexits 0 for every state it reports (storage::media_exit)
media write exited 0 with failure set for not_mountedexits 1 whenever it sets failure; Reborn's write window treats it as no window
A second update check/stage meeting the busy .stage.lock overwrote download.json with Failed [Errno 11] while the first download went onrefused as update_in_progress before anything is written; auto-check leaves the record alone; Reborn: "An update check is already running."
update rollback asked the core before the battery gate: a battery refusal left the journal RollbackPendingthe battery gate comes first
update auto-check printed null when it did not checkprints {"schema": 1, "result": "skipped", "reason": …} (not_due, update_pending, no_channel, offline, clock_not_ready)
firmware scan exited 0 when the owner's copy could not be installedexits 1 with state Failed, as firmware install
Battery: a voltage sample after fuel-gauge samples had no display value (Python's power daemon ended on a TypeError; found during the port)it starts as a first sample
An expired media write window was re-protected only by a reconcile, which the service started only on an insertion, removal or retrythe service also starts one when media::window_expired (protected, mounted rw, no live holder); device check owed
A DHCP renew deleted dns.json: Wi-Fi showed dns_unavailable for up to 300 sa renew of the same lease (boot, epoch, address, DNS servers) keeps it

Golden suite

tests/platform_golden.py runs one subcommand as the Rust host build (Y2_PLATFORM_RUST) on a fresh copy of a fixture root and compares the normalised result with the case's golden, `tests/platform_goldens/<base|group>/

.json`: argv, exit code, stdout (parsed, root and checkout paths replaced), the subprocess requests and the tree difference against the root as the command found it (paths added, changed or removed, with type, mode, contents parsed per JSON line, symlink targets), after normalising clock readings by key name (`monotonic_ns`, `wall_timestamp`, …), today's date and per-case patterns (uuids). The binary must honour `Y2_ROOT`, `Y2_FIXTURE_COMMANDS`, `Y2_FIXTURE_CALLS`, `Y2_DAEMON_TICKS` and `Y2_FIXTURE_TOKEN` (module docstring). The cases are `tests/test_platform_differential.py`'s and `tests/platform_cases/.py`'s; the groups' modules (`tests/test_platform_{devices,observation,reports,suspend,power_rpc}.py`) add the runs a plain one cannot give (renamed random names, archive listings, a plan made first, a fake Reborn socket, held locks, a live power daemon). `tests/test_platform_argparse.py` runs the 5 729 argument lists of `tests/platform_goldens/ argparse.jsonl.gz` (the parse only: `Y2_ARGPARSE_ECHO` prints the canonical form below a fixture root); each line holds Python's argparse outcome with the canonical form routes got then (fixed) and today's outcome (`rust`, re-recorded). A list without a feature the [Arguments](#arguments) decision dropped must keep Python's exit code and canonical form (3 517 lists), one with such a feature is refused now (1 207) or unchanged (1 005). Every golden was first recorded from the Python platform (Linux `3ac47c6`, the Rust build of that commit matched each). All are in `tools/production/tests.sh`; without a host build (the release container has no cargo) the runs are skipped and the goldens' own checks run.

A deliberate behaviour change (a defect fix) re-records the goldens it touches and shows in the git diff:

cd platform && cargo build --offline --locked && cd ..
Y2_RECORD_GOLDENS=1 Y2_PLATFORM_RUST=$PWD/platform/target/debug/y2-platform \
    python3 -m unittest tests.test_platform_differential tests.test_platform_argparse tests.test_platform_devices \
    tests.test_platform_observation tests.test_platform_reports tests.test_platform_suspend tests.test_platform_power_rpc
git diff --stat tests/platform_goldens

Recording writes only goldens that differ and removes goldens no case names; without it a missing golden fails. Add a case (and record its golden) for each changed command.

Published by the schulzcode/y2linux project [1] [3]