Skip to content

← Firmware Downloads

Post-install setup · Reborn (Y2Linux) · Y2

Platform service: port from Python to Rust, then remove Python from the player

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

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

Platform service: port from Python to Rust, then remove Python from the player

Status: All phases implemented on main (2026-10-29): the whole platform is Rust and the image has no Python; test build 169 is on the owner's player (Acknowledged), the owner's hardware checks (incl. two overnight runs) are next. Handoff: PLATFORM-RUST-PORT-HANDOFF.md. Owner's goal: everything that runs on the player is Rust (Reborn), C (kernel, early boot, installer) or shell (init scripts); no Python in the image. Host-side tooling (release, build, validation, the test harness) may stay Python.

This plan is written for an agent picking the work up cold. Read it fully, then AGENTS.md, CURRENT_PLATFORM_STATE and the top entry of NEXT-SESSION-HANDOFF before changing code.

1. Why

Measured on the owner's player (Beta 8.44 test build 165, 37 min awake on the charger, 2026-10-28):

Python platform (4 daemons)Reborn (Rust)
RSSservice 20.3 + connectivity 15.3 + power 12.8 + ssh 11.5 ≈ 60 MB (shared pages counted per process)27.5 MB
CPU time67 s (power-daemon 38 s, service-daemon 23 s)28 s
ImagePython 3.15 ≈ 22 MB (usr/lib/python3.15 17 MB, libpython3.15.so 4.9 MB) + 12 334 lines of source8.6 MB binary
One CLI callpython3 -I -c pass 0.15 s; y2-platform sleep status 0.52 s—

Every CLI call is a new Python process: Reborn's polling (status --reborn --pss every 4–15 s), every wpa-event/dhcp-event/ntp-event, every boot-stage journal write (two per shutdown, ≈0.5 s each, see the F39 trail). Awake CPU costs battery; the 22 MB and the RAM come back only when Python is gone entirely.

Also wanted: one language with Reborn and shared contract types. Reborn and the platform exchange JSON whose field and reason strings are matched by hand on both sides; F52 ("not_authenticated" read as a wrong password) was such a mismatch. A shared serde crate makes the compiler check it.

2. What exists (inventory, 2026-10-28)

Source: tools/platform/y2_platform/ (43 modules, 12 334 lines), entry tools/platform/main.py, helpers product-sleep.py, suspend-record.py, scripts y2-platform, y2-health, y2-power-audit (Python), y2-power-command, y2-status, y2-suspend-record. Installed to /usr/lib/y2-platform + /usr/sbin. Biggest modules: report 1420, media 902, observe 758, update 695, product_sleep 571, internal_media 571, radiofw 568, network 550, usb 483, power 458, cli 430, maintenance 423, bench 396, service 354, battery 337.

Daemons (started by S03y2-platform / platform-supervise / connectivity): power-daemon (battery, charger, sleep, maintenance wakes, shutdown coordinator), service-daemon (media/SD admission, updates, reports, internal storage, time), connectivity-daemon (Wi-Fi/BT supervision, radio policy, F52/F53 heal), ssh-daemon.

CLI subcommands (cli.py): status, health, capabilities, boot-evidence, boot-stage, cpu-idle-policy, bluetooth-power, sleep {request,status,prepare}, power-daemon, service-daemon, connectivity-daemon, usb-mode, codec-runtime, codec-settings, wifi-power-policy, ssh-daemon, transfer, reset, export-diagnostics, report {…}, export-state, firmware, update {check,apply,cancel,rollback,stage,status,…}, time-bootstrap, time, time-set, time-remember, rtc-alarm, ntp-event, wifi-event, dhcp-event, network-check, shutdown, media {…}, internal-media {…}, bench-storage, space, bench-library, collect.

Callers on the player: Reborn crates/reborn-platform/src/client.rs (/usr/sbin/y2-platform with capabilities, codec-settings, export-diagnostics, export-state, health, internal-media setup, media repair, report {enable,disable,new-id,save,send,status}, sleep request, status --reborn --pss, status wifi, time-set, update {apply,cancel,check,rollback}); storage.rs → /usr/sbin/y2-media; power.rs. Overlay scripts (buildroot/board/y2/production-overlay): etc/init.d/S02y2-media, S03y2-platform, S03y2-time, S05y2-cpu-idle, usr/libexec/y2/{platform-supervise,connectivity,wpa-event,dhcp-event,ntp-event}, usr/sbin/{y2-media,y2-radio,y2-suspend}. initramfs: /sbin/y2-platform-start and the platform contract marker /etc/y2linux/platform-contract = y2-platform-v1.

State contract (files other parts read; keep paths and shapes): /run/y2/*.json (power.json, network.json, media.json, media-mount.json, storage.json, wifi-events.json, application-ready.json, boot-stages.jsonl, …), /data/system/platform/* (boot.json, power-journal.jsonl, previous-boot-evidence.json, sd-admissions.json, sleep-diagnostics.jsonl, battery-ui.json, wifi-journal.jsonl, reports/…), /data/updates/* (state.json, …), pmsg/kmsg trail lines (shutdown begin …, platform …), versions platform_api_version 1, feature_contract_version 2, capabilities.json. The error-report scrubber/uploader, the report Worker and the review tooling (tools/reports, UserReports/REVIEWED.md analyses) parse these: field names are an API.

Python elsewhere on the player: none found besides the platform (grep -rlI '^#!.*python' in the target finds only y2-power-audit). Buildroot: BR2_PACKAGE_PYTHON3=y (+ _PY_ONLY, _SQLITE, _SSL, _ZLIB) in buildroot/configs/y2_production_defconfig. Verify nothing else selects it before removal (§5 phase 6).

Tests: 46 test modules import y2_platform (e.g. test_platform_power_contract, test_battery_soc, test_product_auto_sleep, test_platform_update, test_error_reports, test_connectivity_supervisor, test_wifi_failure_evidence, test_platform_evidence, test_sleep_product, test_internal_media, test_platform_storage_tools, …; ≈28 700 lines of Python tests overall). Most build a fake root tree and call functions through ctx (common.py: ctx.path, ctx.read, ctx.json, ctx.command, atomic JSON writes).

3. Principles

  1. Strangler, not big bang. One Rust multicall binary becomes /usr/sbin/y2-platform early. Subcommands it

    implements run in Rust; the rest exec the Python entry (python3 -I /usr/lib/y2-platform/main.py …) with the same arguments. Move command by command, daemon by daemon. Python is removed only in the last phase.

  2. The contract does not change. Same CLI arguments, exit codes, stdout JSON, file paths, file shapes (field

    names, types, enum strings), timeouts, lock files and lock protocol (flock on the same paths), atomic-write semantics (temp + fsync + rename, no randomness at boot: see test_platform_records_do_not_need_the_random_pool), pmsg/kmsg line texts. Reborn and the report tooling must notice nothing. Contract changes, if ever wanted, are a separate step after parity. This includes known defects (owner, 2026-10-28: "port 1:1 then fix"): the suspected defects in CONTRACT.md are ported as they are, marked with a // DEFECT (CONTRACT.md): … comment, and fixed in Rust after Phase 6, when no Python is left in the image.

  3. Python and Rust coexist safely. During the transition a Python daemon and a Rust CLI may touch the same state:

    identical locking and write order are mandatory, not optional.

  4. Port behaviour, including the fixes. Every F-number fix in the Python code (comments name them: F13, F23,

    F34, F36, F38, F39, F46–F57 …) is behaviour to keep. Port the comment with the code. Read the ledger row (UserReports/REVIEWED.md, private) when a comment names one.

  5. Differential testing is the safety net. For each ported piece, run Python and Rust on the same fake root

    trees and compare outputs and resulting files exactly (normalise only timestamps that are inputs).

  6. Hardware gates. Power, sleep, battery, charging, update and shutdown pieces each need a test build on the

    owner's player and an overnight run before the next phase (AGENTS.md: broader qualification for architecture changes; repeat the roadmap gap audit at milestone boundaries).

  7. Small and boring Rust. No async runtime unless a daemon really needs it (the Python daemons are loops with

    sleeps and subprocesses; mirror that with threads/poll). Dependencies: serde, serde_json, libc (or rustix), maybe rusqlite (only if a ported command needs SQLite). Keep using the curl binary for HTTPS as Reborn does (radio.rs, lastfm.rs): no TLS stack in the binary. Release profile opt-level = "s", LTO, panic = "abort" only if no code relies on unwinding.

4. Layout and build

5. Phases

Each phase ends with: host tests green (tools/development/host-tests.sh), Reborn tests green, a test build (release.py build --label N, see docs/BUILDING.md §7) installed on the owner's player by staged OTA and Acknowledged, the phase's device checks, a handoff entry. Publish only when the owner says so.

Phase 0 — Baseline and contract inventory (no product change)

Phase 1 — Skeleton and dispatcher

Phase 2 — Hot short-lived commands (largest CPU win, low risk)

Phase 3 — Small daemons and remaining simple commands

Phase 4 — Service daemon, media, update, reports

Phase 5 — Power daemon (last, most sensitive)

Phase 6 — Remove Python

5a. Remaining port groups (from 2026-10-28, owner: "continue till all phases are done")

Phases 2–5 are carried out as eight groups that can be ported side by side, each in its own module platform/crates/y2-platform/src/commands/<group>/ (with its own route, already chained in commands/mod.rs) and its own differential cases tests/platform_cases/<group>.py (CASES, fixture(root)), so they never edit the same lines. Device checks follow per test build; power and sleep keep their overnight gates.

GroupSubcommands / programsPython
observationstatus (all sections, --reborn, --pss, --pid, --interval), health, boot-evidence, network-check, collect, bench-storage, bench-libraryobserve, health, boot (capture_previous), boot_retained, network_check, collect, bench, workload
connectivityconnectivity-daemon, wifi-power-policy, bluetooth-powerconnectivity, network (rest), radio_policy.wifi_policy, bt_control, bluetooth
devicesssh-daemon, usb-mode, transfer, codec-settings, time-set, time-bootstrap, rtc-alarmssh, usb, transfer, codec_controls, timekeeping (rest)
storagemedia (all actions), internal-media, space, /usr/sbin/y2-mediamedia, internal_media, space
serviceservice-daemon, update (all actions), firmwareservice, update, radiofw
reportsreport (all actions), export-diagnostics, export-state, resetreport, diagnostics, diagnostic_fields, maintenance
powerpower-daemon, shutdownpower, battery, charger_state, power_journal
suspendsleep request, sleep prepare, product-sleep.py, suspend-record.py, y2-suspend-record, y2-power-audit, y2-status, y2-health, y2-power-commandsleep, product_sleep, sleep_diagnostics, suspend_record, the scripts

Rules for every group (the phase 2 part 1 code is the pattern: commands/*.rs, reply.rs, y2p-core):

6. Risks and how to handle them

RiskHandling
Re-introducing fixed bugs (sleep, battery, charging, updates)F-comments ported with code; differential tests; power last; overnight gates
A field or reason string changes and Reborn/report tooling misreads ity2p-contract shared with Reborn; golden JSON fixtures from the Python side
Python daemon and Rust CLI race on shared filessame flock paths and write order; a test that runs both concurrently on one tree
Faster code exposes timing races (things that relied on 0.5 s startups)watch for it in device checks; the F39 shutdown and F47/F48 sleep paths are timing-sensitive
Update/recovery breakagePhase 4 tests full OTA + rollback on the player before publishing; the recovery image is C/shell and not touched
Slower debugging (no copying a .py onto the player)keep --root, verbose logging and the trail lines; host builds for quick repro
Binary sizeone multicall binary, opt-level = "s", LTO, curl subprocess instead of TLS crates

7. Success criteria (fill in Phase 0 and Phase 6)

MetricPhase 0 (Python)TargetPhase 6 (Rust)
Platform RSS (all daemons)58.4 MB RSS, 35.0 MB PSS (power 12.5, service 19.8, connectivity 14.9, ssh 11.2 MB RSS)< 15 MB15.2 MB RSS, 4.6 MB PSS (power 4.2, service 4.2, connectivity 3.4, ssh 3.3 MB RSS; 169)
Platform CPU, 1 h awake idle≈92 s/h (2.5 % of the one online core; power-daemon 60, service 24, ssh 7, connectivity 1)≥ 5× less19.6 s/h, 4.7× less, with Wi-Fi on (power 9.1, service 8.1, ssh 2.4, connectivity 0.2; 612 s on 169)
Platform CPU, playing≈109 s/h (power 62, service 38; Wi-Fi on in this window)≥ 5× less
y2-platform status --reborn --pss latency1.49 s wall, 1.37 s CPU (a Reborn refresh with capabilities and report status: ≈3.0 s CPU)< 50 ms0.48 s wall, 0.32 s CPU, nearly all in Reborn's rebornctl metrics --json (≈0.36 s CPU on its own); capabilities 16 ms, report status 14 ms
Root image size130 MB tree, Python 21.3 MB (stdlib 16.1, libpython 5.1) + platform 0.7 MB−22 MB or more111 MB tree (−19 MB; the binary is part of it); OTA root 50.3 MB instead of 56.5 MB
Awake idle currentnot measurable on the charger (no current_now; the player only runs unplugged when asleep)lower
Overnight drain≈2 mA (161, estimate); 165 overnight owed (owner unplugs)not worseowed (owner, two nights on 169)
Host tests / container suitehost 967 tests OK (2 skips incl. the Rust comparison); container not rungreenhost suite OK; release build 169's container suite, package and first-install validation, ELF and installed-ARM checks pass

Phase 0 measurements (owner's player, test build 165, 2026-10-28)

On the USB charger, screen off, one CPU online. Per-process utime+stime and RSS/PSS from /proc every 60 s (/data/logs/phase0/{idle,play}.log on the player). The owner stopped the idle run early: awake idle 551 s (uptime 2759–3310 s, Wi-Fi off, Bluetooth off); its rates equal the since-boot averages (power-daemon 54.9 s in 55 min). Playing 614 s (MP3 320k, 10-band EQ + limiter, wired headphones, Wi-Fi on and online). Python platform vs the rest, CPU seconds per hour:

ProcessIdle s/hPlaying s/hRSS MBPSS MB
power-daemon (Python)59.962.212.57.4
service-daemon (Python)23.537.619.813.1
ssh-daemon (Python)7.27.411.26.3
connectivity-daemon (Python)1.31.414.98.2
platform total91.8108.658.435.0
reborn43.5744 (20.7 % CPU)26.9 / 33.724.7 / 31.0
everything else (observer, dbus, bluetoothd, wpa, bluealsa, bt-reconnect, umtprd)6.27.424.310.8

System busy: 3.0 % idle, 6.9 % playing. With the screen off Reborn does not poll the platform, so these are the daemons' own loops; every platform screen Reborn shows adds ≈3 s CPU per refresh (below).

CLI latency (10 runs each, median wall / CPU, while nothing else ran). Read-only commands for real; state-changing ones only as a floor (<command> --help, and import of the module):

CallWallCPU
python3 -I -c pass0.1520.150
status --reborn --pss (Reborn refresh)1.4921.366
capabilities (Reborn refresh)0.5470.540
report status (Reborn refresh)1.0070.993
health1.6171.500
status wifi1.5841.476
update status0.8710.867
media status / internal-media status / firmware status0.719 / 0.599 / 0.566≈ wall
sleep status / time / codec-runtime0.564 / 0.572 / 0.541≈ wall
floor for any state-changing call (--help)≈0.84≈0.83
import cli / update / report / maintenance / network0.43 / 0.77 / 0.84 / 0.81 / 0.47≈ wall

status itself starts 4 helpers per call (/bin/cat /sys/power/wakeup_count, y2-bt-observe, ip -4 -o …, rebornctl metrics --json); importing report.py alone costs 0.84 s, so report status is 1 s for a few file reads. The shell-called hot paths pay ≥0.54 s each: boot-stage (0.84 floor, twice per shutdown and three times per boot), time-remember, wifi-event/dhcp-event/ntp-event.

Boot stages (both are first boots after an update, 165's and 8.43's, and agree within 0.1 s; pmsg trail): rc S03y2-platform 12.09 s → platform_start 12.50 → services_started 27.74 → application_ready 48.61. A plain restart (measured on 166, whose dispatcher still runs all of this in Python, so the same baseline): platform_start 8.72 → services_started 23.75 → application_ready 44.44 s; shutdown of the previous boot shutdown_pending 4042.50 → shutdown_complete 4044.53 (2.03 s). Earlier boots keep no stage times (boot.json history has none).

8. After Phase 6

9. Out of scope / later

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