[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-80549":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":9,"language":10,"languages":9,"totalLinesOfCode":9,"stars":11,"forks":12,"watchers":13,"openIssues":12,"contributorsCount":14,"subscribersCount":14,"size":14,"stars1d":14,"stars7d":14,"stars30d":14,"stars90d":14,"forks30d":14,"starsTrendScore":14,"compositeScore":15,"rankGlobal":9,"rankLanguage":9,"license":16,"archived":17,"fork":17,"defaultBranch":18,"hasWiki":17,"hasPages":17,"topics":19,"createdAt":9,"pushedAt":9,"updatedAt":20,"readmeContent":21,"aiSummary":22,"trendingCount":14,"starSnapshotCount":14,"syncStatus":23,"lastSyncTime":24,"discoverSource":25},80549,"picokubelet","cedi\u002Fpicokubelet","cedi","A Rust kubelet that runs on a microcontroller and registers with a real Kubernetes cluster as a worker node",null,"Rust",51,1,53,0,40.9,"Apache License 2.0",false,"main",[],"2026-06-12 04:01:29","[![PR](https:\u002F\u002Fgithub.com\u002Fcedi\u002Fpicokubelet\u002Factions\u002Fworkflows\u002Fpr.yml\u002Fbadge.svg)](https:\u002F\u002Fgithub.com\u002Fcedi\u002Fpicokubelet\u002Factions\u002Fworkflows\u002Fpr.yml)\n[![Release](https:\u002F\u002Fgithub.com\u002Fcedi\u002Fpicokubelet\u002Factions\u002Fworkflows\u002Frelease.yml\u002Fbadge.svg)](https:\u002F\u002Fgithub.com\u002Fcedi\u002Fpicokubelet\u002Factions\u002Fworkflows\u002Frelease.yml)\n\n# picokubelet\n\n```bash\n$ kubectl get nodes -owide\nNAME                   STATUS   ROLES                  AGE     VERSION               INTERNAL-IP     OS-IMAGE                         KERNEL-VERSION         CONTAINER-RUNTIME\nk3s-server             Ready    control-plane,master   546d    v1.31.1+k3s1          192.168.0.103   Debian GNU\u002FLinux 12 (bookworm)   6.12.34+rpt-rpi-2712   containerd:\u002F\u002F1.7.21-k3s2\nclusterpi-worker1      Ready    \u003Cnone>                 546d    v1.31.1+k3s1          192.168.0.188   Debian GNU\u002FLinux 12 (bookworm)   6.12.34+rpt-rpi-v8     containerd:\u002F\u002F1.7.21-k3s2\nesp-node-01-guenther   Ready    \u003Cnone>                 4m53s   v1.31.1-picokubelet   192.168.0.111   picokubelet on bare metal        esp-rs-no_std          lies:\u002F\u002F0.1.0\n```\n\n[![asciicast](https:\u002F\u002Fasciinema.org\u002Fa\u002F1004944.svg)](https:\u002F\u002Fasciinema.org\u002Fa\u002F1004944)\n\n\u003Cp align=\"center\">\n    \u003Cimg alt=\"kubectl describe node output\" height=\"400\" src=\"out.png\">\n\u003C\u002Fp>\n\n## What this is\n\n`picokubelet` is a Kubernetes kubelet, written in Rust, targeting the ESP32-S3. It boots, gets a DHCP lease over Wi-Fi, talks TLS to a real k3s API server, registers itself as a node, and renews its lease so the control plane keeps believing it. As far as the cluster is concerned, it is a worker.\n\nThe hardware is a [Waveshare ESP32-S3-ETH]: an ESP32-S3R8 with a W5500 Ethernet controller on SPI, optionally PoE-powered. The control plane is a k3s instance on a Raspberry Pi. Nothing about that combination is unusual on its own; the unusual part is what's at the other end of the SPI bus.\n\nIt exists because nobody had told the ESP32 it couldn't.\n\n[Waveshare ESP32-S3-ETH]: https:\u002F\u002Fwww.waveshare.com\u002Fesp32-s3-eth.htm\n\n## What works and what's faked\n\nReal:\n\n- Wi-Fi association, DHCP, and TLS to a real k3s API server. (PoE+Ethernet via the on-board W5500 is the intended production form factor; the code path isn't there yet, so dev currently runs over Wi-Fi.)\n- Wall-clock anchoring from the `Date` header on the first `\u002Fversion` response. The board has no RTC, only vibes.\n- `Node` registration via `POST \u002Fapi\u002Fv1\u002Fnodes`, with capacity advertised honestly: `cpu: 240m`, `memory: 320Ki`, arch `xtensa-lx7`, OS `no_std`.\n- Lease creation in `kube-node-lease`, plus renewal at a 10s cadence with a 40s lease duration.\n- `\u002Fstatus` subresource PATCH on a 5min cadence, or immediately whenever any tracked condition flips.\n- `MemoryPressure` driven by actual ESP heap free bytes; flips True below 20KB free.\n- `lastTransitionTime` semantics: only updated when a condition's `status` field actually flips, not on every heartbeat. Conflating the two is a real-kubelet anti-pattern that makes nodes look flappy in monitoring.\n- A `node.specht.dev\u002Fheap-bytes-free` annotation on the main resource that updates with each status push.\n- Five custom joke-but-honest conditions (`Vibes`, `Caffeinated`, `Existential`, `Peckish`, `Haunted`) derived from real state (heap %, uptime, lifetime renewal count, Wi-Fi reconnects, BSSID changes, wall-vs-monotonic skew) and surfaced through `kubectl describe node`.\n- WS2812 status LED on GPIO 21, driven via RMT from a dedicated task, with `SelfTest`, `Booting`, `Connecting`, `Healthy`, and `Activity` patterns.\n\nFaked, by design:\n\n- Pods. The kubelet does not yet watch `\u002Fapi\u002Fv1\u002Fpods`. Promtail has scheduled itself onto the node and sits in `Pending`. That's the next phase of work.\n- Volumes, `exec`, logs, `kubectl exec`, container runtime. None of these exist. The container runtime version is reported as `lies:\u002F\u002F0.1.0`.\n- `PIDPressure` and `DiskPressure` are always reported as False. There are no PIDs, and there is no disk.\n\nOn the roadmap (in roughly this order):\n\n- W5500 Ethernet swap. The board hardware is ready; the code path isn't.\n- Pod theater: watching `\u002Fapi\u002Fv1\u002Fpods?fieldSelector=spec.nodeName=…`, accepting scheduled work, and walking pods through their status transitions on a timer without ever running anything.\n- OTA via OCI artifacts. The recursive joke being that the cluster deploys its own workers.\n- The eight-node Tamagotchi rack. Boards on order.\n\n## What it survived\n\nAt time of writing, `esp-node-01-guenther` has been `Ready` for \u003C!-- TODO: actual count at publish time --> consecutive lease renewals, including survival of a real `BeaconTimeout` event with RSSI -90 dBm. The reconciler architecture recovered without intervention; the lease counter continued unbroken through the reconnection.\n\n- Wi-Fi disconnect and re-association: handled\n- TLS connection reset mid-request: handled\n- Routes vanishing while reconcilers fire: handled\n\n## Hardware\n\n- [Waveshare ESP32-S3-ETH](https:\u002F\u002Fwww.waveshare.com\u002Fesp32-s3-eth.htm): ESP32-S3R8 plus a W5500, with an optional PoE module.\n- A Raspberry Pi running k3s for the control plane. Any k3s install will do; the node doesn't care.\n\n## Architecture\n\nThe ESP boots, brings up Wi-Fi via `esp-radio`, and lets `embassy-net` handle DHCP. Once it has an IP, `kubelet::bootstrap` walks through anchoring the wall clock from the `\u002Fversion` `Date` header, registering the Node, creating the Lease, and pushing an initial status PATCH so every condition has a fresh `lastHeartbeatTime` out of the gate. The API server address, bearer token, and Wi-Fi credentials are baked in at compile time via `env!`, sourced from a `.env` file loaded by `mise`.\n\nAfter bootstrap, two `embassy` tasks run the syncloop:\n\n- `reconcilers::lease` PATCHes `kube-node-lease\u002F\u003Cname>` every 10s (against a 40s lease duration), recreates the Lease on 404, and drives the LED.\n- `reconcilers::status` PATCHes `\u002Fapi\u002Fv1\u002Fnodes\u002F\u003Cname>\u002Fstatus` every 5min (or immediately on any condition flip) and then PATCHes the heap-free annotation on the main resource.\n\nBoth tasks share a single `ApiClient` wrapped in an `embassy_sync::Mutex`, acquired briefly per request. The LED runs as its own independent task and listens on an `embassy_sync::Signal`, so it stays responsive even when the network code is mid-handshake. There is no CRI, no runtime, no network namespace; just JSON patches saying yes, this node is `Ready`, why do you ask.\n\nModule map:\n\n- `kubelet`: `NodeIdentity` and the bootstrap sequence (anchor clock → register node → create lease → push initial status).\n- `k8s::{conditions, models}`: per-condition tracker state, plus typed Kubernetes resource builders that own their own JSON serialisation (no `serde`; the bodies are short and fixed-shape).\n- `net::{wifi, http, client}`: Wi-Fi controller task, the parsed HTTP response shape, and the TLS+HTTP `ApiClient` built on `embedded-tls`.\n- `reconcilers::{lease, status}`: the two syncloop tasks.\n- `led`: the WS2812 driver task.\n\nThe whole thing is built on `embassy` for async, `embassy-net` for the network stack, `esp-radio` for Wi-Fi, `embedded-tls` for TLS, and `esp-hal` for the chip.\n\nThis is shaped like a real kubelet's syncloop, smaller.\n\n## Status LED\n\nThe Waveshare board has a single onboard WS2812 on GPIO 21. Firmware drives it from a dedicated embassy task over RMT channel 0, so the LED keeps animating even when the kubelet is mid-TLS handshake. Brightness is capped at ~15%; full power is genuinely painful indoors.\n\n| Pattern                               | State                                                                                                     |\n| ------------------------------------- | --------------------------------------------------------------------------------------------------------- |\n| Red → green → blue → off, 200 ms each | Self-test at boot; confirms the LED is alive before anything else runs.                                   |\n| Solid dim white                       | Booting. Set after self-test, before network init.                                                        |\n| Blue, ~1.5 Hz breathe                 | Connecting. Covers Wi-Fi association, DHCP, TLS handshake, node registration, and initial Lease creation. |\n| Green, ~0.33 Hz breathe               | Healthy. Set after the _first_ successful Lease renewal; Lease creation alone isn't enough.               |\n| Yellow flash, 100 ms                  | Lease renewal heartbeat, every ~10 s, overlaid on the green breathe.                                      |\n\n`Warning`, `Disconnected`, and `Panic` exist as enum variants but currently render to off. They're reserved for phases when error handling is built out enough to drive them honestly; an LED that lies under stress is worse than one that goes dark.\n\n## Building and flashing\n\nYou will need the Espressif Rust toolchain. The repo uses `mise` to pin everything (`espup`, `espflash`, the Xtensa-aware Rust toolchain) so you don't have to reason about it.\n\n```sh\nmise install\ncp .env.example .env   # set K3S_API_HOST, K3S_API_PORT, K3S_TOKEN, WIFI_SSID, WIFI_PSK, NODE_NAME\ncargo build --release\nespflash flash --monitor target\u002Fxtensa-esp32s3-none-elf\u002Frelease\u002Fpicokubelet\n```\n\nThe dev loop currently runs over Wi-Fi; PoE+Ethernet via the on-board W5500 is the intended production setup but the code path isn't there yet. If you're new to Rust on Espressif chips, the [esp-rs book](https:\u002F\u002Fdocs.espressif.com\u002Fprojects\u002Frust\u002Fbook\u002F) is the right starting point. The Xtensa toolchain situation is what it is; `espup` makes it bearable.\n\n## FAQ\n\n**Should I use this in production?**\nNo.\n\n**Why?**\nIt seemed like the obvious next step.\n\n**Why Rust?**\nThe embedded Rust ecosystem on ESP32 (`esp-hal`, `embassy`, the `embedded-hal` traits) is currently the most pleasant way to write firmware. C would also work, in the same sense that you could also walk to the moon.\n\n**Does it run Doom?**\nNo, but a pod scheduled to it can claim to.\n\n## Acknowledgements\n\nThis wouldn't exist without the [esp-rs](https:\u002F\u002Fgithub.com\u002Fesp-rs) working group, the [Embassy](https:\u002F\u002Fembassy.dev\u002F) project, and the maintainers of `embassy-net-wiznet`. All the actually-hard work (async on `no_std`, a TCP\u002FIP stack that fits, a W5500 driver that doesn't lie about its DMA) is theirs. The novelty here is just pointing it at a Kubernetes cluster.\n\nA [Specht Labs](https:\u002F\u002Fspecht-labs.de) project.\n\n## License\n\nLicensed under either of [MIT](LICENSE-MIT) or [Apache 2.0](LICENSE-APACHE), at your option.\n","picokubelet 是一个用 Rust 编写的微型 Kubernetes kubelet，可以在微控制器上运行并注册为真实 Kubernetes 集群的工作节点。其核心功能包括通过 Wi-Fi 连接、获取 DHCP 租约、与 k3s API 服务器进行 TLS 通信，并以 `xtensa-lx7` 架构和 `no_std` 操作系统注册自身。该项目还实现了节点租约的创建与续订、状态更新以及基于实际内存使用情况的压力监测。适用于需要将资源受限设备（如 ESP32-S3 微控制器）集成到 Kubernetes 环境中的场景，特别是物联网或边缘计算领域。",2,"2026-06-11 04:01:10","CREATED_QUERY"]