From 1febba516a0435015bbcb4510d1321c3a466d930 Mon Sep 17 00:00:00 2001 From: Lance Albertson Date: Sat, 25 Jul 2026 16:28:01 -0700 Subject: [PATCH 01/16] docs: add English documentation for the PROC-V1 firmware Everything upstream is written in Chinese, the sketch is a single 1350-line file with no headers, and the vendor manual no longer matches the code. This adds the English working notes OSU OSL needs to maintain and improve the firmware on the boards it runs. AGENTS.md covers: - a verified build: FQBN, libraries, and the fact that the sketch uses 29062 of 30720 bytes, leaving ~1.6KB of headroom - bootloader and fuses, and why the crystal is unpopulated - a map of the sketch, the EEPROM layout, the menu, the script language, session handling and the watchdog tick - how the DS18B20 ROM code becomes the serial number, MAC address and default hostname - the full commit log, translated - a list of verified issues, each with file:line Findings worth calling out from that list: - menu option 'n' (change name) falls through into 'r' and fires a 300ms reset pulse at the managed host - the menu prints "V:Vout=" but only lowercase 'v' is handled - ds1820() reads addr[8] out of bounds on an 8-byte array - builds with -DPWM=5 do not compile at all (duplicate definition of pwm); confirmed with arduino-cli - the WATCHDOG* EEPROM slots are written but never read: the host watchdog they describe was never implemented doc/ carries a full English translation of the vendor software manual (node-926), annotated at every point where it no longer matches this code, including the escape sequence (six '+' and six 'U', not seven) and the autolink feature removed in 2024. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Lance Albertson --- AGENTS.md | 430 ++++++++++++++++++++++++++++++++ doc/node-926-software-manual.md | 228 +++++++++++++++++ 2 files changed, 658 insertions(+) create mode 100644 AGENTS.md create mode 100644 doc/node-926-software-manual.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a144fc7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,430 @@ +# AGENTS.md — `proc`: PROC-V1 production firmware + +**This is the repository to change if the task is "improve the PROC software".** + +Workspace overview: [../AGENTS.md](../AGENTS.md) · Hardware: [../prc/AGENTS.md](../prc/AGENTS.md) + +**Last verified:** 2026-07-25 against `a8f458f` (2024-11-03), including an actual +`arduino-cli` build. + +--- + +## 1. What this repo is + +The firmware that runs on shipped PROC-V1 boards. One 1350-line Arduino sketch +plus two shell scripts. Upstream `lshw/proc`, GPL-3.0, 57 commits from +2020-01-02 to 2024-11-03, sole author Liu Shiwei. + +Upstream README, translated: + +> **proc** — source for proc_v1 +> +> The IDE is Arduino; select model **Arduino Uno**. Connect the board's serial +> port to the computer and you can compile and download. +> +> To build from the command line: run `build.sh`, which uses `arduino-cli`. If +> `arduino-cli` is missing it downloads it; if libraries are missing the script +> downloads them. After compiling, the ROM lands in `prc.hex` in the same +> directory as `build.sh`. +> +> To upgrade from the command line: connect the device to a USB serial adapter. +> If you are upgrading from the managed machine itself, edit the script for the +> right serial device number, and under Linux make sure the serial `login` +> program has released the port. Run `update.sh`, then apply power to the device +> when the countdown reaches 1 and the upgrade begins. If it fails, adjust the +> timing of when you power the device and try again. + +Note the README says "Arduino Uno" but `build.sh` actually builds for +**Arduino Pro / Pro Mini, ATmega328P 3.3 V 8 MHz**. Trust `build.sh`. + +### Relationship to `../prc` + +`prc` is the *hardware* repo. Its `control/control.ino` is an abandoned +prototype; this repo's first commit (`fc48b63`, 2020-01-02) is that same file, +and the two still differ by only 11 lines. All firmware development happened +here. The commit IDs in the released `.hex` images on the vendor site +(`d58845a`, `4b34ae9`) exist in *this* history's lineage, not in `prc`. + +### Layout + +| Path | What | +|---|---| +| `prc/prc.ino` | The entire firmware. 1350 lines, single translation unit | +| `build.sh` | `arduino-cli` wrapper. **Writes to `$HOME`** — see §3 | +| `update.sh` | `avrdude` wrapper with a 3-2-1 countdown | +| `README.md` | 14 lines, Chinese, translated above | +| `LICENSE` | GPL-3.0 | +| [doc/node-926-software-manual.md](doc/node-926-software-manual.md) | **English translation of the vendor software manual** (https://bjlx.org.cn/node/926), annotated with every point where it no longer matches this code. Added by OSL, not upstream | + +Branches: `main` (default, current). `origin/public` is a stale 2021 divergence +— ignore it. Tags: `20230903-c0e3c26`, `20230907-3efd94e`, `v2024-11-03` +(the last points at `809b47f`, two commits behind `main`). + +--- + +## 2. Commit history, translated + +Newest first. This is the whole development arc of the product. + +| Date | Commit | English | +|---|---|---| +| 2024-11-03 | `a8f458f` | Add fqbn compile parameter to CXXFLAGS | +| 2024-11-03 | `d95065d` | Build script can auto-install the toolchain and libraries, then compile | +| 2024-11-03 | `809b47f` | Sessions still authenticating must be cleaned up on timeout | +| 2024-11-03 | `c843fe0` | Enter the menu from the managed machine with six `+` and six `u` then Enter | +| 2024-11-03 | `bad3f8d` | Exit pass-through when switching client | +| 2024-11-02 | `8ac424e` | Rewrite the accept path: up to 3 remote clients may authenticate at once | +| 2024-11-02 | `ba2b018` | Move auth to connection setup; an authenticated new connection kicks the old one | +| 2024-11-02 | `6dbb189` | **Remove the PWM and autolink code** | +| 2024-11-02 | `10f183e` | Clean up code | +| 2024-11-02 | `4a0f1a7` | `+++++` from the serial side returns to the menu without auth; re-login only after disconnect | +| 2023-09-26 | `f9f900f` | Add PWM | +| 2023-09-07 | `3efd94e` | update.sh | +| 2023-09-07 | `98f04de` | RC clock calibration can only be started from the network side | +| 2023-09-07 | `eb0a2c4` | astyle | +| 2023-09-07 | `1ea18f3` | build.sh astyle | +| 2023-09-03 | `c0e3c26` | bootloader | +| 2023-09-02 | `968d9d1` | Fixed: a 0.6 s stall during the 60-second temperature read | +| 2023-09-02 | `0c188f3` | In DHCP mode, print nothing to the serial port at boot | +| 2023-09-02 | `ca4f739` | MAC address OUI bit is 0 | +| 2023-09-02 | `a5cecf4` | Show build time | +| 2023-09-02 | `1f39e46` | Build script passes `git_ver` to the compiler with `-D` | +| 2021-02-01 | `dcf0a1c` | Change the calibration range to ±80, check for overflow | +| 2021-03-12 | `ca983d9` | In DHCP mode too, don't send anything to serial at boot — avoid disturbing the PC's boot | +| 2021-01-07 | `42480d7` | Widen the calibration range | +| 2020-05-23 | `e5477f4` | Add `#define PWM` to configure whether PWM exists; add `#define AUTOLINK`; change the serial menu entry to 7×`+` then 7×`U` | +| 2020-05-20 | `1b047a7` | Drop the EEPROM checksum; ask for confirmation on factory reset | +| 2020-05-19 | `8530414` | Reset the network chip at boot | +| 2020-05-19 | `24c3281` / `4d9410b` | A new telnet connection can kick an old one in "bye" state | +| 2020-05-19 | `30daf30` | Add the degrees-Celsius symbol | +| 2020-05-11 | `9d51903` | Handle serial data for at most 2 seconds, then go check the network | +| 2020-04-25 | `d6b3875` | Clean up, tidy the display format | +| 2020-04-25 | `88de6f6` | Add scripting: 10 scripts, execution and configuration | +| 2020-04-25 | `604ffff` | Only set the output state once, when the countdown hits 0 | +| 2020-04-22 | `6e87179` | Add PWM test | +| 2020-02-27 | `0c37c0e` | Add a custom device name | +| 2020-02-27 | `ff16038` | Save initial values to EEPROM | +| 2020-02-27 | `dac1586` | Enlarge the buffer to 512 bytes | +| 2020-02-27 | `b2955c8` | A key press at boot can skip DHCP | +| 2020-02-27 | `58e2093` | Actively connect out to a remote server | +| 2020-02-27 | `0f6c29c` | Add active-outbound settings, usable when dhcp=y | +| 2020-01-21 | `4588c3a` | Firmware update script | +| 2020-01-20 | `472bc1e` | At most 11 temperature probes | +| 2020-01-19 | `7fd09ad` | Default 115200 bps; no password needed from the serial side | +| 2020-01-14 | `89882f7` | Exit quickly after TCP disconnect | +| 2020-01-11 | `24ba27e` | Rework the menu | +| 2020-01-09 | `c3188d0` | Change part of the watchdog setup | +| 2020-01-09 | `ba8750a` | Move directory | +| 2020-01-07 | `7690849` | Factory default 38400; add several calibrations for different serial speeds | +| 2020-01-05 | `62c5391` | ver 20200105 | +| 2020-01-05 | `4466ca0` | Basically complete, just the speed change left | +| 2020-01-03 | `2e27e55` | menu | +| 2021-03-12 | `9a924ed` | GPLv3 | +| 2020-01-02 | `fc48b63` | init | + +--- + +## 3. Building + +### The upstream way + +```bash +./build.sh # produces ./prc.hex +``` + +Be aware of what it does before you run it: `apt install` for `wget`/`git`, +downloads `arduino-cli` v1.0.4 into `~/bin`, `mkdir ~/Arduino`, `git clone`s +this repo into `~/Arduino/proc`, and installs libraries into +`~/Arduino/libraries`. Convenient on the author's machine, intrusive elsewhere. + +### Contained equivalent + +```bash +export ARDUINO_DIRECTORIES_DATA=/some/sandbox/data +export ARDUINO_DIRECTORIES_USER=/some/sandbox/user +arduino-cli core install arduino:avr +arduino-cli lib install OneWire Ethernet3 + +FQBN="arduino:avr:pro:cpu=8MHzatmega328" +VER="$(git log -1 --date=short --format=%ad | tr -d -)-$(git rev-parse --short=7 HEAD)" +arduino-cli compile --fqbn "$FQBN" \ + --build-property build.extra_flags="-DGIT_VER=\"$VER\" -DBUILD_SET=\"$FQBN\"" \ + prc +``` + +`arduino-cli` is not packaged in Debian; it is a GitHub release tarball. +Everything else the build needs (`avr-gcc`, `avrdude`, `astyle`) is installed. + +### Facts + +- **FQBN `arduino:avr:pro:cpu=8MHzatmega328`** — "Arduino Pro or Pro Mini, + ATmega328P 3.3 V 8 MHz". The board carries a **328PB**, but nothing here + touches PB-only peripherals, so the stock 328P core is used. The alternative + `m328pb:avr:atmega328pbic` (from [../ATmega328PB](../ATmega328PB/AGENTS.md)) + is commented out in `build.sh` and is not what ships. +- **Libraries: `OneWire`** (Paul Stoffregen) and **`Ethernet3`** (W5500). The + prototype in `../prc` used `Ethernet2` — do not confuse them. +- `GIT_VER` and `BUILD_SET` must come from the build. The `GIT_COMMIT_ID` + fallback at the top of `prc.ino` is vestigial and never referenced. +- **Verified build at `a8f458f`** (arduino:avr 1.8.8, Ethernet3 1.6.0, + OneWire 2.3.8): **29062 bytes flash (94% of 30720)**, 694 bytes globals + (33% of 2048), 1354 bytes left for locals. The stale comment at the bottom of + `build.sh` says 29078 — same ballpark, different library versions. +- **You have roughly 1.6 KB of flash headroom.** Check the `Sketch uses N bytes` + line on every build; anything over 30720 will not fit. +- Formatter: `astyle` with `../procV2/lib/formatter.conf` (2-space indent, + indented switches/cases, `pad-oper`, `pad-header`, `keep-one-line-statements`). + Do not reformat the file — it makes rebases painful. + +### Bootloader and fuses + +From the comment block at the top of `prc.ino`, translated: + +> The bootloader is based on "pro mini", with fuses H, L, E changed from +> FF, DA, FD to **C2, DA, FD** — from an external 8 MHz crystal to the internal +> 8 MHz RC. Write the Arduino IDE's ArduinoISP example into an Uno, then +> temporarily fit an 8 MHz crystal and wire `10 → reset` (the left pin of the +> "update" header), `11 → MOSI`, `12 → MISO`, `13 → CLK`, `GND → GND`, +> `VCC → 5Vin`. Choose programmer "Arduino as ISP", then Tools → Burn Bootloader. +> +> Compiling requires the OneWire library, maintainer Paul Stoffregen. + +(The label order "H, L, E" is wrong; the values make it clear it is L, H, E.) +lfuse `0xC2` = CKSEL 0010, internal 8 MHz RC, CKDIV8 off — which is why the +crystal footprint on the board is unpopulated and why this firmware has to +calibrate `OSCCAL`. hfuse `0xDA` = a 2 KB boot section, hence the 30720-byte +application limit. + +### Flashing + +```bash +./update.sh +# or directly: +avrdude -v -patmega328p -carduino -P/dev/ttyUSB0 -b57600 -D -Uflash:w:prc.hex:i +``` + +The bootloader window is short, so `update.sh` counts down 3-2-1 and you apply +power to the board as it reaches 1. If it fails, retry with different timing. + +For this to work the **CONN3 "Update" jumper must be closed** so the adapter's +RTS/DTR can pull the MCU's `/RESET` — see [../prc/AGENTS.md](../prc/AGENTS.md) +§6.4. Leave it open in normal service so the managed PC cannot reset the +controller. + +If flashing from the managed machine itself, stop whatever `getty` holds the +port first. + +--- + +## 4. Firmware reference + +All line numbers refer to `prc/prc.ino` at `a8f458f`. + +### 4.1 Structure + +There are no headers; it is one flat sketch. Rough map: + +| Lines | Area | +|---|---| +| 1–160 | Pin defines, EEPROM enum, globals, `eeprom_read/write` helpers | +| 163–222 | `setup()` — identity, EEPROM validation, serial, DHCP/static network | +| 223–247 | `magic()` — the serial escape-sequence state machine | +| 260–334 | `new_link()` — accept + authenticate up to 3 pending TCP clients | +| 336–359 | `loop()` | +| 361–404 | `com_shell()` — the serial pass-through | +| 406–459 | `ISR(WDT_vect)` 30 ms tick + `setup_watchdog()` | +| 462–604 | `menu()` | +| 605–708 | DS18B20 enumeration, conversion, display | +| 709–795 | `check_rom()` — EEPROM validation and factory defaults | +| 796–822 | `set_passwd()` | +| 825–916 | `rc_calibration()` | +| 918–1019 | `info()` / `save_set()` — network settings menu | +| 1020–1136 | Serial-parameter get/set | +| 1157–1227 | `getc_()`, `hello()` banner, string entry | +| 1228–1349 | Script engine: `run_script()`, `modi_script()`, `disp_script()` | + +### 4.2 EEPROM layout + +All accesses go through `eeprom_read()`/`eeprom_write()`, which add +`EEPROM_OFFSET = 12` — so index 0 below is physical EEPROM address 12. +`eeprom_write()` skips writes when the value is unchanged (wear reduction). + +| Symbol | Bytes | Contents | +|---|---|---| +| `CAL38400`, `CAL57600`, `CAL115200`, `CAL230400` | 4 | Per-baud `OSCCAL` calibration values | +| `VOUT_SET` | 1 | Saved VOUT state, restored at boot | +| `MAC0..MAC5` | 6 | MAC. `MAC0..2 == DC AD BE` doubles as the "settings are valid" magic | +| `IS_DHCP` | 1 | `'Y'` / `'N'` | +| `IP0..3`, `NETMARK0..3`, `GW0..3` | 12 | Static network config | +| `SPEED0..3` | 4 | Baud rate, big-endian u32 | +| `DATA_LEN`, `DATA_PARI`, `STOP_LEN` | 3 | `'5'`–`'8'`, `'N'/'O'/'E'`, `'1'/'2'` | +| `PASSWD0..3` | 4 | Numeric password, big-endian u32. `0` = none | +| `SN0..SN8` | 9 | DS18B20 ROM code = device serial number | +| `NAME0..NAME10` | 11 | Device name, NUL-terminated | +| `WATCHDOG0..10`, `WATCHDOG_EN` | 12 | **Defaults are written; nothing ever reads them.** Unfinished | +| `PWM_NOW` | 1 | Last PWM value | +| `ROMCRC` | 1 | End of the checked region; the CRC itself was dropped in 2020 (`1b047a7`) | +| `REMOTE_CYCLE`, `REMOTE_PORT_H/L`, `REMOTE_HOST` | 4+ | **Dead** since `6dbb189` | +| scripts | 10 × 50 | From `SCRIPT_ADDR = ROMLEN + 2`; script *n* at `+50n`. Slot 0 exists but only 1–9 are reachable | + +Factory defaults (`check_rom()`): IP `192.168.1.2`, mask `255.255.255.0`, +gateway `192.168.1.1`, DHCP off, 115200 8N1, no password, name `PROC`, +VOUT on, PWM 128, scripts cleared. + +### 4.3 Identity — the DS18B20 is the serial number + +On first boot the firmware enumerates the 1-Wire bus; if exactly one probe is +present its 64-bit ROM code becomes the device serial number (`SN0..SN7`), the +MAC becomes `DC:AD:BE:::`, and the default name becomes +`PROC` + those three bytes in hex. The on-board DS18B20 (D3 on the PCB) is +therefore what makes each unit unique. See §5 issues 5 and 6 for what goes +wrong when it is absent, and issue 15 for the OUI problem. + +### 4.4 RC-oscillator calibration + +The MCU runs from the internal 8 MHz RC oscillator (±10% from the factory), +which is not accurate enough for 115200 baud. The firmware stores a separate +`OSCCAL` trim per baud rate and applies it at boot. + +Menu key `z`/`Z` runs `rc_calibration()` — **network logins only, and only +within the first 200 s of uptime** (line 566). It sweeps `OSCCAL` over ±80 while +you hold `U` on the serial console, finds the range where the character still +decodes, takes the midpoint, and stores it. + +Practical consequence: **switching to a baud rate that was never calibrated can +leave the serial link unreadable until you run `z` again.** + +### 4.5 Menu + +``` +====== +0:com shell (network sessions only) +r:reset (300ms) +R:reset (5 sec) +p:powerdown(300ms) +P:powerdown(5 sec) +V:Vout= ON <- shown as 'V', but only lowercase 'v' works (issue 3) +<,.> :PWM=128 (PWM builds only, which do not compile — issue 1) +===script 1-9==== +... +===set=== +a:reboot +b:restore default set +c:network info & modi +d:com set +e:setpasswd +f:modi script 1-9 +n:change name:PROC1A2B3C <- also fires a 300 ms PC reset (issue 2) +q:quit offline +``` + +Hidden: `z`/`Z` = RC calibration. Idle timeout 20 s per keystroke (`getc_()`), +then the session drops. + +Connect banner (`hello()`, values illustrative): + +``` +#DOC HTTPS://bjlx.org.cn/node/914 +#Ver:PROC-V1-20241103-a8f458f +#Buile Set:'arduino:avr:pro:cpu=8MHzatmega328' <- "Buile" is an upstream typo +#Build Time:2024-11-03 10:22:31 +#name:PROC1A2B3C +#SN:28FF641E8016034A +#com:115200,8N1 +#C1A2B3=23.50℃ +``` + +### 4.6 Script language + +Menu `f`, then pick 1–9. Max 50 characters per script. From the +[vendor manual](doc/node-926-software-manual.md) cross-checked against +`run_script()`: + +| Cmd | Meaning | +|---|---| +| `P` | Press power. Optional following number (1–65536) = hold in ms. **Non-blocking** | +| `p` | Release power | +| `R` | Press reset. Optional number = hold in ms. **Non-blocking** | +| `r` | Release reset | +| `V` | Turn the 5–28 V output on. With a number, `analogWrite()` that value | +| `v` | Turn the output off | +| `T`/`t` | Followed by a number: wait that many ms. **Blocking** | +| `M` | Documented as "set PWM 0–255" — **not implemented in `run_script()`** | + +### 4.7 Sessions, authentication, escape sequences + +- TCP port **23**. One active session plus up to **3** in the auth queue + (`clientn[3]`). +- Password is a decimal number in a `uint32_t`. Default `0`, so pressing Enter + logs you in. 20 s to enter it; wrong → 5 s penalty, then disconnect. +- A newly authenticated client **kicks the current one** + ("new client up, you are offline."). +- **Serial → menu:** six `+`, then six `u`/`U`, then Enter (lines 224–247). +- **Pass-through → menu:** at least five `+` immediately followed by Enter + (lines 380–388). +- While no client is connected, incoming serial data is **read and discarded** + (lines 355–358). There is no scrollback buffer. + +### 4.8 Timing / watchdog + +The watchdog runs as a 30 ms **interrupt** (`WDIE`, not `WDE`) and doubles as +the system tick: + +- `pc_reset_on` / `pc_power_on` are millisecond counters. Any code can write + `pc_reset_on = 300` and the ISR holds the line for 300 ms then releases it. + This is how non-blocking script commands work. +- `dogcount` is cleared by the main loop; at 100 s the ISR does `jmp 0`. +- Temperature is re-read every 60 s (`timer1`). + +--- + +## 5. Verified issues and improvement targets + +Read by inspection at `a8f458f`; items marked **verified by build** were +reproduced with `arduino-cli`. + +| # | Severity | Issue | +|---|---|---| +| 1 | **High** | **PWM builds do not compile.** `uint8_t pwm;` (line 28, inside `#ifdef PWM`) and `uint8_t pwm = 128;` (line 461) are both namespace-scope definitions. **Verified by build:** `-DPWM=5` fails with `error: redefinition of 'uint8_t pwm'`. Dead since the 2024 cleanup | +| 2 | **High** | **Renaming the device presses the PC's reset button.** `case 'n'`/`'N'` (lines 532–535) has no `break` and falls into `case 'r'` (line 536), which sets `pc_reset_on = 300` | +| 3 | Medium | **Menu advertises `V` but only `v` works.** Menu prints `"V:Vout= "` (line 488); the only handler is `case 'v'` (line 548). The `case 'V'` at line 1266 is in `run_script()`, not the menu | +| 4 | Medium | **1-Wire CRC check reads out of bounds.** `OneWire::crc8(addr, 8) != addr[8]` (line 664) where `addr = ds_addr[n]` and `ds_addr` is `[11][8]` — `addr[8]` is the first byte of the *next* probe's ROM code. Should be `crc8(addr, 7) != addr[7]`. Healthy probes can be silently marked dead (`celsius[n] = -400*16`), or bad ones accepted | +| 5 | Medium | **A unit with no valid DS18B20 factory-resets on every boot.** `check_rom()` sets `sets[MAC2] = 0` when no valid ROM ID is available (line 732), but the "settings valid" early exit requires `MAC2 == 0xBE` (line 716). Every boot then rewrites IP, password, baud and scripts back to defaults. Production boards have D3 fitted so this normally does not fire — but a failed probe turns the unit into a factory-reset-on-boot device | +| 6 | Low | **`if (ds_addr[0] != 0)` (line 739) is always true** — it compares an array to 0. The `else` branch assigning the fallback MAC `…:00:01:25` is dead code | +| 7 | **Design** | **Auth is a numeric PIN over cleartext telnet, tested on every keystroke** (lines 280–296). Default `0`. A password that is a numeric prefix of what you type matches early; the accumulator can overflow `uint32_t`; the only penalty is 5 s and there is no source filtering. Not meaningfully fixable on an ATmega328 — treat these as management-VLAN-only appliances behind a jump host | +| 8 | Medium | **`com_shell()` drops every byte ≥ 0xF4 network→serial** (line 379), a crude telnet-IAC filter. Binary transfers toward the host (XMODEM/Kermit, pasted binary) corrupt silently | +| 9 | Medium | **Escape sequences no longer match the published manual.** Serial→menu is 6 `+` then 6 `u`/`U` then Enter; the manual says 7 and 7, and a 7th `+` permanently breaks the match until a non-matching character resets `magic_flag`. Pass-through→menu needs ≥5 `+` immediately before Enter, while the firmware's own banner says `+++` (line 368) | +| 10 | Medium | **Autolink (dial-out) removed in 2024** by `6dbb189`. `REMOTE_CYCLE` / `REMOTE_PORT_H/L` / `REMOTE_HOST` still occupy EEPROM (lines 133–136, 787–790); nothing reads them, no menu entry exists, and the vendor manual still documents the feature. **This is the code to restore if OSL needs to reach boards with no inbound route** — `git show 6dbb189` is the deleted implementation | +| 11 | **Opportunity** | **The `WATCHDOG*` EEPROM slots (lines 118–129, 779–786) are defaults-only.** The comments describe a recovery sequence (`r` reset, `w` wait, `P` power 5 s, `o`/`O` Vout off/on) that no code implements. For a colo fleet this is the highest-value feature to build: a board that power-cycles a wedged host by itself | +| 12 | Medium | **"Reboot" is `jmp 0`, not a reset.** `setup_watchdog()` enables `WDIE` but not `WDE` (lines 447–459); the 100 s timeout (lines 412–415) and menu `a` (lines 581–582) both jump to address 0 without resetting peripherals or the W5500. A wedged peripheral survives. Switching to a real WDT reset is a small, high-value patch | +| 13 | Low | **`../prc/README.md` claims 32 temperature probes; this firmware supports 10.** `celsius[11]` / `ds_addr[11][8]` with slot 0 reserved for the identity probe (lines 24, 58) | +| 14 | **Constraint** | **~1.6 KB of flash left** (29062/30720). RAM: 694 B globals plus a 512 B stack buffer in `com_shell()` (line 362) and a 256 B `oscs[]` in `rc_calibration()` (line 827) that is written but never read — 256 free bytes right there. `ds1820_disp()` pulls in floating-point `print`; fixed-point would free noticeably more | +| 15 | Low | **MAC OUI `DC:AD:BE` has the locally-administered bit clear**, claiming a globally-unique OUI the project does not own. `DE:AD:BE` would be correct. Commit `ca4f739` shows the author was aware of the OUI bit | + +### Suggested order of work + +1. Issues 2, 3, 4 — small, obviously correct, upstreamable as one series. +2. Issue 1 — fix or delete the `#ifdef PWM` path outright if OSL's boards have + no PWM pad. +3. Issue 12 — a real watchdog reset makes the fleet self-healing. +4. Issue 11 — the host watchdog. This is the feature that pays for itself. +5. Reclaim flash (issue 14) to pay for the above. +6. Decide the security posture (issue 7): network isolation now; `../procV2` or + a Linux-side front-end (`conserver`, `ser2net`) later. +7. Optional: restore autolink (issue 10) for hosts with no inbound route. + +--- + +## 6. Conventions + +- **Cite `file:line`.** One 1350-line file with no headers; line references are + how anyone else finds what you mean. +- **Do not reformat.** `astyle` with `../procV2/lib/formatter.conf`, and only on + lines you actually touch. +- **Check the flash number on every build.** Over 30720 bytes will not fit. +- **Do not change pin defines** without changing the hardware — the mapping is + fixed by the PCB. See [../prc/AGENTS.md](../prc/AGENTS.md) §6.2. +- **Write commit messages in English** in an OSL fork; add a Chinese subject + line if you intend to send the patch upstream. +- Upstream releases by tagging `YYYYMMDD-`; the newest tag is not + necessarily HEAD. diff --git a/doc/node-926-software-manual.md b/doc/node-926-software-manual.md new file mode 100644 index 0000000..24563af --- /dev/null +++ b/doc/node-926-software-manual.md @@ -0,0 +1,228 @@ +# PROC Software Instructions + +> **English translation of** https://bjlx.org.cn/node/926 +> Original title: **proc软件使用说明** +> Author: 刘世伟 (Liu Shiwei) · Published Sunday, 2020-04-26 19:48 +> Site: 北京龙芯&debian用户俱乐部 (Beijing Loongson & Debian User Club) +> +> Translated 2026-07-25. Editorial notes added by the translator are marked +> **[Note]** and are not part of the original. + +**Companion page:** [Hardware installation manual](../../prc/doc/node-914-hardware-manual.md) +(original: https://bjlx.org.cn/node/914) + +> ⚠ **This page documents the firmware as of 2020.** Several details no longer +> match upstream HEAD (`a8f458f`, 2024-11-03). Every divergence found is marked +> **[Note]** inline. The current behaviour is documented in +> [../AGENTS.md](../AGENTS.md). + +--- + +PROC can control a PC's reset button and power button over the network, for +remote power on/off and remote restart. It can also enter the serial port and +interact with the BIOS, the bootloader and the Linux console, to complete OS +installation, network configuration, fault recovery, remote maintenance and so +on. + +## Getting in + +PROC (PC remote operation controller) has a default IP of **192.168.1.2/24**. +For initial setup you can log in over the serial port, or configure your +computer onto that subnet and `telnet 192.168.1.2` to log in. + +**There is no password by default.** You can set one; the password applies only +to network logins. + +### Logging in over serial + +``` +minicom -D /dev/ttyS0 -b 115200 -R utf-8 +``` + +Then type **seven `+` and seven `U`** followed by Enter. If there is no +response, PROC may be busy doing DHCP — wait 30 seconds and type the seven `+` +and seven `U` and Enter again. + +> **[Note]** Upstream HEAD requires **six** `+` and **six** `u`/`U`, then Enter. +> Worse, typing a seventh `+` jams the matcher until a non-matching character +> resets it, so following this page literally will not work on current firmware. +> See [../AGENTS.md](../AGENTS.md) §5 issue 9. + +### Logging in over the network + +Set telnet to use character mode by default; in the default line mode nothing is +sent until you press Enter. + +Create `~/.telnetrc` containing the following three lines. The first line must +have no leading space; the other two must be indented: + +``` +default + mode character + set binary +``` + +Then: + +``` +telnet 192.168.1.2 +``` + +Besides telnet you can also use PuTTY to connect over the network. + +When logging in over serial you must first type `+++++++UUUUUUU` and Enter +before you get a response. This is to stop the serial output during PC boot from +interfering with the boot process. + +## The menu + +After logging in the main menu appears. Its contents are very simple: + +- **`0`** enters serial pass-through +- **`r`, `R`, `p`, `P`** — four keys controlling the PC's power switch and reset +- If the PWM option is fitted, **`<`, `,`, `.`, `>`** control the PWM output +- **`1`–`9`** are user-defined scripts. Script definition is documented on a + separate page … + +There are also functions to configure the network, set the password, configure +the serial port, edit scripts, restore factory settings, and reboot. + +> **[Note]** The menu also prints `V:Vout=` but only lowercase `v` toggles the +> output, and `n` (change name) also fires a 300 ms PC reset. See +> [../AGENTS.md](../AGENTS.md) §5 issues 2 and 3. Exiting pass-through back to +> the menu takes **five or more** `+` immediately followed by Enter, despite the +> firmware's own banner saying `+++`. + +## Active outbound mode (autolink) + +After setting the network to DHCP mode you can enable **active outbound mode** +to work around not having a public IP or a VPN. + +In active outbound mode you can configure a remote server — an IP address or a +domain name both work. PROC will periodically connect to that server's port; on +the server you only need to listen with `nc` or `socat` to reach PROC's menu, +control the computer, and log in over the serial port. + +> **[Note] This feature no longer exists.** It was deleted from the firmware on +> 2024-11-02 by commit `6dbb189` ("清理pwm和autolink代码"). The EEPROM fields +> remain but nothing reads them and there is no menu entry. `git show 6dbb189` +> recovers the implementation. See [../AGENTS.md](../AGENTS.md) §5 issue 10. + +--- + +That covers using PROC, which is fairly simple. Next, how to use the serial port. + +## Serial console setup on the managed machine + +### Redirecting BIOS output to the serial port + +This requires motherboard support and is configured in the BIOS, for example: + +``` +Server Management --> Console Redirection --> Console Redirection = "Serial Port A" +``` + +### Getting PMON output on the serial port + +This needs no configuration at all — PMON supports serial operation directly. +The `boot.cfg` menu is simply not drawn, but keyboard input still works. + +LoongArch motherboard firmware will output on the serial port as long as no +monitor is plugged in. However, release builds generally do not include serial +support, so you need a `dbg` build of the firmware. + +### Redirecting GRUB 1 output to the serial port + +Edit `/boot/grub/menu.list`: + +``` +GRUB_CMDLINE_LINUX_DEFAULT="console=tty0 console=ttyS0,115200n8" +``` + +### Redirecting GRUB 2 output to the serial port + +Edit `/etc/default/grub` or `/etc/default/grub.d/serial.cfg`: + +``` +GRUB_TERMINAL="serial console" +GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1" +GRUB_CMDLINE_LINUX="console=ttyS0,115200n8 console=tty" +``` + +### Kernel boot messages on the serial port + +Modify the GRUB configuration to add this to the kernel command line: + +``` +console=ttyS0,115200n8 console=tty0 +``` + +### Logging into a Linux shell over the serial port + +On non-systemd systems, edit `/etc/inittab`, add the following, then `kill -1 1` +to make init reload its configuration file: + +``` +T0:23:respawn:/sbin/getty -L ttyS0 115200 vt100 +``` + +On systemd systems: + +``` +systemctl start getty@ttyS0 +systemctl enable getty@ttyS0 +``` + +--- + +## PROC script configuration + +From PROC's main menu, press **`f`** to enter script configuration, then choose +which script to modify (**1–9**). + +Each script can be at most **50 characters**. The command format is: + +| Command | Meaning | +|---|---| +| `P` | Press the power switch. May be followed by a number (1–65536) giving how many ms to hold it. The delay is **non-blocking** — the following commands execute immediately | +| `p` | Release the power switch | +| `R` | Press the reset button. May be followed by a number (1–65536) giving how many ms to hold it. The delay is **non-blocking** — the following commands execute immediately | +| `r` | Release the reset button | +| `V` | Turn the (5–28 V) output on | +| `v` | Turn the (5–28 V) output off | +| `M` | Followed by a number (0–255): set the PWM output | +| `T` | Followed by a number (1–65536): wait this many ms. **Blocking** — the following commands only execute once the delay finishes | + +> **[Note]** In upstream HEAD, `run_script()` implements `P p R r V v` and +> `T`/`t`, but **not `M`**. `V` with a numeric argument does an `analogWrite()` +> rather than a plain on. The "separate page" promised above for script +> documentation is this section — no other page exists. + +--- + +## Attachments on the original page + +| File | Size | URL | +|---|---|---| +| Firmware, 3 builds — the minimal one, one with PWM, one with PWM and autolink | 93.99 KB | https://bjlx.org.cn/system/files/procv1_20200523.zip | +| `telnetrc.` | 38 bytes | https://bjlx.org.cn/system/files/telnetrc. | + +**[Note]** The zip contains `prcv1.hex`, `prcv1_pwm.hex` and +`prcv1_pwm_autolink.hex`, all reporting `PROC-V1-20200523-d58845a`. That commit +is not in the public [`prc`](../../prc) repository. All three are near-full +flash images; `prcv1_pwm_autolink.hex` reaches 31406 bytes, which **exceeds the +30720 bytes left by the 2 KB bootloader current builds assume** — check a +board's fuses before flashing that image onto it. See +[../AGENTS.md](../AGENTS.md) §3. + +**[Note]** `telnetrc.` is exactly the three lines given above. + +## Original page navigation (not part of the article) + +The site chrome links to: Debian (http://debian.org), flygoat's blog +(https://blog.flygoat.com/), USTC Linux User Association +(https://lug.ustc.edu.cn/wiki/), Loongson official site (http://www.loongson.cn/), +Loongson User Club (http://www.loongsonclub.cn), and the author's blog at +https://bjlx.org.cn/blog/1. + +© 2007-2024 北京龙芯用户俱乐部 (Beijing Loongson User Club) From 15f0e32ce53eeef5ea2cd47040b6e5d3a9c7f16f Mon Sep 17 00:00:00 2001 From: Lance Albertson Date: Sat, 25 Jul 2026 16:44:31 -0700 Subject: [PATCH 02/16] docs: translate all in-repo Chinese text to English Everything upstream is written in Chinese, which makes the firmware hard for OSU OSL to maintain. This translates the text that lives in the repository itself: README.md, the messages and comments in build.sh, and all 64 comment lines in prc/prc.ino, including the bootloader and fuse instructions in the header block. Comments only -- no code, formatting or behaviour is touched. Verified by building before and after with arduino-cli: both images are 29062 bytes with 694 bytes of globals, and differ in exactly five bytes, all inside the embedded __TIME__ string literal, which changes on every build. Kept as one isolated commit so it can be replayed or dropped in one operation when rebasing on upstream, which remains Chinese. Two upstream inaccuracies were left alone rather than silently corrected here, since this commit is a translation: - README.md says to select "arduino uno", but build.sh actually builds for arduino:avr:pro (Pro Mini, ATmega328P 3.3V 8MHz) - the comment above the pc_power block said "handle the reset key", a copy-paste from the block above it; rendered as "power key" because carrying the error into English helps nobody Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Lance Albertson --- AGENTS.md | 9 +++- README.md | 18 +++---- build.sh | 18 +++---- prc/prc.ino | 132 +++++++++++++++++++++++++++------------------------- 4 files changed, 94 insertions(+), 83 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a144fc7..16c868a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,14 @@ The firmware that runs on shipped PROC-V1 boards. One 1350-line Arduino sketch plus two shell scripts. Upstream `lshw/proc`, GPL-3.0, 57 commits from 2020-01-02 to 2024-11-03, sole author Liu Shiwei. -Upstream README, translated: +> **Fork divergence:** in this fork the in-repo Chinese has already been +> translated in place — `README.md`, `build.sh`, and all 64 comment lines in +> `prc/prc.ino`. Upstream is still Chinese, so **expect conflicts on those +> files when rebasing on `upstream/main`**. The translation is one isolated +> commit and touches comments only: the compiled image is byte-identical apart +> from the embedded `__TIME__` literal, verified with `arduino-cli`. + +Upstream README (now English in this fork): > **proc** — source for proc_v1 > diff --git a/README.md b/README.md index a118e26..3d28105 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # proc -proc_v1 的源码 +Source code for proc_v1 -ide使用arduino, 型号选择 arduino uno -把板子上的串口接到电脑上,就可以编译下载了, +The IDE is arduino; for the model select arduino uno +Connect the board's serial port to the computer and you can compile and download. -用命令行进行编译: -执行build.sh, 会使用arduino-cli进行编译下载。如果没有arduino-cli,会自动下载arduino-cli, 如果缺库, 脚本会自动下载库, 完成编译后, rom会在build.sh所在的目录prc.hex +Compiling from the command line: +Run build.sh, which uses arduino-cli to compile and download. If arduino-cli is missing it downloads arduino-cli automatically; if a library is missing the script downloads the library automatically. Once the build finishes, the rom is at prc.hex in the directory containing build.sh -用命令行进行升级: -把设备接到usb串口上,如果使用被控机进行升级, 根据串口设备编号,修改脚本,然后,linux下要退出串口login程序对串口的占用。 -执行update.sh, 然后给在倒计时到1时, 给设备上电, 升级过程就会开始。 -如果升级失败,调整一下给设备上电的时机,再试一下。 +Upgrading from the command line: +Connect the device to a usb serial port. If you are upgrading from the controlled machine, edit the script according to the serial device number, and under linux the serial login program must first release its hold on the serial port. +Run update.sh, then apply power to the device as the countdown reaches 1, and the upgrade process starts. +If the upgrade fails, adjust the timing of when you power the device and try again. diff --git a/build.sh b/build.sh index f75efd4..da00ef4 100755 --- a/build.sh +++ b/build.sh @@ -27,8 +27,8 @@ if [ $? != 0 ] ; then if [ -x $home/bin/arduino-cli ] ; then arduino_cli=$home/bin/arduino-cli else - echo 没有找到 arduino-cli - echo 请到https://github.com/arduino/arduino-cli/releases 下载, 并放到 /usr/local/bin目录下 + echo arduino-cli not found + echo Please download it from https://github.com/arduino/arduino-cli/releases and put it in /usr/local/bin mkdir ~/bin -p wget https://github.com/arduino/arduino-cli/releases/download/v1.0.4/arduino-cli_1.0.4_Linux_64bit.tar.gz -c tar zxf arduino-cli_1.0.4_Linux_64bit.tar.gz -C $home/bin arduino-cli @@ -58,16 +58,16 @@ mkdir -p /tmp/${me}_build /tmp/${me}_cache fqbn="arduino:avr:pro:cpu=8MHzatmega328" #fqbn="m328pb:avr:atmega328pbic:speed=8mhz" -#开发板:Arduino AVR Boards -> Arduino Pro or Pro Mini -#处理器:Atmega328P(3.3V,8Mhz) +#Board: Arduino AVR Boards -> Arduino Pro or Pro Mini +#Processor: Atmega328P(3.3V,8Mhz) -#传递宏定义 GIT_VER 到源码中,源码git版本 编译参数 +#pass the GIT_VER macro into the source: the git revision and the build settings CXXFLAGS="-DGIT_VER=\"$ver\" -DBUILD_SET=\"$fqbn\"" -#安装编译环境 +#install the build environment $arduino_cli core install $( echo $fqbn |awk -F: '{print $1":"$2}' ) -#安装硬件驱动库 +#install the hardware driver libraries for lib in OneWire Ethernet3 do if ! [ -x $home/Arduino/libraries/$lib ] ; then @@ -84,8 +84,8 @@ $arduino_cli compile \ prc |tee /tmp/${me}_info.log sync if [ -e /tmp/${me}_build/prc.ino.hex ] ; then - grep "Global vari" /tmp/${me}_info.log |sed -n "s/^Global variables use \([0-9]*\) bytes (\([0-9]*\)%) of dynamic memory, leaving \([0-9]*\) bytes for local variables. Maximum is 2048 bytes.$/RAM:使用\2%(\1字节),剩余\3字节/p" - grep "^Sketch" /tmp/${me}_info.log |sed -n "s/Sketch uses \([0-9]*\) bytes (\([0-9]*\)%.*$/ROM:使用\2%(\1字节)/p" + grep "Global vari" /tmp/${me}_info.log |sed -n "s/^Global variables use \([0-9]*\) bytes (\([0-9]*\)%) of dynamic memory, leaving \([0-9]*\) bytes for local variables. Maximum is 2048 bytes.$/RAM: \2% used (\1 bytes), \3 bytes free/p" + grep "^Sketch" /tmp/${me}_info.log |sed -n "s/Sketch uses \([0-9]*\) bytes (\([0-9]*\)%.*$/ROM: \2% used (\1 bytes)/p" echo ver:$ver cp -a /tmp/${me}_build/prc.ino.hex ./prc.hex diff --git a/prc/prc.ino b/prc/prc.ino index 600e1d2..e63ceda 100644 --- a/prc/prc.ino +++ b/prc/prc.ino @@ -1,9 +1,13 @@ /* - bootloader 以 "pro mini 为基础,融丝H,L,E从FF,DA,FD 改成 C2 DA FD",从外置晶振8Mhz,改成RC8Mhz - 把arduino例子里的arduinoISP写到一个uno里, 然后,临时插上一个8M晶振,10->reset(update-左脚),11->MOSI,12->MISO,13-CLK,GND-GND,VCC->5Vin - 然后编程器选 "Arduino as ISP",点"工具"->"烧录引导程序" + bootloader: based on "pro mini", with the fuses changed from FF,DA,FD to + C2,DA,FD (lfuse FF->C2), i.e. from an external 8MHz crystal to the internal + 8MHz RC oscillator. + Write the ArduinoISP example sketch into an Uno, then temporarily fit an 8MHz + crystal and wire: 10->reset (the left pin of the "update" header), 11->MOSI, + 12->MISO, 13-CLK, GND-GND, VCC->5Vin + Then select the programmer "Arduino as ISP" and click Tools -> Burn Bootloader - 编译时, 需要安装OneWire库, maintainer=Paul Stoffregen + Building requires the OneWire library, maintainer=Paul Stoffregen */ #ifndef GIT_COMMIT_ID #define GIT_COMMIT_ID "test" @@ -27,9 +31,9 @@ EthernetClient client; #ifdef PWM uint8_t pwm; #endif -//定时器最长65536秒 18小时 -uint16_t timer1 = 0; //秒 定时测温 -uint16_t volatile dogcount = 0; //超时重启,主程序循环清零,不清零的话100秒重启系统 +//the timer maxes out at 65536 seconds = 18 hours +uint16_t timer1 = 0; //seconds; periodic temperature reading +uint16_t volatile dogcount = 0; //watchdog counter, cleared by the main loop. If it is never cleared the system reboots after 100 seconds #define S_TCP 1 #define S_SERIAL 0 @@ -126,18 +130,18 @@ enum WATCHDOG8, WATCHDOG9, WATCHDOG10, - WATCHDOG_EN, //开启watchdog功能 - PWM_NOW, //当前PWM位置 + WATCHDOG_EN, //enable the watchdog feature + PWM_NOW, //current PWM position ROMCRC, - //后面的不做校验 - REMOTE_CYCLE, //主动外联,重试周期 - REMOTE_PORT_H, //主动外联端口 + //the entries below this point are not checksummed + REMOTE_CYCLE, //active outbound connection: retry interval + REMOTE_PORT_H, //active outbound connection: port REMOTE_PORT_L, - REMOTE_HOST, //主动外联ip + REMOTE_HOST, //active outbound connection: ip ROMLEN }; -#define SCRIPT_SIZE 50 //每个脚本的长度 ; -#define SCRIPT_ADDR ROMLEN+2 //起始地址 +#define SCRIPT_SIZE 50 //length of each script ; +#define SCRIPT_ADDR ROMLEN+2 //start address EthernetServer server(23); uint8_t osc; @@ -167,7 +171,7 @@ void setup() { analogWrite(PWM, pwm); #endif pinMode(_24V_OUT, OUTPUT); - digitalWrite(_24V_OUT, HIGH); //默认24V开启输出 + digitalWrite(_24V_OUT, HIGH); //24V output enabled by default pinMode(PC_RESET, OUTPUT); digitalWrite(PC_RESET, LOW); pinMode(PC_POWER, OUTPUT); @@ -192,7 +196,7 @@ void setup() { if (com_speed == 0) com_speed = 115200; Serial.begin(com_speed, get_comset()); digitalWrite(_24V_OUT, eeprom_read(VOUT_SET)); - mac[0] = 0xdc; //mac的第一位必须是偶数,否则就是广播地址 + mac[0] = 0xdc; //the first mac octet must be even, otherwise it is a broadcast address mac[1] = 0xad; mac[2] = 0xbe; mac[3] = eeprom_read(MAC3); @@ -214,7 +218,7 @@ void setup() { } } if (dhcp_ok == false) - Ethernet.begin(mac, ip, gateway, subnet); //dhcp==N 或者dhcp获取失败 + Ethernet.begin(mac, ip, gateway, subnet); //dhcp==N, or dhcp failed for (uint8_t i = 0; i < 3; i++) clientn[i].proc = 0; server.begin(); @@ -247,12 +251,12 @@ bool magic() { } void temp() { - if (timer1 == 2) { //60秒测温一次 + if (timer1 == 2) { //read the temperature once every 60 seconds ds1820_start(); - timer1 = 1; //跳过2, ds1820_start只执行1次 + timer1 = 1; //skip 2, so ds1820_start only runs once } if (timer1 == 0) { - timer1 = 60; //测温ok + timer1 = 60; //temperature read ok ds1820_all(); } } @@ -269,14 +273,14 @@ bool new_link() { && ((clientn[2].proc == 0) || (host != clientn[2].host)) ) { have_new = true; } - for (uint8_t i = 0 ; i < 3; i++) { //检查3个新的连接的认证过程 + for (uint8_t i = 0 ; i < 3; i++) { //check the authentication progress of the 3 pending connections switch (clientn[i].proc) { - case 1: //等待输入密码 + case 1: //waiting for the password while (clientn[i].host.available()) { - clientn[i].ms = millis() + 20000; //20秒延迟 + clientn[i].ms = millis() + 20000; //20 second timeout ch = clientn[i].host.read(); if (ch >= '0' && ch <= '9') { - //输入有效数字 + //a valid digit was entered clientn[i].passwd = clientn[i].passwd * 10 + (ch & 0xf); } else if (ch == 0x8) { if (clientn[i].passwd != 0) { @@ -286,11 +290,11 @@ bool new_link() { if (clientn[i].passwd == eeprom_read_u32(PASSWD0)) { if ( alreadyConnected) { client.println(F("\r\nnew client up, you are offline.\r\n")); - client.stop(); //有bye状态的老的连接,就先踢掉 + client.stop(); //kick the old connection first if it is in the bye state } alreadyConnected = true; client = clientn[i].host; - clientn[i].proc = 0; //释放当前的连接池 + clientn[i].proc = 0; //release this slot in the connection pool client.println(F("OK!")); clientn[i].host.flush(); return true; @@ -299,13 +303,13 @@ bool new_link() { } } if ( clientn[i].ms < millis()) { - //完成密码输入或超时 + //password entry finished, or timed out clientn[i].proc = 2; - clientn[i].ms = millis() + 5000; //密码错误, 5秒惩罚 + clientn[i].ms = millis() + 5000; //wrong password: 5 second penalty break; } break; - case 2: //认证失败等待惩罚时间到期 + case 2: //auth failed: wait for the penalty to expire if (clientn[i].ms < millis()) { clientn[i].host.println(F("auth fail!")); clientn[i].proc = 0; @@ -316,7 +320,7 @@ bool new_link() { clientn[i].host.read(); break; default: - if (have_new) { //有新的连接上来 + if (have_new) { //a new connection has arrived have_new = false; clientn[i].host = host; clientn[i].ms = millis() + 20000; @@ -337,7 +341,7 @@ void loop() { dogcount = 0; new_link(); if (alreadyConnected) { - if (!client.connected()) {//连接断开 + if (!client.connected()) {//connection dropped client.stop(); alreadyConnected = false; } @@ -367,14 +371,14 @@ void com_shell() { s_clean(&client); client.println(F("\r\nWelcome to com, enter'+++' to quit")); while (1) { - if (new_link()) return; //切换了连接 + if (new_link()) return; //the connection was switched dogcount = 0; if (!client.connected()) { client.stop(); alreadyConnected = false; return; } - while (client.available() > 0) { //tcp有数据进来 + while (client.available() > 0) { //data arriving from tcp ch = client.read(); if (ch >= 0xf4) continue; if (ch == 0xd || ch == 0xa) { @@ -398,52 +402,52 @@ void com_shell() { } client.write(chs, chlen); if (ms0 < millis() || client.available()) - break; //最多2秒 + break; //2 seconds maximum } } } -//看门狗中断做定时任务 30ms 1次 +//the watchdog interrupt runs the periodic tasks, once every 30ms uint16_t volatile ms = 0; -int16_t volatile pc_reset_on = 0; //按下pc_reset键的ms时长 -int16_t volatile pc_power_on = 0; //按下pc_power键的ms时长 +int16_t volatile pc_reset_on = 0; //how many ms the pc_reset key stays pressed +int16_t volatile pc_power_on = 0; //how many ms the pc_power key stays pressed ISR(WDT_vect) { dogcount++; //30ms if (dogcount > 100000 / 30) { OSCCAL = osc; - asm volatile (" jmp 0"); //100秒看门狗超时重启 + asm volatile (" jmp 0"); //100 second watchdog timeout: reboot } ms += 30; if (ms > 1000) { - if (timer1 > 0) timer1--;//定时器1 测温 + if (timer1 > 0) timer1--;//timer 1: temperature reading ms -= 1000; } - //处理reset键,其它程序只要修改 pc_reset_on=300,就可以按下300ms + //handle the reset key: other code only has to set pc_reset_on=300 to press it for 300ms if (pc_reset_on > 0) { pc_reset_on -= 30; //30ms - if (pc_reset_on > 0) { //reset开关按下 + if (pc_reset_on > 0) { //reset switch pressed if (digitalRead(PC_RESET) != HIGH) digitalWrite(PC_RESET, HIGH); - } else { //reset开关松开 + } else { //reset switch released if (digitalRead(PC_RESET) != LOW) digitalWrite(PC_RESET, LOW); } } - //处理reset键,其它程序只要修改 pc_power_on=300,就可以按下300ms + //handle the power key: other code only has to set pc_power_on=300 to press it for 300ms if (pc_power_on > 0) { pc_power_on -= 30; //30ms - if (pc_power_on > 0) { //power开关按下 + if (pc_power_on > 0) { //power switch pressed if (digitalRead(PC_POWER) != HIGH) digitalWrite(PC_POWER, HIGH); - } else { //power开关松开 + } else { //power switch released if (digitalRead(PC_POWER) != LOW) digitalWrite(PC_POWER, LOW); } } } -//设置看门狗定时中断时间ii=WDTO_15MS .... WDTO_8S +//set the watchdog interrupt interval; ii=WDTO_15MS .... WDTO_8S void setup_watchdog(int ii) { byte bb; if (ii > 9 ) ii = 9; @@ -493,7 +497,7 @@ void menu( uint8_t stype) { s->println(pwm); #endif s->println(F("===script 1-9====")); - disp_script( s, false); //从0号开始显示 + disp_script( s, false); //display starting from number 0 s->print(F("===set===\r\n" "a:reboot\r\n" "b:restore default set\r\n" @@ -579,7 +583,7 @@ void menu( uint8_t stype) { case 'a': case 'A': OSCCAL = osc; - asm volatile (" jmp 0"); //重启 + asm volatile (" jmp 0"); //reboot break; #ifdef PWM case '.': @@ -608,7 +612,7 @@ void ds1820_search() { uint8_t * addr; ds.reset_search(); delay(250); - celsius[0] = -400 * 16; //跳过0号 + celsius[0] = -400 * 16; //skip number 0 memset(ds_addr, 0, sizeof(ds_addr)); for (i = 0; i < 8; i++) ds_addr[0][i] = eeprom_read(SN0 + i); i = 1; @@ -630,8 +634,8 @@ void ds1820_search() { && ds_addr[i][6] == ds_addr[0][6] && ds_addr[i][7] == ds_addr[0][7] ) { - celsius[0] = 0; //存在0号温度探头 ,取消跳过 - continue; //跳过0号探头 + celsius[0] = 0; //probe number 0 exists, so stop skipping it + continue; //skip probe number 0 } else { i++; } @@ -718,18 +722,18 @@ void check_rom() { sets[MAC1] = 0xAD; sets[MAC2] = 0xBE; addr = &sets[SN0]; - if (ds1820_count == 1 && ds_addr[1][0] != 0) //只有一个1820,并且有效,复制1820的sn到sn + if (ds1820_count == 1 && ds_addr[1][0] != 0) //exactly one valid 1820: copy the 1820 rom code into sn for (i = 0; i < 8; i++) { addr[i] = ds_addr[1][i]; } - if (OneWire::crc8(addr, 7) != (uint8_t)addr[7]) {//SN不对 - if (OneWire::crc8(ds_addr[0], 7) == ds_addr[0][7]) {//但当前SN有效 + if (OneWire::crc8(addr, 7) != (uint8_t)addr[7]) {//SN is wrong + if (OneWire::crc8(ds_addr[0], 7) == ds_addr[0][7]) {//but the current SN is valid for (i = 0; i < 8; i++) { - addr[i] = ds_addr[0][i]; //复制当前SN + addr[i] = ds_addr[0][i]; //copy the current SN } } else { sets[MAC5] = 1; - sets[MAC2] = 0;//SN都不对,先用DE:AD:00:xx:xx:xx,下次再试一下 + sets[MAC2] = 0;//no SN is valid: use DE:AD:00:xx:xx:xx for now and try again next boot } } sets[NAME0] = 'P'; @@ -784,13 +788,13 @@ void check_rom() { sets[WATCHDOG4] = 'P'; sets[WATCHDOG5] = 'V'; sets[WATCHDOG6] = 0xff; - sets[REMOTE_HOST] = 0; //主动外联服务器地址,默认为空 + sets[REMOTE_HOST] = 0; //active outbound server address, empty by default sets[REMOTE_CYCLE] = 0; sets[REMOTE_PORT_H] = 1234 / 0x100; sets[REMOTE_PORT_L] = 1234 % 0x100; sets[PWM_NOW] = 128; for (i = 0; i < 10; i++) - eeprom_write(SCRIPT_ADDR + SCRIPT_SIZE * i, 0); //清开机脚本 + eeprom_write(SCRIPT_ADDR + SCRIPT_SIZE * i, 0); //clear the startup scripts for (i = 0; i < sizeof(sets); i++) eeprom_write(i, sets[i]); } void set_passwd(Stream *s) { @@ -821,7 +825,7 @@ void set_passwd(Stream *s) { eeprom_write_u32(PASSWD0, passwd); } -//校准rc振荡器 +//calibrate the rc oscillator void rc_calibration() { uint8_t osc1, osc0 = OSCCAL; uint8_t oscs[256]; @@ -1192,7 +1196,7 @@ void disp_name(Stream * s) { } } -//通过console 输入字符串,保存到eeprom的 addr0- addr1 +//read a string from the console and save it into eeprom addr0 - addr1 void eeprom_set_str(Stream * s, uint16_t addr0, uint16_t addr1) { uint32_t ms0; uint8_t ch; @@ -1239,7 +1243,7 @@ void run_script( Stream * s, uint8_t script_n) { case 'P': if (next_is_number(eeprom_addr)) { eeprom_addr++; - pc_power_on = get_uint16(eeprom_addr); //addr要被更新 + pc_power_on = get_uint16(eeprom_addr); //addr gets updated s->print(pc_power_on); } else { if (ch == 'P') @@ -1252,7 +1256,7 @@ void run_script( Stream * s, uint8_t script_n) { case 'R': if (next_is_number(eeprom_addr)) { eeprom_addr++; - pc_reset_on = get_uint16(eeprom_addr); //i要被更新 + pc_reset_on = get_uint16(eeprom_addr); //i gets updated s->print(pc_reset_on); } else { if (ch == 'R') @@ -1316,7 +1320,7 @@ void modi_script( Stream * s) { delay(100); s_clean(s); s->println(); - disp_script( s, true); //从0号开始显示 + disp_script( s, true); //display starting from number 0 while (!s->available()) dogcount = 0; ch = getc_(s); s->write(ch); From 68fe8352216a5ab022b451c3f8ec41070a05e8ca Mon Sep 17 00:00:00 2001 From: Lance Albertson Date: Sun, 26 Jul 2026 14:31:29 -0700 Subject: [PATCH 03/16] fix: reclaim auth slots so the telnet server cannot go deaf A node in production stopped accepting telnet logins: the TCP connection established, no "passwd:" prompt ever appeared, and the client eventually timed out. Only a reboot cleared it. new_link() returned early whenever server.available() yielded no client: host = server.available(); if (!host.connected()) return false; Ethernet3's EthernetServer::available() only returns a socket that has unread bytes waiting (EthernetServer.cpp:60, "if (client.available())"), so it returns an invalid client whenever every socket is quiet. The pending-slot timeout handling lives inside the loop *after* that return, so it never ran in exactly the situation it exists to handle. Three abandoned sessions -- a closed terminal, a slept laptop, a dropped tunnel -- therefore left all three clientn[] slots pinned in proc == 1 forever. New connections were accepted by the W5500, which allows 7, but the firmware tracks only 4 and had no slot to give them, so they sat connected and silent. No keepalive is configured (Sn_KPALVTR is never written), so the stale sockets never expired on their own and a link flap did not clear them either. Four changes, all in new_link(): - drop the early return, folding host.connected() into the have_new condition so the state machine runs on every call. This is the bug. - reclaim a slot as soon as its peer disconnects rather than waiting out the 20s timeout. connected() stays true in CLOSE_WAIT while bytes remain, so a password typed just before the FIN is still read. - compare deadlines with ms_expired(), which uses the signed difference and survives the ~49 day millis() rollover. These boxes run for months, and a slot caught by the wrap was stuck for weeks. The "ms = 0" expire-now sentinel becomes millis() - 1, since 0 is not in the past under signed-delta arithmetic. - refuse a connection when every slot is busy, with "busy" and a stop(), instead of leaving it accepted but unread. available() always returns the lowest socket holding data, so a single unread orphan masks every later connection. Costs 48 bytes of flash (29062 -> 29110, 94%, 1610 free) and no RAM. Verified with arduino-cli. Not yet tested on hardware. Note stop() blocks up to 1s waiting for the socket to close, so each reclaim can stall the loop that long -- far below the 100s watchdog, and once per disconnect rather than repeatedly. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Lance Albertson --- AGENTS.md | 41 +++++++++++++++++++++++++++++++++++++++++ prc/prc.ino | 36 +++++++++++++++++++++++++++++++----- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 16c868a..0ac4418 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -392,6 +392,7 @@ reproduced with `arduino-cli`. | # | Severity | Issue | |---|---|---| +| 0 | **Critical — FIXED on this branch** | **The telnet server goes permanently deaf after abandoned sessions.** `new_link()` returned at line 269 whenever `server.available()` yielded no client — but Ethernet3's `EthernetServer::available()` only returns a socket that has **unread bytes waiting** (`EthernetServer.cpp:60`), and the pending-slot timeout handling lives *inside* the loop after that return. So three abandoned sessions (closed terminal, dropped tunnel) left all three `clientn[]` slots stuck in `proc == 1` with nothing ever reclaiming them, and every later connection was accepted by the W5500 but never given a slot: TCP connects, no `passwd:` prompt, silence until the client gives up. Contributing factors: `clientn[i].ms < millis()` breaks across the ~49-day rollover, and the W5500 accepts up to 7 connections while the firmware tracks 4, with no keepalive configured (`Sn_KPALVTR` is never written) so stale sockets survive indefinitely — a link flap does **not** clear them. See §5.1 | | 1 | **High** | **PWM builds do not compile.** `uint8_t pwm;` (line 28, inside `#ifdef PWM`) and `uint8_t pwm = 128;` (line 461) are both namespace-scope definitions. **Verified by build:** `-DPWM=5` fails with `error: redefinition of 'uint8_t pwm'`. Dead since the 2024 cleanup | | 2 | **High** | **Renaming the device presses the PC's reset button.** `case 'n'`/`'N'` (lines 532–535) has no `break` and falls into `case 'r'` (line 536), which sets `pc_reset_on = 300` | | 3 | Medium | **Menu advertises `V` but only `v` works.** Menu prints `"V:Vout= "` (line 488); the only handler is `case 'v'` (line 548). The `case 'V'` at line 1266 is in `run_script()`, not the menu | @@ -408,6 +409,46 @@ reproduced with `arduino-cli`. | 14 | **Constraint** | **~1.6 KB of flash left** (29062/30720). RAM: 694 B globals plus a 512 B stack buffer in `com_shell()` (line 362) and a 256 B `oscs[]` in `rc_calibration()` (line 827) that is written but never read — 256 free bytes right there. `ds1820_disp()` pulls in floating-point `print`; fixed-point would free noticeably more | | 15 | Low | **MAC OUI `DC:AD:BE` has the locally-administered bit clear**, claiming a globally-unique OUI the project does not own. `DE:AD:BE` would be correct. Commit `ca4f739` shows the author was aware of the OUI bit | +### 5.1 The `new_link()` deafness fix + +Four changes, all in `new_link()`, costing **48 bytes** of flash (29062 → 29110, +still 94%, 1610 free) and no RAM: + +1. **Removed the early return**, folding `host.connected()` into the `have_new` + condition, so the pending-slot state machine runs on every call rather than + only when some socket happens to hold data. This is the actual bug. +2. **Reclaim a slot the moment its peer disconnects**, instead of waiting out + the 20 s timeout. `EthernetClient::connected()` stays true in `CLOSE_WAIT` + while bytes remain, so a password typed just before the FIN is still read. +3. **Rollover-safe deadlines** via `ms_expired()`, comparing the signed + difference. The `clientn[i].ms = 0` sentinel became `millis() - 1`, since + `0` is not "already expired" under signed-delta arithmetic. +4. **Refuse connections when every slot is busy** (`busy`, then `stop()`) + instead of leaving them accepted but unread. `server.available()` always + returns the *lowest* socket holding data, so one unread orphan masks every + later connection. + +Walking the original failure through the patched code: a closed terminal puts +the socket in `CLOSE_WAIT` with no data, `server.available()` returns nothing, +`host` is invalid and `have_new` is false — but the loop now still runs, case 1 +sees `!connected()` and frees the slot on the very next iteration. + +Operational notes for anyone debugging a wedged unit before this is deployed: + +- **Bouncing the switch port does not help.** No keepalive is configured, and + nothing in the sketch monitors PHY link state or re-inits the chip after + `setup()`, so stale sockets survive a link flap unchanged. +- **The serial escape still works** — it runs through `magic()` in `loop()`, + independent of the TCP path — *provided* `alreadyConnected` is false, since + `menu(S_SERIAL)` is gated on it. +- **Prefer the menu's `a` reboot over S1 or a power cycle.** `a` is `jmp 0`, + which does not reset the I/O registers, so PD3 holds its state and VOUT stays + up. A real reset drops VOUT for about a second (R17 pulls Q1's gate down, + R7 pulls the P-FET gates to VIN), which power-cycles anything fed from it. +- `EthernetClient::stop()` blocks for up to 1 s waiting for the socket to close, + so each reclaim can stall the loop that long. Far below the 100 s watchdog, + and it happens once per disconnect. + ### Suggested order of work 1. Issues 2, 3, 4 — small, obviously correct, upstreamable as one series. diff --git a/prc/prc.ino b/prc/prc.ino index e63ceda..df38651 100644 --- a/prc/prc.ino +++ b/prc/prc.ino @@ -261,13 +261,23 @@ void temp() { } } +//true once a millis() deadline has passed. Comparing the deadline against +//millis() directly breaks across the ~49 day rollover and can leave a slot +//pending for weeks; comparing the signed difference does not. +bool ms_expired(uint32_t deadline) { + return (int32_t)(millis() - deadline) >= 0; +} + bool new_link() { char ch; EthernetClient host; bool have_new = false; + //server.available() only returns a socket that has unread bytes waiting, so + //host is often an invalid client. Do not return early on that: the timeout + //handling below has to run on every call, otherwise a pending slot whose + //peer stopped sending is never reclaimed and the server goes deaf. host = server.available(); - if (!host.connected()) return false; - if (host && (host != client) + if (host.connected() && (host != client) && ((clientn[0].proc == 0) || (host != clientn[0].host)) && ((clientn[1].proc == 0) || (host != clientn[1].host)) && ((clientn[2].proc == 0) || (host != clientn[2].host)) ) { @@ -276,6 +286,14 @@ bool new_link() { for (uint8_t i = 0 ; i < 3; i++) { //check the authentication progress of the 3 pending connections switch (clientn[i].proc) { case 1: //waiting for the password + //peer disconnected (a closed terminal, a dropped tunnel): reclaim the + //slot now instead of holding it for the full timeout. connected() stays + //true in CLOSE_WAIT while bytes remain, so nothing typed is lost. + if (!clientn[i].host.connected()) { + clientn[i].proc = 0; + clientn[i].host.stop(); + break; + } while (clientn[i].host.available()) { clientn[i].ms = millis() + 20000; //20 second timeout ch = clientn[i].host.read(); @@ -299,10 +317,10 @@ bool new_link() { clientn[i].host.flush(); return true; } else if (ch == 0xd || ch == 0xa ) { - clientn[i].ms = 0; + clientn[i].ms = millis() - 1; //expire immediately } } - if ( clientn[i].ms < millis()) { + if (ms_expired(clientn[i].ms)) { //password entry finished, or timed out clientn[i].proc = 2; clientn[i].ms = millis() + 5000; //wrong password: 5 second penalty @@ -310,7 +328,7 @@ bool new_link() { } break; case 2: //auth failed: wait for the penalty to expire - if (clientn[i].ms < millis()) { + if (ms_expired(clientn[i].ms)) { clientn[i].host.println(F("auth fail!")); clientn[i].proc = 0; clientn[i].host.stop(); @@ -334,6 +352,14 @@ bool new_link() { } } } + //every slot was busy. Refuse the connection instead of leaving it accepted + //but unread: server.available() always returns the lowest socket holding + //data, so an unread orphan masks every later connection. + if (have_new) { + host.println(F("busy")); + host.flush(); + host.stop(); + } return false; } From 54ab1930857d607ebfe7c5959474e392ed942345 Mon Sep 17 00:00:00 2001 From: Lance Albertson Date: Sun, 26 Jul 2026 15:09:10 -0700 Subject: [PATCH 04/16] fix: repair the -DPWM=5 build, broken since 2024 pwm was defined twice at namespace scope: once under #ifdef PWM near the top so setup() could reach it, and once unconditionally further down. In C++ that is a redefinition, not a tentative definition, so any build with -DPWM= failed outright: prc/prc.ino:491:9: error: redefinition of 'uint8_t pwm' Keep the early definition, which has to stay early because the .ino preprocessor hoists function prototypes but not variable declarations, and give it the initialiser. Drop the later one. Also drops the unused 'vout' global next to it. Both variants now build: stock 29110 / 30720 (94%) 1642 free -DPWM=5 29312 / 30720 (95%) 1440 free The stock build is 42 bytes smaller than before, from removing two unused globals and the initialiser one of them carried. This is a prerequisite for CI covering the PWM variant: without it the matrix is red from the first run. Refs: AGENTS.md section 5 issue 1 Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Lance Albertson --- prc/prc.ino | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/prc/prc.ino b/prc/prc.ino index df38651..d825568 100644 --- a/prc/prc.ino +++ b/prc/prc.ino @@ -29,7 +29,10 @@ int16_t celsius[11]; boolean alreadyConnected = false; EthernetClient client; #ifdef PWM -uint8_t pwm; +//the single definition of the PWM duty. It has to live up here because setup() +//uses it, and the .ino preprocessor only hoists function prototypes, not +//variable declarations. Defining it a second time further down is a hard error. +uint8_t pwm = 128; #endif //the timer maxes out at 65536 seconds = 18 hours uint16_t timer1 = 0; //seconds; periodic temperature reading @@ -487,8 +490,6 @@ void setup_watchdog(int ii) { WDTCSR = bb; WDTCSR |= _BV(WDIE); } -uint8_t vout; -uint8_t pwm = 128; void menu( uint8_t stype) { uint32_t passwd, password; uint8_t ch; From 5f0c5af7a9e8c71afa69e3b3975b4ee0bc7b51ac Mon Sep 17 00:00:00 2001 From: Lance Albertson Date: Sun, 26 Jul 2026 15:09:10 -0700 Subject: [PATCH 05/16] ci: add a build pipeline with a flash-headroom gate This firmware fills ~95% of a 30720 byte flash, so the thing CI most needs to tell us is how much room a change costs. Everything here is built around that. CI (.github/workflows/ci.yml), on push and PR: - compiles both the stock and -DPWM=5 variants - pins arduino:avr 1.8.8, OneWire 2.3.8, Ethernet3 1.6.0, so a silent upstream bump cannot push the image over the ceiling on an unrelated PR - posts flash/RAM deltas on pull requests via arduino/compile-sketches, which is the signal that matters with ~1.6KB of headroom - reports compiler warnings (28 today, so reported not gated) - fails if fewer than 512 bytes of flash remain; avr-gcc already enforces the hard ceiling, this is the early warning - uploads a flashable .hex for every commit, so testing a branch on real hardware does not require a local toolchain - checks astyle formatting, advisory for now because the runner image ships a newer astyle than the 3.1 the tree is clean under Release (.github/workflows/release.yml), on a vX.Y.Z tag: - builds both variants with the version compiled in, enforces the same size gate, and attaches .hex, .elf, SHA256SUMS and toolchain.txt - the banner string becomes PROC-V1-X.Y.Z-g. The sha is included so a banner read off a deployed board maps to an exact commit even if a tag is moved -- the banner is often the only identification available on a machine that is otherwise unreachable - toolchain.txt records exact core and library versions, because an image this close to full is sensitive to toolchain drift tools/formatter.conf is vendored from lshw/procV2, which is where upstream keeps it; that repo is not forked, so the config would otherwise be unavailable to this one. tools/format.sh applies it. Note both workflows check out with fetch-depth: 0. build.sh derives GIT_VER from the git log and the delta report needs the base ref; neither works on a shallow clone. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Lance Albertson --- .github/workflows/ci.yml | 156 ++++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 132 ++++++++++++++++++++++++++++ tools/format.sh | 12 +++ tools/formatter.conf | 32 +++++++ 4 files changed, 332 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 tools/format.sh create mode 100644 tools/formatter.conf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f798da6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,156 @@ +name: CI + +on: + push: + branches: [main, osl-docs] + pull_request: + branches: [main] + workflow_dispatch: + +# Pinned deliberately. This firmware fills 95% of a 30720 byte flash, so a +# toolchain or library bump can push it over the edge; we want that to be a +# reviewed change, not a surprise on an unrelated PR. +env: + FQBN: arduino:avr:pro:cpu=8MHzatmega328 + PLATFORM_VERSION: 1.8.8 + ONEWIRE_VERSION: 2.3.8 + ETHERNET3_VERSION: 1.6.0 + # Fail the build if fewer than this many bytes of flash are left. The hard + # ceiling is enforced by avr-gcc itself; this is the early warning. + MIN_FREE_FLASH: 512 + +jobs: + compile: + name: compile (${{ matrix.name }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: stock + flags: "" + - name: pwm + flags: "-DPWM=5" + steps: + - uses: actions/checkout@v7 + with: + # build.sh derives GIT_VER from the git log, and the size-delta report + # needs the base ref. Neither works on a shallow clone. + fetch-depth: 0 + + - name: Derive version string + id: ver + run: | + echo "ver=$(git log -1 --date=short --format=%ad | tr -d -)-$(git rev-parse --short=7 HEAD)" >> "$GITHUB_OUTPUT" + + - uses: arduino/compile-sketches@v1.1.3 + with: + fqbn: ${{ env.FQBN }} + platforms: | + - name: arduino:avr + version: ${{ env.PLATFORM_VERSION }} + libraries: | + - name: OneWire + version: ${{ env.ONEWIRE_VERSION }} + - name: Ethernet3 + version: ${{ env.ETHERNET3_VERSION }} + sketch-paths: | + - ./prc + cli-compile-flags: | + - --build-property + - build.extra_flags=${{ matrix.flags }} -DGIT_VER="${{ steps.ver.outputs.ver }}" -DBUILD_SET="${{ env.FQBN }}" + enable-deltas-report: true + enable-warnings-report: true + sketches-report-path: sketches-reports + + - uses: actions/upload-artifact@v7 + with: + name: sketches-report-${{ matrix.name }} + path: sketches-reports + if-no-files-found: error + + headroom: + name: flash headroom + artifact + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - uses: arduino/setup-arduino-cli@v2.0.0 + + - name: Install pinned toolchain + run: | + arduino-cli core update-index + arduino-cli core install "arduino:avr@${PLATFORM_VERSION}" + arduino-cli lib install "OneWire@${ONEWIRE_VERSION}" + arduino-cli lib install "Ethernet3@${ETHERNET3_VERSION}" + + - name: Build + id: build + run: | + VER="$(git log -1 --date=short --format=%ad | tr -d -)-$(git rev-parse --short=7 HEAD)" + echo "ver=${VER}" >> "$GITHUB_OUTPUT" + arduino-cli compile \ + --fqbn "${FQBN}" \ + --build-property "build.extra_flags=-DGIT_VER=\"${VER}\" -DBUILD_SET=\"${FQBN}\"" \ + --output-dir dist \ + prc 2>&1 | tee build.log + + - name: Check flash headroom + run: | + # "Sketch uses 29068 bytes (94%) of program storage space. Maximum is 30720 bytes." + read -r USED MAX < <( + sed -n 's/^Sketch uses \([0-9]*\) bytes .*Maximum is \([0-9]*\) bytes\.$/\1 \2/p' build.log + ) + if [ -z "${USED}" ]; then + echo "::error::could not parse the size line out of build.log" + exit 1 + fi + FREE=$(( MAX - USED )) + echo "flash: ${USED} / ${MAX} used, ${FREE} free (threshold ${MIN_FREE_FLASH})" + { + echo "### Flash usage" + echo "" + echo "| used | max | free |" + echo "|---:|---:|---:|" + echo "| ${USED} | ${MAX} | **${FREE}** |" + } >> "$GITHUB_STEP_SUMMARY" + if [ "${FREE}" -lt "${MIN_FREE_FLASH}" ]; then + echo "::error::only ${FREE} bytes of flash left, below the ${MIN_FREE_FLASH} byte threshold" + exit 1 + fi + + - name: Name the artifact after the build + run: | + mv dist/prc.ino.hex "dist/proc-v1-${{ steps.build.outputs.ver }}.hex" + rm -f dist/prc.ino.with_bootloader.hex + sha256sum dist/*.hex > dist/SHA256SUMS + + - uses: actions/upload-artifact@v7 + with: + name: firmware-${{ steps.build.outputs.ver }} + path: dist + if-no-files-found: error + + format: + name: astyle + runs-on: ubuntu-latest + # Advisory for now. The tree is clean under astyle 3.1 with tools/formatter.conf, + # but the runner image ships a newer astyle whose defaults differ slightly. Flip + # this to a blocking check once a run confirms the runner produces no diff. + continue-on-error: true + steps: + - uses: actions/checkout@v7 + + - name: Install astyle + run: sudo apt-get update && sudo apt-get install -y astyle + + - name: Check formatting + run: | + astyle --version + astyle --options=tools/formatter.conf --suffix=none prc/prc.ino + if ! git diff --exit-code -- prc/prc.ino; then + echo "::warning::astyle reformatted the sketch; run tools/format.sh and commit" + exit 1 + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9a4a161 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,132 @@ +name: Release + +on: + push: + tags: ["v*"] + workflow_dispatch: + inputs: + tag: + description: "Tag to build (must already exist)" + required: true + +permissions: + contents: write + +env: + FQBN: arduino:avr:pro:cpu=8MHzatmega328 + PLATFORM_VERSION: 1.8.8 + ONEWIRE_VERSION: 2.3.8 + ETHERNET3_VERSION: 1.6.0 + MIN_FREE_FLASH: 512 + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ inputs.tag || github.ref }} + fetch-depth: 0 + + - name: Derive version + id: ver + run: | + TAG="${{ inputs.tag || github.ref_name }}" + VERSION="${TAG#v}" + SHA="$(git rev-parse --short=7 HEAD)" + # This string is compiled into the image and shown in the telnet banner + # as "#Ver:PROC-V1-". The sha is included so a banner read off a + # deployed unit always maps to an exact commit, even if a tag is moved. + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "git_ver=${VERSION}-g${SHA}" >> "$GITHUB_OUTPUT" + + - uses: arduino/setup-arduino-cli@v2.0.0 + + - name: Install pinned toolchain + run: | + arduino-cli core update-index + arduino-cli core install "arduino:avr@${PLATFORM_VERSION}" + arduino-cli lib install "OneWire@${ONEWIRE_VERSION}" + arduino-cli lib install "Ethernet3@${ETHERNET3_VERSION}" + arduino-cli version > toolchain.txt + arduino-cli core list >> toolchain.txt + arduino-cli lib list >> toolchain.txt + + - name: Build release variants + run: | + set -euo pipefail + mkdir -p dist + build () { + local name="$1" extra="$2" + echo "::group::build ${name}" + arduino-cli compile \ + --fqbn "${FQBN}" \ + --build-property "build.extra_flags=${extra} -DGIT_VER=\"${{ steps.ver.outputs.git_ver }}\" -DBUILD_SET=\"${FQBN}\"" \ + --output-dir "build-${name}" \ + prc 2>&1 | tee "build-${name}.log" + read -r USED MAX < <( + sed -n 's/^Sketch uses \([0-9]*\) bytes .*Maximum is \([0-9]*\) bytes\.$/\1 \2/p' "build-${name}.log" + ) + local FREE=$(( MAX - USED )) + echo "${name}: ${USED}/${MAX} used, ${FREE} free" + if [ "${FREE}" -lt "${MIN_FREE_FLASH}" ]; then + echo "::error::${name}: only ${FREE} bytes free, below ${MIN_FREE_FLASH}" + exit 1 + fi + cp "build-${name}/prc.ino.hex" "dist/proc-v1-${{ steps.ver.outputs.version }}-${name}.hex" + cp "build-${name}/prc.ino.elf" "dist/proc-v1-${{ steps.ver.outputs.version }}-${name}.elf" + echo "${name}|${USED}|${MAX}|${FREE}" >> sizes.txt + echo "::endgroup::" + } + build stock "" + build pwm "-DPWM=5" + cp toolchain.txt dist/ + ( cd dist && sha256sum ./*.hex ./*.elf > SHA256SUMS ) + + - name: Compose release notes + id: notes + run: | + { + echo "## PROC-V1 firmware ${{ steps.ver.outputs.tag }}" + echo "" + echo "Banner string on a flashed unit:" + echo "" + echo '```' + echo "#Ver:PROC-V1-${{ steps.ver.outputs.git_ver }}" + echo '```' + echo "" + echo "### Builds" + echo "" + echo "| variant | flash used | max | free |" + echo "|---|---:|---:|---:|" + while IFS='|' read -r n used max free; do + echo "| \`${n}\` | ${used} | ${max} | ${free} |" + done < sizes.txt + echo "" + echo "\`stock\` is the build to flash unless the board has the V1.1 PWM pad populated." + echo "" + echo "### Flashing" + echo "" + echo '```bash' + echo "sudo systemctl stop getty@ttyS0 # free the port if flashing from the managed host" + echo "avrdude -v -patmega328p -carduino -P/dev/ttyUSB0 -b57600 -D \\" + echo " -Uflash:w:proc-v1-${{ steps.ver.outputs.version }}-stock.hex:i" + echo '```' + echo "" + echo "Power the board as the countdown reaches 1; the bootloader window is short." + echo "Requires the CONN3 \"Update\" jumper so DTR can pull /RESET." + echo "" + echo "Verify with \`SHA256SUMS\`. \`toolchain.txt\` records the exact core and" + echo "library versions used, since this image fills ~95% of flash and is" + echo "sensitive to toolchain drift." + } > notes.md + cat notes.md >> "$GITHUB_STEP_SUMMARY" + + - uses: softprops/action-gh-release@v3.0.2 + with: + tag_name: ${{ steps.ver.outputs.tag }} + name: ${{ steps.ver.outputs.tag }} + body_path: notes.md + files: dist/* + fail_on_unmatched_files: true diff --git a/tools/format.sh b/tools/format.sh new file mode 100644 index 0000000..3bf9cfb --- /dev/null +++ b/tools/format.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Reformat the sketch the way upstream does, so our diffs stay rebase-friendly. +# The config is vendored from lshw/procV2 (lib/formatter.conf), which is where +# upstream keeps it -- that repo is not forked, so it lives here instead. +set -euo pipefail +cd "$(dirname "$0")/.." +if ! command -v astyle >/dev/null; then + echo "astyle not found: apt install astyle" >&2 + exit 1 +fi +astyle --options=tools/formatter.conf --suffix=none prc/prc.ino +git diff --stat -- prc/prc.ino diff --git a/tools/formatter.conf b/tools/formatter.conf new file mode 100644 index 0000000..7065e54 --- /dev/null +++ b/tools/formatter.conf @@ -0,0 +1,32 @@ +# This configuration file contains a selection of the available options provided by the formatting tool "Artistic Style" +# http://astyle.sourceforge.net/astyle.html +# +# If you wish to change them, don't edit this file. +# Instead, copy it in the same folder of file "preferences.txt" and modify the copy. This way, you won't lose your custom formatter settings when upgrading the IDE +# If you don't know where file preferences.txt is stored, open the IDE, File -> Preferences and you'll find a link + +mode=c + +# 2 spaces indentation +indent=spaces=2 + +# also indent macros +indent-preprocessor + +# indent classes, switches (and cases), comments starting at column 1 +indent-classes +indent-switches +indent-cases +indent-col1-comments + +# put a space around operators +pad-oper + +# put a space after if/for/while +pad-header + +# if you like one-liners, keep them +keep-one-line-statements + +remove-comment-prefix + From 4f17b2762e5ea244a282dbb720fe3f0d7cb2722b Mon Sep 17 00:00:00 2001 From: Lance Albertson Date: Sun, 26 Jul 2026 15:10:00 -0700 Subject: [PATCH 06/16] docs: add README, CONTRIBUTING and CHANGELOG for a maintained fork The README was still upstream's product blurb, translated. That describes the board but says nothing about how to work on it, that this is a fork, what diverges, or what constraints a change has to respect. README.md now covers what the board does, the fork's relationship to lshw/proc and what differs, a build/flash/connect quickstart, and the four constraints worth knowing before writing any code: flash is nearly full, it is one 1400-line file, it is not securable on this MCU, and the DS18B20 is the device identity. CONTRIBUTING.md records the things that are easy to get wrong: - commits need git commit -s - because this is a fork, GitHub defaults new PRs to base lshw/proc, not us. There is no repository setting that changes this, so it has to be checked every time - sync with upstream by merging, never rebasing. Our translation commit rewrites every Chinese comment; a rebase replays it on top of theirs and re-conflicts every line every time, where a merge conflicts once - send behaviour fixes upstream without the translation attached - say what you actually tested, since "builds and fits" and "tested on hardware" are very different claims CHANGELOG.md starts at the fork point, a8f458f. AGENTS.md keeps the technical analysis but now maps its issue numbers to the GitHub issues that own the state. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Lance Albertson --- AGENTS.md | 18 +++++++ CHANGELOG.md | 71 ++++++++++++++++++++++++++++ CONTRIBUTING.md | 110 +++++++++++++++++++++++++++++++++++++++++++ README.md | 121 ++++++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 310 insertions(+), 10 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md diff --git a/AGENTS.md b/AGENTS.md index 0ac4418..95232e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -390,6 +390,24 @@ the system tick: Read by inspection at `a8f458f`; items marked **verified by build** were reproduced with `arduino-cli`. +> **These are tracked as GitHub issues** — . +> This table keeps the analysis; the issues carry the state. Update both, or +> delete the row here and let the issue own it. +> +> | Here | Issue | | Here | Issue | +> |---|---|---|---|---| +> | 0 | **fixed** — see §5.1 | | 8 | [#7](https://github.com/osuosl/proc/issues/7) | +> | 1 | **fixed** | | 9 | [#8](https://github.com/osuosl/proc/issues/8) | +> | 2 | [#1](https://github.com/osuosl/proc/issues/1) | | 10 | [#9](https://github.com/osuosl/proc/issues/9) | +> | 3 | [#2](https://github.com/osuosl/proc/issues/2) | | 11 | [#10](https://github.com/osuosl/proc/issues/10) | +> | 4 | [#3](https://github.com/osuosl/proc/issues/3) | | 12 | [#11](https://github.com/osuosl/proc/issues/11) | +> | 5 | [#4](https://github.com/osuosl/proc/issues/4) | | 13 | [#12](https://github.com/osuosl/proc/issues/12) | +> | 6 | [#5](https://github.com/osuosl/proc/issues/5) | | 14 | [#13](https://github.com/osuosl/proc/issues/13) | +> | 7 | [#6](https://github.com/osuosl/proc/issues/6) | | 15 | [#14](https://github.com/osuosl/proc/issues/14) | +> +> Also filed, not in the table below: +> [#15 — clear the 28 compiler warnings](https://github.com/osuosl/proc/issues/15). + | # | Severity | Issue | |---|---|---| | 0 | **Critical — FIXED on this branch** | **The telnet server goes permanently deaf after abandoned sessions.** `new_link()` returned at line 269 whenever `server.available()` yielded no client — but Ethernet3's `EthernetServer::available()` only returns a socket that has **unread bytes waiting** (`EthernetServer.cpp:60`), and the pending-slot timeout handling lives *inside* the loop after that return. So three abandoned sessions (closed terminal, dropped tunnel) left all three `clientn[]` slots stuck in `proc == 1` with nothing ever reclaiming them, and every later connection was accepted by the W5500 but never given a slot: TCP connects, no `passwd:` prompt, silence until the client gives up. Contributing factors: `clientn[i].ms < millis()` breaks across the ~49-day rollover, and the W5500 accepts up to 7 connections while the firmware tracks 4, with no keepalive configured (`Sn_KPALVTR` is never written) so stale sockets survive indefinitely — a link flap does **not** clear them. See §5.1 | diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..668b142 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,71 @@ +# Changelog + +Notable changes to OSU OSL's fork of [`lshw/proc`](https://github.com/lshw/proc). +Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); +versioning is [SemVer](https://semver.org/). + +The version is compiled into the firmware and shown in the telnet banner as +`#Ver:PROC-V1--g`, so a banner read off a deployed board maps to an +exact commit. + +## [Unreleased] + +### Fixed + +- **The telnet server no longer goes permanently deaf after abandoned sessions.** + `new_link()` returned early whenever `server.available()` yielded no client, + but Ethernet3 only returns a socket that has *unread bytes waiting*, and the + pending-slot timeout handling lived after that return. Three abandoned + sessions — a closed terminal, a slept laptop, a dropped tunnel — pinned all + three authentication slots forever, and every later connection was accepted by + the W5500 but never given a slot: TCP connects, no `passwd:` prompt, silence. + Only a reboot cleared it. Also: slots are now reclaimed the moment a peer + disconnects rather than after the full 20 s timeout; deadlines survive the + ~49-day `millis()` rollover; and a connection arriving with every slot busy is + refused with `busy` instead of being left accepted but unread, where it would + mask every later connection. (+48 bytes flash) +- **`-DPWM=5` builds compile again.** `pwm` was defined twice at namespace + scope, which is a hard error in C++, so the PWM variant had been unbuildable + since the 2024 cleanup. Also removed the unused `vout` global. (−42 bytes + flash in the stock build) + +### Changed + +- **All Chinese text translated to English** — every comment in `prc/prc.ino`, + the messages and comments in `build.sh`, and `README.md`. Comments only: the + compiled image is byte-identical apart from the embedded `__TIME__` literal. +- `README.md` rewritten for a maintained fork. + +### Added + +- **CI** — both build variants, pinned toolchain, flash-headroom gate at 512 + bytes free, size-delta reports on PRs, compiler-warning reports, downloadable + firmware artifact per commit. +- **Release pipeline** — tagging `vX.Y.Z` builds both variants and attaches + `.hex`, `.elf`, `SHA256SUMS` and `toolchain.txt`. +- `AGENTS.md` — technical reference: build, EEPROM layout, menu, scripting, + session handling, and the verified issue list. +- `doc/node-926-software-manual.md` — the vendor software manual translated and + annotated wherever it no longer matches the code. +- `CONTRIBUTING.md`, this changelog, and `tools/` (vendored astyle config from + `lshw/procV2`, plus `format.sh`). + +### Known issues + +Tracked at . The ones to know about: + +- Menu option `n` (change name) falls through and fires a 300 ms reset at the + managed host. +- The menu prints `V:Vout=` but only lowercase `v` is handled. +- `ds1820()` reads one byte past the end of an 8-byte ROM code. +- A board whose DS18B20 has failed factory-resets on every boot. +- The `WATCHDOG*` EEPROM slots are written but never read — the host watchdog + they describe was never implemented. +- Authentication is a numeric PIN over cleartext telnet. Not fixable on this + MCU; treat these as management-VLAN-only appliances. + +--- + +Upstream history before the fork is in `git log`; upstream does not keep a +changelog. The fork point is +[`a8f458f`](https://github.com/lshw/proc/commit/a8f458f) (2024-11-03). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..93f867c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,110 @@ +# Contributing + +## Sign off every commit + +```bash +git commit -s +``` + +Every commit needs a `Signed-off-by:` trailer (the +[DCO](https://developercertificate.org/)). PRs without it will be asked to +amend. `-s` generates it from your git identity; don't write the line by hand. + +## ⚠ Check the base repo on every PR + +This repository is a **fork**, so GitHub defaults the base of a new pull request +to **`lshw/proc`** — upstream — not to us. Opening a PR without checking means +proposing your change to the original author instead of to OSL. + +Always confirm the base is `osuosl/proc` and the branch is `main`: + +```bash +gh pr create --repo osuosl/proc --base main +``` + +There is no repository setting that changes this default; it has to be checked +each time. + +## Branch model + +- `main` is the maintained line. It is protected and takes changes by PR. +- Work on a topic branch, open a PR, get CI green. +- `upstream/main` tracks [`lshw/proc`](https://github.com/lshw/proc). + +## Syncing with upstream: merge, never rebase + +```bash +git fetch upstream +git checkout main +git merge upstream/main # NOT rebase +``` + +**This matters.** Our translation commit rewrites every Chinese comment in +`prc/prc.ino`, both README files and `build.sh`. Rebasing `main` onto upstream +replays that commit on top of theirs and re-conflicts *every line, every time*. +Merging conflicts once, only where upstream actually touched the same lines. + +Expected conflict points: + +| File | Why | +|---|---| +| `prc/prc.ino` | ~64 comment lines translated | +| `build.sh` | messages and comments translated | +| `README.md` | rewritten for the fork | + +When resolving, keep our English and fold in upstream's *behaviour* change. + +## Sending fixes upstream + +The author is responsive and ships these boards commercially, so genuine fixes +are worth sending back. Upstream is Chinese-language; a bilingual subject line +is a courtesy: + +```bash +git format-patch -1 --to=liushiwei@gmail.com +# or +gh pr create --repo lshw/proc --base main +``` + +Send the **behaviour change alone**, without the translation — a patch that also +retranslates his comments is unlikely to be merged, and rightly so. + +## What CI enforces + +| Check | Blocking | Notes | +|---|---|---| +| Builds for `stock` and `-DPWM=5` | yes | both variants must compile | +| Flash headroom ≥ 512 bytes free | yes | avr-gcc enforces the hard 30720 ceiling itself | +| Size delta report | no | posted on the PR; watch it, headroom is ~1.6 KB | +| Compiler warnings | no | reported only — there are ~28 already; don't add more | +| `astyle` formatting | not yet | advisory until a run confirms the runner's astyle matches ours | + +Before pushing: + +```bash +tools/format.sh # astyle, using upstream's config +./build.sh # confirm it still fits +``` + +## Firmware-specific rules + +- **Watch the flash number on every build.** ~1.6 KB of headroom. A feature that + doesn't fit isn't a feature. +- **Don't reformat.** `tools/format.sh` only, and only lines you touched. + Gratuitous reformatting makes every future upstream merge worse. +- **Don't change the pin defines.** `_24V_OUT`=D3, `NET_RESET`=D4, W5500 SPI on + D10–D13, `DS`=A3, `PC_RESET`=A4, `PC_POWER`=A5. They are fixed by the PCB; see + [osuosl/prc](https://github.com/osuosl/prc). +- **Cite `file:line`.** One 1400-line file with no headers. +- **Say what you tested.** "Builds and fits" and "tested on hardware" are very + different claims. Put whichever is true in the commit message — several + existing commits say "not yet tested on hardware" for exactly this reason. + +## Releases + +Tag `vX.Y.Z` on `main`. The release workflow builds both variants, enforces the +size gate, and attaches `.hex`, `.elf`, `SHA256SUMS` and `toolchain.txt`. + +The version is compiled into the image and shown in the telnet banner as +`#Ver:PROC-V1-X.Y.Z-g`, so a banner read off a deployed board always maps to +an exact commit. Update [CHANGELOG.md](CHANGELOG.md) in the same PR as the tag. diff --git a/README.md b/README.md index 3d28105..9f1ee97 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,115 @@ -# proc -Source code for proc_v1 +# proc — PROC-V1 remote management firmware +[![CI](https://github.com/osuosl/proc/actions/workflows/ci.yml/badge.svg)](https://github.com/osuosl/proc/actions/workflows/ci.yml) -The IDE is arduino; for the model select arduino uno -Connect the board's serial port to the computer and you can compile and download. +Firmware for **PROC-V1**, a small board that gives a PC out-of-band management: +network-controlled power and reset buttons, and the machine's serial console +exposed over TCP. Roughly "poor man's IPMI" for hardware without a BMC. -Compiling from the command line: -Run build.sh, which uses arduino-cli to compile and download. If arduino-cli is missing it downloads arduino-cli automatically; if a library is missing the script downloads the library automatically. Once the build finishes, the rom is at prc.hex in the directory containing build.sh +An ATmega328PB drives a W5500 for wired Ethernet. You telnet to the board, get a +menu, and can power the host on or off, reset it, or drop into a transparent +serial console to talk to the BIOS, the bootloader or a Linux getty. -Upgrading from the command line: -Connect the device to a usb serial port. If you are upgrading from the controlled machine, edit the script according to the serial device number, and under linux the serial login program must first release its hold on the serial port. -Run update.sh, then apply power to the device as the countdown reaches 1, and the upgrade process starts. -If the upgrade fails, adjust the timing of when you power the device and try again. +``` +#DOC HTTPS://bjlx.org.cn/node/914 +#Ver:PROC-V1-1.0.0-g4edb245 +#name:PROC1A2B3C +#SN:28FF641E8016034A +#com:115200,8N1 +#C1A2B3=23.50℃ + +====== +0:com shell +r:reset (300ms) +R:reset (5 sec) +p:powerdown(300ms) +P:powerdown(5 sec) +v:Vout= ON +===set=== +a:reboot c:network info & modi e:setpasswd +b:restore default d:com set f:modi script 1-9 +q:quit offline +``` + +## Fork status + +This is OSU Open Source Lab's maintained fork of +[`lshw/proc`](https://github.com/lshw/proc) by Liu Shiwei +(刘世伟, [bjlx.org.cn](https://bjlx.org.cn/)), who designed the hardware and wrote +the original firmware. **All credit for the design is his.** OSL runs several of +these boards and forked to fix bugs and add features, with the intent of sending +fixes back upstream. + +What differs from upstream: + +- **All Chinese text translated to English** — comments, README, build script. + Comments only; the compiled image is byte-identical. +- **Bug fixes**, most importantly one where the telnet server went permanently + deaf after abandoned sessions. See [CHANGELOG.md](CHANGELOG.md). +- **CI and releases** — every push is built and size-checked; tags produce + signed-off `.hex` artifacts. +- **Documentation in English**, including translations of the vendor manuals. + +Upstream remains Chinese, so expect conflicts on translated files when syncing. +The policy for that is in [CONTRIBUTING.md](CONTRIBUTING.md). + +## Quickstart + +**Build:** + +```bash +./build.sh # downloads arduino-cli into ~/bin if missing, writes ./prc.hex +``` + +`build.sh` writes to `$HOME`. For a contained build, or to reproduce exactly what +CI does, see [AGENTS.md §3](AGENTS.md#3-building). + +**Flash** — from the managed host itself, or any machine with a USB serial adapter: + +```bash +sudo systemctl stop getty@ttyS0 # free the port first +./update.sh # power the board as the countdown hits 1 +``` + +The bootloader window is short; if it fails, retry with different timing. The +CONN3 "Update" jumper must be fitted so DTR can pull `/RESET`. + +Prebuilt images are attached to each [release](https://github.com/osuosl/proc/releases). + +**Connect:** + +```bash +telnet # default 192.168.1.2/24, no password +``` + +Set telnet to character mode first — see +[docs](doc/node-926-software-manual.md), which also covers getting a BIOS, +GRUB, kernel and getty console onto the serial port. + +To reach the menu from the managed host's serial port instead, send +**six `+`, six `u`, then Enter**. (The vendor manual says seven of each; that +changed in 2024 and the manual was never updated.) + +## Constraints worth knowing before you write code + +- **Flash is nearly full.** ~29 KB of 30720 bytes. CI fails below 512 bytes free. +- **One `.ino`, ~1400 lines, no headers.** Cite `file:line` in reviews. +- **Not secure by design.** Cleartext telnet, numeric PIN, no TLS, and none of + that is fixable on an ATmega328. Put these on an isolated management VLAN + behind a jump host. +- **The DS18B20 is the device identity.** Its 1-Wire ROM code becomes the serial + number, the MAC address and the default hostname. + +## Documentation + +| | | +|---|---| +| [AGENTS.md](AGENTS.md) | Deep technical reference — build, EEPROM map, menu, scripting, known issues | +| [CONTRIBUTING.md](CONTRIBUTING.md) | Sign-off requirement, branch model, upstream sync policy | +| [CHANGELOG.md](CHANGELOG.md) | What changed, per release | +| [doc/node-926-software-manual.md](doc/node-926-software-manual.md) | Vendor software manual, translated and annotated | +| [osuosl/prc](https://github.com/osuosl/prc) | The hardware: schematic, PCB, BOM, pin map | + +## License + +GPL-3.0, inherited from upstream. See [LICENSE](LICENSE). From 308f1458b2b0e3a1a26683f0fa2be40c5788d61e Mon Sep 17 00:00:00 2001 From: Lance Albertson Date: Sun, 26 Jul 2026 15:36:57 -0700 Subject: [PATCH 07/16] fix: make cross-repo doc links absolute, and add a link check prc's CI caught ~25 cross-repo links that resolve only in a workspace where both repositories are siblings. This repository had exactly the same bug and no check to catch it, so add the same job here. Cross-repo and out-of-tree targets become absolute URLs; links within the repository stay relative so they keep working on a branch or a fork. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Lance Albertson --- .github/workflows/ci.yml | 28 ++++++++++++++++++++++++++++ AGENTS.md | 8 ++++---- doc/node-926-software-manual.md | 4 ++-- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f798da6..ce3fecf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,6 +133,34 @@ jobs: path: dist if-no-files-found: error + links: + name: docs links + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + # Cross-repo links must be absolute URLs. A relative "../prc/..." resolves + # only in a workspace where both repos are siblings, and silently breaks + # for anyone who clones this repo on its own. + - name: Check relative links in markdown resolve + run: | + python3 - <<'PY' + import pathlib, re, sys + bad = 0 + for md in sorted(pathlib.Path('.').rglob('*.md')): + if '.git' in md.parts: + continue + for m in re.finditer(r'\[([^\]]*)\]\(([^)\s]+)\)', md.read_text(encoding='utf-8')): + target = m.group(2) + if target.startswith(('http://', 'https://', '#', 'mailto:')): + continue + if not (md.parent / target.split('#')[0]).exists(): + print(f"BROKEN {md}: [{m.group(1)}]({target})") + bad += 1 + print(f"\n{bad} broken relative link(s)") + sys.exit(1 if bad else 0) + PY + format: name: astyle runs-on: ubuntu-latest diff --git a/AGENTS.md b/AGENTS.md index 95232e3..88d2eec 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ **This is the repository to change if the task is "improve the PROC software".** -Workspace overview: [../AGENTS.md](../AGENTS.md) · Hardware: [../prc/AGENTS.md](../prc/AGENTS.md) +Hardware reference: [osuosl/prc](https://github.com/osuosl/prc/blob/main/AGENTS.md) **Last verified:** 2026-07-25 against `a8f458f` (2024-11-03), including an actual `arduino-cli` build. @@ -167,7 +167,7 @@ Everything else the build needs (`avr-gcc`, `avrdude`, `astyle`) is installed. - **FQBN `arduino:avr:pro:cpu=8MHzatmega328`** — "Arduino Pro or Pro Mini, ATmega328P 3.3 V 8 MHz". The board carries a **328PB**, but nothing here touches PB-only peripherals, so the stock 328P core is used. The alternative - `m328pb:avr:atmega328pbic` (from [../ATmega328PB](../ATmega328PB/AGENTS.md)) + `m328pb:avr:atmega328pbic` (from [lshw/ATmega328PB](https://github.com/lshw/ATmega328PB)) is commented out in `build.sh` and is not what ships. - **Libraries: `OneWire`** (Paul Stoffregen) and **`Ethernet3`** (W5500). The prototype in `../prc` used `Ethernet2` — do not confuse them. @@ -214,7 +214,7 @@ The bootloader window is short, so `update.sh` counts down 3-2-1 and you apply power to the board as it reaches 1. If it fails, retry with different timing. For this to work the **CONN3 "Update" jumper must be closed** so the adapter's -RTS/DTR can pull the MCU's `/RESET` — see [../prc/AGENTS.md](../prc/AGENTS.md) +RTS/DTR can pull the MCU's `/RESET` — see [osuosl/prc AGENTS.md](https://github.com/osuosl/prc/blob/main/AGENTS.md) §6.4. Leave it open in normal service so the managed PC cannot reset the controller. @@ -489,7 +489,7 @@ Operational notes for anyone debugging a wedged unit before this is deployed: lines you actually touch. - **Check the flash number on every build.** Over 30720 bytes will not fit. - **Do not change pin defines** without changing the hardware — the mapping is - fixed by the PCB. See [../prc/AGENTS.md](../prc/AGENTS.md) §6.2. + fixed by the PCB. See [osuosl/prc AGENTS.md](https://github.com/osuosl/prc/blob/main/AGENTS.md) §6.2. - **Write commit messages in English** in an OSL fork; add a Chinese subject line if you intend to send the patch upstream. - Upstream releases by tagging `YYYYMMDD-`; the newest tag is not diff --git a/doc/node-926-software-manual.md b/doc/node-926-software-manual.md index 24563af..7a8c2f4 100644 --- a/doc/node-926-software-manual.md +++ b/doc/node-926-software-manual.md @@ -8,7 +8,7 @@ > Translated 2026-07-25. Editorial notes added by the translator are marked > **[Note]** and are not part of the original. -**Companion page:** [Hardware installation manual](../../prc/doc/node-914-hardware-manual.md) +**Companion page:** [Hardware installation manual](https://github.com/osuosl/prc/blob/main/doc/node-914-hardware-manual.md) (original: https://bjlx.org.cn/node/914) > ⚠ **This page documents the firmware as of 2020.** Several details no longer @@ -209,7 +209,7 @@ Each script can be at most **50 characters**. The command format is: **[Note]** The zip contains `prcv1.hex`, `prcv1_pwm.hex` and `prcv1_pwm_autolink.hex`, all reporting `PROC-V1-20200523-d58845a`. That commit -is not in the public [`prc`](../../prc) repository. All three are near-full +is not in the public [`prc`](https://github.com/osuosl/prc) repository. All three are near-full flash images; `prcv1_pwm_autolink.hex` reaches 31406 bytes, which **exceeds the 30720 bytes left by the 2 KB bootloader current builds assume** — check a board's fuses before flashing that image onto it. See From 19e6eed95d926da61d2ec282d98a31a8b84a2732 Mon Sep 17 00:00:00 2001 From: Lance Albertson Date: Sun, 26 Jul 2026 15:42:11 -0700 Subject: [PATCH 08/16] ci: make the astyle check blocking It was advisory because the runner image ships a newer astyle than the 3.1 the tree is clean under, and a spurious formatting failure is worse than no check at all. Run 30223491125 confirms the runner produces no diff, so the version difference does not matter here. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Lance Albertson --- .github/workflows/ci.yml | 7 +++---- CONTRIBUTING.md | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce3fecf..eaf2d1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -164,10 +164,9 @@ jobs: format: name: astyle runs-on: ubuntu-latest - # Advisory for now. The tree is clean under astyle 3.1 with tools/formatter.conf, - # but the runner image ships a newer astyle whose defaults differ slightly. Flip - # this to a blocking check once a run confirms the runner produces no diff. - continue-on-error: true + # Blocking. Confirmed on run 30223491125 that the runner's astyle produces no + # diff against a tree formatted with astyle 3.1 and tools/formatter.conf, so + # this cannot fail spuriously on a version difference alone. steps: - uses: actions/checkout@v7 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 93f867c..a916644 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -77,7 +77,7 @@ retranslates his comments is unlikely to be merged, and rightly so. | Flash headroom ≥ 512 bytes free | yes | avr-gcc enforces the hard 30720 ceiling itself | | Size delta report | no | posted on the PR; watch it, headroom is ~1.6 KB | | Compiler warnings | no | reported only — there are ~28 already; don't add more | -| `astyle` formatting | not yet | advisory until a run confirms the runner's astyle matches ours | +| `astyle` formatting | yes | run `tools/format.sh` before pushing | Before pushing: From 8c4e67e6163fcc04f6573feadd54fb1d364cc7e2 Mon Sep 17 00:00:00 2001 From: Lance Albertson Date: Sun, 26 Jul 2026 16:16:32 -0700 Subject: [PATCH 09/16] ci: adopt release-please and conventional commits Versioning was a hand-rolled "tag vX.Y.Z and hope someone remembered to update CHANGELOG.md" process. release-please derives both from the commit history instead, so the changelog cannot drift from what actually shipped. release-please-config.json simple release type, tuned changelog sections .release-please-manifest.json pinned to 0.0.0 -- see below version.txt single source of truth, release-please owned .github/workflows/ release-please.yml maintains the release PR, tags, publishes pr-title.yml rejects unconventional PR titles The manifest is pinned explicitly because this repository carries a legacy tag, v2024-11-03, which is not valid semver. Without a manifest release-please would have to infer a starting point from that. Changelog sections are tuned for firmware rather than left at the defaults: docs, build and ci are visible, because in this repository a documentation or toolchain change is often the whole point of a release, and perf is labelled "Performance and flash savings" since with ~1.6KB of headroom a few hundred bytes reclaimed is a release-worthy event. release.yml no longer creates releases. release-please publishes the release; this workflow now triggers on release:published and attaches the firmware to it. Two workflows creating the same release is how you get a release with notes but no binaries. PR titles are linted because PRs are squash-merged, so the title becomes the commit subject on main -- which is what release-please parses. An unconventional title does not fail loudly at release time, the change just silently vanishes from the changelog. CHANGELOG.md keeps the hand-written fork context under a "Fork baseline" heading and hands everything above it to release-please. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Lance Albertson --- .github/workflows/pr-title.yml | 38 +++++++++ .github/workflows/release-please.yml | 24 ++++++ .github/workflows/release.yml | 118 +++++++++++++++------------ .release-please-manifest.json | 3 + CHANGELOG.md | 89 +++++++------------- CONTRIBUTING.md | 65 +++++++++++++-- release-please-config.json | 26 ++++++ version.txt | 1 + 8 files changed, 244 insertions(+), 120 deletions(-) create mode 100644 .github/workflows/pr-title.yml create mode 100644 .github/workflows/release-please.yml create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json create mode 100644 version.txt diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml new file mode 100644 index 0000000..7433640 --- /dev/null +++ b/.github/workflows/pr-title.yml @@ -0,0 +1,38 @@ +name: PR title + +on: + pull_request_target: + types: [opened, edited, synchronize, reopened] + +permissions: + pull-requests: read + +jobs: + conventional: + name: conventional commit format + runs-on: ubuntu-latest + steps: + # PRs are squash-merged, so the PR title becomes the commit subject on + # main -- which is what release-please parses. An unconventional title + # silently drops the change out of the changelog, so it is checked here + # rather than discovered at release time. + - uses: amannn/action-semantic-pull-request@v6.1.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + feat + fix + perf + docs + build + ci + refactor + test + chore + revert + requireScope: false + subjectPattern: ^(?![A-Z])(?!.*\.$).+$ + subjectPatternError: | + The subject "{subject}" is invalid: start lowercase and do not end + with a period. For example: "fix: reclaim auth slots on disconnect" diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..1a35b29 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,24 @@ +name: release-please + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@v5.0.0 + id: release + with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + + outputs: + released: ${{ steps.release.outputs.release_created }} + tag: ${{ steps.release.outputs.tag_name }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9a4a161..ce569ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,12 +1,17 @@ -name: Release +name: Release artifacts +# release-please owns versioning: merging its release PR bumps version.txt and +# CHANGELOG.md, tags vX.Y.Z, and publishes the GitHub Release. This workflow +# reacts to that publication and attaches the firmware to it. It deliberately +# does not create releases of its own -- two things creating the same release is +# how you end up with a release that has notes but no binaries, or vice versa. on: - push: - tags: ["v*"] + release: + types: [published] workflow_dispatch: inputs: tag: - description: "Tag to build (must already exist)" + description: "Existing tag to rebuild and re-upload" required: true permissions: @@ -20,26 +25,29 @@ env: MIN_FREE_FLASH: 512 jobs: - release: + artifacts: runs-on: ubuntu-latest steps: + - name: Resolve tag + id: tag + run: | + TAG="${{ inputs.tag || github.event.release.tag_name }}" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" + - uses: actions/checkout@v7 with: - ref: ${{ inputs.tag || github.ref }} + ref: ${{ steps.tag.outputs.tag }} fetch-depth: 0 - - name: Derive version + - name: Compose the version string id: ver run: | - TAG="${{ inputs.tag || github.ref_name }}" - VERSION="${TAG#v}" SHA="$(git rev-parse --short=7 HEAD)" - # This string is compiled into the image and shown in the telnet banner - # as "#Ver:PROC-V1-". The sha is included so a banner read off a - # deployed unit always maps to an exact commit, even if a tag is moved. - echo "tag=${TAG}" >> "$GITHUB_OUTPUT" - echo "version=${VERSION}" >> "$GITHUB_OUTPUT" - echo "git_ver=${VERSION}-g${SHA}" >> "$GITHUB_OUTPUT" + # Compiled into the image and shown in the telnet banner as + # "#Ver:PROC-V1-". The sha is included so a banner read off a + # deployed board maps to an exact commit even if a tag is moved. + echo "git_ver=${{ steps.tag.outputs.version }}-g${SHA}" >> "$GITHUB_OUTPUT" - uses: arduino/setup-arduino-cli@v2.0.0 @@ -49,14 +57,14 @@ jobs: arduino-cli core install "arduino:avr@${PLATFORM_VERSION}" arduino-cli lib install "OneWire@${ONEWIRE_VERSION}" arduino-cli lib install "Ethernet3@${ETHERNET3_VERSION}" - arduino-cli version > toolchain.txt - arduino-cli core list >> toolchain.txt - arduino-cli lib list >> toolchain.txt + { arduino-cli version; arduino-cli core list; arduino-cli lib list; } > toolchain.txt - name: Build release variants + id: build run: | set -euo pipefail mkdir -p dist + V="${{ steps.tag.outputs.version }}" build () { local name="$1" extra="$2" echo "::group::build ${name}" @@ -69,64 +77,70 @@ jobs: sed -n 's/^Sketch uses \([0-9]*\) bytes .*Maximum is \([0-9]*\) bytes\.$/\1 \2/p' "build-${name}.log" ) local FREE=$(( MAX - USED )) - echo "${name}: ${USED}/${MAX} used, ${FREE} free" if [ "${FREE}" -lt "${MIN_FREE_FLASH}" ]; then echo "::error::${name}: only ${FREE} bytes free, below ${MIN_FREE_FLASH}" exit 1 fi - cp "build-${name}/prc.ino.hex" "dist/proc-v1-${{ steps.ver.outputs.version }}-${name}.hex" - cp "build-${name}/prc.ino.elf" "dist/proc-v1-${{ steps.ver.outputs.version }}-${name}.elf" - echo "${name}|${USED}|${MAX}|${FREE}" >> sizes.txt + cp "build-${name}/prc.ino.hex" "dist/proc-v1-${V}-${name}.hex" + cp "build-${name}/prc.ino.elf" "dist/proc-v1-${V}-${name}.elf" + printf '| `%s` | %s | %s | %s |\n' "${name}" "${USED}" "${MAX}" "${FREE}" >> sizes.md echo "::endgroup::" } build stock "" build pwm "-DPWM=5" cp toolchain.txt dist/ - ( cd dist && sha256sum ./*.hex ./*.elf > SHA256SUMS ) + ( cd dist && sha256sum ./* > SHA256SUMS ) - - name: Compose release notes - id: notes + - name: Build summary run: | { - echo "## PROC-V1 firmware ${{ steps.ver.outputs.tag }}" + echo "### ${{ steps.tag.outputs.tag }} — \`PROC-V1-${{ steps.ver.outputs.git_ver }}\`" echo "" - echo "Banner string on a flashed unit:" + echo "| variant | flash used | max | free |" + echo "|---|---:|---:|---:|" + cat sizes.md + } >> "$GITHUB_STEP_SUMMARY" + + - name: Attach artifacts to the release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release upload "${{ steps.tag.outputs.tag }}" dist/* --clobber + + - name: Append build details to the release notes + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Best effort: release-please wrote the changelog section, this adds the + # operational half. Never fail the release over formatting. + continue-on-error: true + run: | + BODY="$(gh release view "${{ steps.tag.outputs.tag }}" --json body --jq .body)" + { + printf '%s\n\n' "${BODY}" + echo "---" + echo "" + echo "### Firmware" + echo "" + echo "Banner string once flashed:" echo "" echo '```' echo "#Ver:PROC-V1-${{ steps.ver.outputs.git_ver }}" echo '```' echo "" - echo "### Builds" - echo "" echo "| variant | flash used | max | free |" echo "|---|---:|---:|---:|" - while IFS='|' read -r n used max free; do - echo "| \`${n}\` | ${used} | ${max} | ${free} |" - done < sizes.txt + cat sizes.md echo "" - echo "\`stock\` is the build to flash unless the board has the V1.1 PWM pad populated." - echo "" - echo "### Flashing" + echo "Flash \`stock\` unless the board has the V1.1 PWM pad populated." echo "" echo '```bash' - echo "sudo systemctl stop getty@ttyS0 # free the port if flashing from the managed host" + echo "sudo systemctl stop getty@ttyS0 # if flashing from the managed host" echo "avrdude -v -patmega328p -carduino -P/dev/ttyUSB0 -b57600 -D \\" - echo " -Uflash:w:proc-v1-${{ steps.ver.outputs.version }}-stock.hex:i" + echo " -Uflash:w:proc-v1-${{ steps.tag.outputs.version }}-stock.hex:i" echo '```' echo "" - echo "Power the board as the countdown reaches 1; the bootloader window is short." - echo "Requires the CONN3 \"Update\" jumper so DTR can pull /RESET." - echo "" - echo "Verify with \`SHA256SUMS\`. \`toolchain.txt\` records the exact core and" - echo "library versions used, since this image fills ~95% of flash and is" - echo "sensitive to toolchain drift." + echo "Power the board as the countdown reaches 1; the bootloader window is" + echo "short. Requires the CONN3 \"Update\" jumper so DTR can pull /RESET." + echo "Verify against \`SHA256SUMS\`; \`toolchain.txt\` records the exact core" + echo "and library versions, which matters for an image this close to full." } > notes.md - cat notes.md >> "$GITHUB_STEP_SUMMARY" - - - uses: softprops/action-gh-release@v3.0.2 - with: - tag_name: ${{ steps.ver.outputs.tag }} - name: ${{ steps.ver.outputs.tag }} - body_path: notes.md - files: dist/* - fail_on_unmatched_files: true + gh release edit "${{ steps.tag.outputs.tag }}" --notes-file notes.md diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..e18ee07 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.0.0" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 668b142..005d5cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,71 +1,42 @@ # Changelog -Notable changes to OSU OSL's fork of [`lshw/proc`](https://github.com/lshw/proc). -Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); -versioning is [SemVer](https://semver.org/). +**This file is maintained by [release-please](https://github.com/googleapis/release-please).** +Do not edit the generated sections by hand — write +[conventional commits](CONTRIBUTING.md#commit-messages) and they will appear +here on the next release. The version is compiled into the firmware and shown in the telnet banner as `#Ver:PROC-V1--g`, so a banner read off a deployed board maps to an exact commit. -## [Unreleased] + -### Fixed +## Fork baseline -- **The telnet server no longer goes permanently deaf after abandoned sessions.** - `new_link()` returned early whenever `server.available()` yielded no client, - but Ethernet3 only returns a socket that has *unread bytes waiting*, and the - pending-slot timeout handling lived after that return. Three abandoned - sessions — a closed terminal, a slept laptop, a dropped tunnel — pinned all - three authentication slots forever, and every later connection was accepted by - the W5500 but never given a slot: TCP connects, no `passwd:` prompt, silence. - Only a reboot cleared it. Also: slots are now reclaimed the moment a peer - disconnects rather than after the full 20 s timeout; deadlines survive the - ~49-day `millis()` rollover; and a connection arriving with every slot busy is - refused with `busy` instead of being left accepted but unread, where it would - mask every later connection. (+48 bytes flash) -- **`-DPWM=5` builds compile again.** `pwm` was defined twice at namespace - scope, which is a hard error in C++, so the PWM variant had been unbuildable - since the 2024 cleanup. Also removed the unused `vout` global. (−42 bytes - flash in the stock build) - -### Changed - -- **All Chinese text translated to English** — every comment in `prc/prc.ino`, - the messages and comments in `build.sh`, and `README.md`. Comments only: the - compiled image is byte-identical apart from the embedded `__TIME__` literal. -- `README.md` rewritten for a maintained fork. - -### Added +Context for everything above, written by hand before release automation existed. -- **CI** — both build variants, pinned toolchain, flash-headroom gate at 512 - bytes free, size-delta reports on PRs, compiler-warning reports, downloadable - firmware artifact per commit. -- **Release pipeline** — tagging `vX.Y.Z` builds both variants and attaches - `.hex`, `.elf`, `SHA256SUMS` and `toolchain.txt`. -- `AGENTS.md` — technical reference: build, EEPROM layout, menu, scripting, - session handling, and the verified issue list. -- `doc/node-926-software-manual.md` — the vendor software manual translated and - annotated wherever it no longer matches the code. -- `CONTRIBUTING.md`, this changelog, and `tools/` (vendored astyle config from - `lshw/procV2`, plus `format.sh`). +OSU OSL's fork of [`lshw/proc`](https://github.com/lshw/proc) by Liu Shiwei, +who designed the hardware and wrote the firmware. Fork point is +[`a8f458f`](https://github.com/lshw/proc/commit/a8f458f) (2024-11-03). Upstream +keeps no changelog; its history is in `git log`. -### Known issues +The fork exists because OSL runs several of these boards and hit a bug in +production. Two fixes landed before automation: -Tracked at . The ones to know about: - -- Menu option `n` (change name) falls through and fires a 300 ms reset at the - managed host. -- The menu prints `V:Vout=` but only lowercase `v` is handled. -- `ds1820()` reads one byte past the end of an 8-byte ROM code. -- A board whose DS18B20 has failed factory-resets on every boot. -- The `WATCHDOG*` EEPROM slots are written but never read — the host watchdog - they describe was never implemented. -- Authentication is a numeric PIN over cleartext telnet. Not fixable on this - MCU; treat these as management-VLAN-only appliances. - ---- - -Upstream history before the fork is in `git log`; upstream does not keep a -changelog. The fork point is -[`a8f458f`](https://github.com/lshw/proc/commit/a8f458f) (2024-11-03). +- **The telnet server went permanently deaf after abandoned sessions.** + `new_link()` returned early whenever `server.available()` yielded no client, + but Ethernet3 only returns a socket with *unread bytes waiting*, and the + pending-slot timeout handling lived after that return. Three abandoned + sessions pinned all three authentication slots forever; every later connection + was accepted by the W5500 but never given a slot, so telnet connected and then + sat silent until it timed out. Only a reboot cleared it. +- **`-DPWM=5` had been unbuildable since 2024** — `pwm` was defined twice at + namespace scope. + +Also in the baseline: all Chinese text translated to English (comments only — +the compiled image is byte-identical apart from the embedded `__TIME__`), the +vendor manuals translated under `doc/`, `AGENTS.md` as a technical reference, +and CI covering both build variants with a flash-headroom gate. + +**Nothing in the baseline was tested on hardware.** The `new_link()` fix in +particular wants a real node before it is trusted in a release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a916644..f6c0f61 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,37 @@ # Contributing +## Commit messages + +We use [Conventional Commits](https://www.conventionalcommits.org/), because +[release-please](https://github.com/googleapis/release-please) derives the +version bump and the changelog from them. + +``` +fix: reclaim auth slots when a peer disconnects + +Longer explanation of why, what breaks without it, and what you tested. + +Refs: #14 +Signed-off-by: Your Name +``` + +| Type | Use for | Bumps | +|---|---|---| +| `feat` | new capability | minor | +| `fix` | a defect | patch | +| `perf` | flash or RAM savings, speedups | patch | +| `docs`, `build`, `ci`, `refactor`, `test`, `chore` | everything else | none | + +Breaking changes get a `!` (`feat!: ...`) or a `BREAKING CHANGE:` footer. For +this firmware "breaking" means something an operator would be caught out by — +an EEPROM layout change that discards settings, a changed default, a renamed +menu key, or a MAC address change. + +**⚠ PRs are squash-merged, so the PR *title* becomes the commit on `main`.** +That title is what release-please parses, so it has to be conventional too. CI +checks it. An unconventional title doesn't fail loudly at release time — the +change just silently vanishes from the changelog. + ## Sign off every commit ```bash @@ -10,6 +42,28 @@ Every commit needs a `Signed-off-by:` trailer (the [DCO](https://developercertificate.org/)). PRs without it will be asked to amend. `-s` generates it from your git identity; don't write the line by hand. +## Releases + +Merging to `main` makes release-please open (or update) a release PR that bumps +`version.txt` and `CHANGELOG.md`. **Merging that PR is the release**: it tags +`vX.Y.Z`, publishes the GitHub Release, and triggers the build that attaches +`.hex`, `.elf`, `SHA256SUMS` and `toolchain.txt`. + +To force a specific version, put `Release-As: 1.0.0` in a commit footer. + +The version is compiled into the image and shown in the telnet banner as +`#Ver:PROC-V1-X.Y.Z-g`, so a banner read off a deployed board always maps to +an exact commit. Never hand-edit [CHANGELOG.md](CHANGELOG.md) or `version.txt`; +release-please owns both. + +Two things to know: + +- Release PRs are created with `GITHUB_TOKEN`, so **CI does not run on them.** + That is a GitHub restriction, not a misconfiguration. The content is generated + from commits that already passed CI on `main`. +- **Nothing is hardware-tested by CI.** A green release means it compiles and + fits. Flash a spare board before rolling a release out to a fleet. + ## ⚠ Check the base repo on every PR This repository is a **fork**, so GitHub defaults the base of a new pull request @@ -78,6 +132,8 @@ retranslates his comments is unlikely to be merged, and rightly so. | Size delta report | no | posted on the PR; watch it, headroom is ~1.6 KB | | Compiler warnings | no | reported only — there are ~28 already; don't add more | | `astyle` formatting | yes | run `tools/format.sh` before pushing | +| Conventional PR title | yes | it becomes the squash commit release-please reads | +| Relative markdown links resolve | yes | cross-repo links must be absolute URLs | Before pushing: @@ -99,12 +155,3 @@ tools/format.sh # astyle, using upstream's config - **Say what you tested.** "Builds and fits" and "tested on hardware" are very different claims. Put whichever is true in the commit message — several existing commits say "not yet tested on hardware" for exactly this reason. - -## Releases - -Tag `vX.Y.Z` on `main`. The release workflow builds both variants, enforces the -size gate, and attaches `.hex`, `.elf`, `SHA256SUMS` and `toolchain.txt`. - -The version is compiled into the image and shown in the telnet banner as -`#Ver:PROC-V1-X.Y.Z-g`, so a banner read off a deployed board always maps to -an exact commit. Update [CHANGELOG.md](CHANGELOG.md) in the same PR as the tag. diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..59b008a --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "release-type": "simple", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "include-component-in-tag": false, + "separate-pull-requests": false, + "packages": { + ".": { + "package-name": "proc", + "changelog-path": "CHANGELOG.md" + } + }, + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance and flash savings" }, + { "type": "revert", "section": "Reverts" }, + { "type": "docs", "section": "Documentation" }, + { "type": "build", "section": "Build System" }, + { "type": "ci", "section": "Continuous Integration" }, + { "type": "refactor", "section": "Refactoring" }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "chore", "section": "Miscellaneous", "hidden": true } + ] +} diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..77d6f4c --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +0.0.0 From daa2c25b5737eb2f4ba9434e2e39766c759df188 Mon Sep 17 00:00:00 2001 From: Lance Albertson Date: Sun, 26 Jul 2026 16:17:35 -0700 Subject: [PATCH 10/16] ci: pin release-please bootstrap-sha to the fork point Without it release-please scans the entire upstream history on every run. Those commits are Chinese and non-conventional so they produce no changelog entries either way, but pinning makes the intent explicit: only commits after a8f458f, the last upstream commit, are ours to release. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Lance Albertson --- release-please-config.json | 55 ++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/release-please-config.json b/release-please-config.json index 59b008a..183e027 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -12,15 +12,48 @@ } }, "changelog-sections": [ - { "type": "feat", "section": "Features" }, - { "type": "fix", "section": "Bug Fixes" }, - { "type": "perf", "section": "Performance and flash savings" }, - { "type": "revert", "section": "Reverts" }, - { "type": "docs", "section": "Documentation" }, - { "type": "build", "section": "Build System" }, - { "type": "ci", "section": "Continuous Integration" }, - { "type": "refactor", "section": "Refactoring" }, - { "type": "test", "section": "Tests", "hidden": true }, - { "type": "chore", "section": "Miscellaneous", "hidden": true } - ] + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "perf", + "section": "Performance and flash savings" + }, + { + "type": "revert", + "section": "Reverts" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "build", + "section": "Build System" + }, + { + "type": "ci", + "section": "Continuous Integration" + }, + { + "type": "refactor", + "section": "Refactoring" + }, + { + "type": "test", + "section": "Tests", + "hidden": true + }, + { + "type": "chore", + "section": "Miscellaneous", + "hidden": true + } + ], + "bootstrap-sha": "a8f458f3726aa54334e9205a058a9431f81abf98" } From d925f4d8184725ddd4dac83dcf528eb3a89e1137 Mon Sep 17 00:00:00 2001 From: Lance Albertson Date: Sun, 26 Jul 2026 16:46:42 -0700 Subject: [PATCH 11/16] ci: lint every commit instead of the PR title We are not squash-merging, so the PR title never becomes a commit on main and linting it checks nothing that matters. Every individual commit is what lands, what release-please parses, and what the changelog is built from. Replaces pr-title.yml with commits.yml, which walks every non-merge commit in the PR and requires a conventional subject and a Signed-off-by trailer. It also rejects a capitalised subject or a trailing period, so release notes read consistently. The failure mode this guards against is quiet: a non-conventional subject does not break the release, the change just silently never appears in the changelog. CONTRIBUTING.md gains a Merging section stating the rule and the reason -- squashing would collapse a branch's distinct fixes into one changelog line and discard the individual sign-offs. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Lance Albertson --- .github/workflows/commits.yml | 61 ++++++++++++++++++++++++++++++++++ .github/workflows/pr-title.yml | 38 --------------------- CONTRIBUTING.md | 25 +++++++++++--- 3 files changed, 81 insertions(+), 43 deletions(-) create mode 100644 .github/workflows/commits.yml delete mode 100644 .github/workflows/pr-title.yml diff --git a/.github/workflows/commits.yml b/.github/workflows/commits.yml new file mode 100644 index 0000000..f6a9ee5 --- /dev/null +++ b/.github/workflows/commits.yml @@ -0,0 +1,61 @@ +name: Commits + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + lint: + name: conventional + sign-off + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + # Every commit is preserved on main (we do not squash), so every commit is + # what release-please parses and what the changelog is built from. A + # non-conventional subject does not fail at release time -- the change just + # silently never appears in the changelog. So it is checked here instead. + - name: Check every commit in this PR + env: + BASE: ${{ github.event.pull_request.base.sha }} + HEAD: ${{ github.event.pull_request.head.sha }} + run: | + python3 - <<'PY' + import os, re, subprocess, sys + + TYPES = "feat|fix|perf|docs|build|ci|refactor|test|chore|revert" + SUBJECT = re.compile(rf"^({TYPES})(\([a-z0-9._/-]+\))?!?: .+") + + rng = f"{os.environ['BASE']}..{os.environ['HEAD']}" + shas = subprocess.run( + ["git", "rev-list", "--no-merges", rng], + capture_output=True, text=True, check=True, + ).stdout.split() + + bad = [] + for sha in shas: + msg = subprocess.run( + ["git", "log", "-1", "--format=%B", sha], + capture_output=True, text=True, check=True, + ).stdout + subject = msg.splitlines()[0] if msg.strip() else "" + short = sha[:8] + if not SUBJECT.match(subject): + bad.append(f"{short} not conventional: {subject!r}") + elif subject[len(subject.split(':')[0]) + 2:][:1].isupper(): + bad.append(f"{short} subject should start lowercase: {subject!r}") + if subject.endswith("."): + bad.append(f"{short} subject should not end with a period: {subject!r}") + if "Signed-off-by:" not in msg: + bad.append(f"{short} missing Signed-off-by (commit with -s): {subject!r}") + + print(f"checked {len(shas)} commit(s) in {rng}") + for line in bad: + print(f"::error::{line}") + if bad: + print("\nFix with: git rebase -i --signoff " + os.environ["BASE"]) + sys.exit(1) + print("all commits OK") + PY diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml deleted file mode 100644 index 7433640..0000000 --- a/.github/workflows/pr-title.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: PR title - -on: - pull_request_target: - types: [opened, edited, synchronize, reopened] - -permissions: - pull-requests: read - -jobs: - conventional: - name: conventional commit format - runs-on: ubuntu-latest - steps: - # PRs are squash-merged, so the PR title becomes the commit subject on - # main -- which is what release-please parses. An unconventional title - # silently drops the change out of the changelog, so it is checked here - # rather than discovered at release time. - - uses: amannn/action-semantic-pull-request@v6.1.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - types: | - feat - fix - perf - docs - build - ci - refactor - test - chore - revert - requireScope: false - subjectPattern: ^(?![A-Z])(?!.*\.$).+$ - subjectPatternError: | - The subject "{subject}" is invalid: start lowercase and do not end - with a period. For example: "fix: reclaim auth slots on disconnect" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f6c0f61..c50817d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,10 +27,14 @@ this firmware "breaking" means something an operator would be caught out by — an EEPROM layout change that discards settings, a changed default, a renamed menu key, or a MAC address change. -**⚠ PRs are squash-merged, so the PR *title* becomes the commit on `main`.** -That title is what release-please parses, so it has to be conventional too. CI -checks it. An unconventional title doesn't fail loudly at release time — the -change just silently vanishes from the changelog. +**⚠ We do not squash-merge.** Every commit you write lands on `main` intact, +which means **every commit** is parsed by release-please and shows up in the +changelog on its own. Write them accordingly: one logical change each, with a +subject that reads well in a release note. + +CI checks every commit in a PR for both a conventional subject and a +`Signed-off-by` trailer. A non-conventional subject doesn't fail at release +time — the change just silently never appears in the changelog. ## Sign off every commit @@ -64,6 +68,17 @@ Two things to know: - **Nothing is hardware-tested by CI.** A green release means it compiles and fits. Flash a spare board before rolling a release out to a fleet. +## Merging + +**Merge commit or rebase — never squash.** Squashing collapses a branch into one +commit, which would reduce a PR's worth of distinct fixes to a single changelog +line and lose the individual sign-offs. Squash merging is disabled on the +repository for that reason. + +Keep the branch tidy before it merges, since nothing will tidy it afterwards: +fold up "fix typo" commits with `git rebase -i`, and make sure each surviving +commit is one logical change. + ## ⚠ Check the base repo on every PR This repository is a **fork**, so GitHub defaults the base of a new pull request @@ -132,7 +147,7 @@ retranslates his comments is unlikely to be merged, and rightly so. | Size delta report | no | posted on the PR; watch it, headroom is ~1.6 KB | | Compiler warnings | no | reported only — there are ~28 already; don't add more | | `astyle` formatting | yes | run `tools/format.sh` before pushing | -| Conventional PR title | yes | it becomes the squash commit release-please reads | +| Conventional subject + sign-off, every commit | yes | commits land on `main` intact and drive the changelog | | Relative markdown links resolve | yes | cross-repo links must be absolute URLs | Before pushing: From cda623b8e249a7f024681f221445631dde2f929f Mon Sep 17 00:00:00 2001 From: Lance Albertson Date: Sun, 26 Jul 2026 16:54:26 -0700 Subject: [PATCH 12/16] ci: add dependabot with grouped github-actions updates Weekly, and grouped: one PR covering every action rather than one PR per action. Majors are in the group too, so a breaking bump shows up as a red CI run on that PR instead of arriving silently later. Two integration details that would otherwise bite: - Dependabot commits are prefixed "ci(deps):" via commit-message.prefix. Without that they would not be conventional, and every Dependabot PR would fail the commit lint added in the previous commit. - Dependabot has no DCO option, so commits.yml now exempts bot authors from the sign-off requirement. They are not exempt from the format check: if the prefix is ever dropped from dependabot.yml, CI catches it. The same exemption covers release-please's own "chore(main): release X" commit. github-actions is the only ecosystem that applies. There is none for arduino-cli, so the core and library pins stay manual, which is documented in CONTRIBUTING.md along with why: this firmware sits at ~95% of flash, and a toolchain bump can push it over the ceiling or change generated code. It wants a human reading the size delta, not an automerge. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Lance Albertson --- .github/dependabot.yml | 37 +++++++++++++++++++++++++++++++++++ .github/workflows/commits.yml | 12 +++++++++++- CONTRIBUTING.md | 25 +++++++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..2524441 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,37 @@ +version: 2 + +# Only github-actions applies here. There is no Dependabot ecosystem for +# arduino-cli, so the core and library pins in .github/workflows/*.yml +# (PLATFORM_VERSION, ONEWIRE_VERSION, ETHERNET3_VERSION) are NOT tracked below +# and have to be reviewed by hand. That is deliberate rather than an oversight: +# this firmware sits at ~95% of flash, so a toolchain bump can push it over the +# ceiling and wants a human looking at the size delta. +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + time: "09:00" + timezone: America/Los_Angeles + + # One PR for everything, rather than one per action. + groups: + github-actions: + patterns: + - "*" + update-types: + - minor + - patch + - major + + # Produces "ci(deps): bump the github-actions group with 3 updates", which + # satisfies the conventional-commit check in .github/workflows/commits.yml. + # Without this prefix every Dependabot PR would fail that check. + commit-message: + prefix: ci + include: scope + + labels: + - dependencies + open-pull-requests-limit: 3 diff --git a/.github/workflows/commits.yml b/.github/workflows/commits.yml index f6a9ee5..47fbedf 100644 --- a/.github/workflows/commits.yml +++ b/.github/workflows/commits.yml @@ -34,21 +34,31 @@ jobs: capture_output=True, text=True, check=True, ).stdout.split() + # Bots cannot sign off: Dependabot has no DCO option. Its commits still + # have to be conventional, because they land on main and appear in the + # changelog like any other. + BOTS = ("dependabot[bot]", "github-actions[bot]", "release-please[bot]") + bad = [] for sha in shas: msg = subprocess.run( ["git", "log", "-1", "--format=%B", sha], capture_output=True, text=True, check=True, ).stdout + author = subprocess.run( + ["git", "log", "-1", "--format=%an <%ae>", sha], + capture_output=True, text=True, check=True, + ).stdout.strip() subject = msg.splitlines()[0] if msg.strip() else "" short = sha[:8] + is_bot = any(b in author for b in BOTS) if not SUBJECT.match(subject): bad.append(f"{short} not conventional: {subject!r}") elif subject[len(subject.split(':')[0]) + 2:][:1].isupper(): bad.append(f"{short} subject should start lowercase: {subject!r}") if subject.endswith("."): bad.append(f"{short} subject should not end with a period: {subject!r}") - if "Signed-off-by:" not in msg: + if not is_bot and "Signed-off-by:" not in msg: bad.append(f"{short} missing Signed-off-by (commit with -s): {subject!r}") print(f"checked {len(shas)} commit(s) in {rng}") diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c50817d..1e9dd29 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -138,6 +138,31 @@ gh pr create --repo lshw/proc --base main Send the **behaviour change alone**, without the translation — a patch that also retranslates his comments is unlikely to be merged, and rightly so. +## Dependency updates + +Dependabot opens a **single grouped PR** for GitHub Actions, weekly on Monday. +One PR for all actions rather than one per action; if a major bump breaks +something, CI says so on that PR. + +Its commits are prefixed `ci(deps):` so they satisfy the conventional-commit +check, and bot authors are exempt from the sign-off requirement because +Dependabot has no DCO option. They are *not* exempt from the format check — if +that prefix is ever removed from `.github/dependabot.yml`, CI will catch it. + +**⚠ The Arduino pins are not covered.** There is no Dependabot ecosystem for +`arduino-cli`, so these, in `.github/workflows/*.yml`, have to be reviewed by +hand: + +| Pin | Current | +|---|---| +| `PLATFORM_VERSION` (`arduino:avr`) | 1.8.8 | +| `ONEWIRE_VERSION` | 2.3.8 | +| `ETHERNET3_VERSION` | 1.6.0 | + +That is deliberate. This firmware sits at ~95% of a 30720 byte flash, so a core +or library bump can push it over the ceiling, change generated code, or alter +timing. Bump one at a time, on its own PR, and read the size delta CI posts. + ## What CI enforces | Check | Blocking | Notes | From c7e6e4eede04fb48a9fc0cef72ca8889352f6369 Mon Sep 17 00:00:00 2001 From: Lance Albertson Date: Sun, 26 Jul 2026 17:02:16 -0700 Subject: [PATCH 13/16] ci: stop dependabot updates from triggering a release Dependabot was prefixing its commits "ci(deps):". That was enough to pass the commit lint, but "ci" is visible in changelog-sections here on purpose, because a toolchain change is often the whole point of a release in this repository. So action bumps would have shown up in release notes that are read by someone deciding whether to reflash a board. Switch the prefix to "chore(deps):", which fixes both halves: - release-please's default versioning strategy bumps only for feat, fix and breaking changes, so a chore commit cannot cause a release on its own. Merging a Dependabot PR no longer mints a firmware version. - chore is already hidden in changelog-sections, so the bumps stay out of the notes while remaining in git history where they belong. It still has to be a conventional prefix at all, or every Dependabot PR would fail the commit check -- that part is unchanged, and the check still catches the prefix being removed. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Lance Albertson --- .github/dependabot.yml | 16 ++++++++++++---- .github/workflows/commits.yml | 7 ++++--- CONTRIBUTING.md | 16 ++++++++++++---- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2524441..20913a5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -25,11 +25,19 @@ updates: - patch - major - # Produces "ci(deps): bump the github-actions group with 3 updates", which - # satisfies the conventional-commit check in .github/workflows/commits.yml. - # Without this prefix every Dependabot PR would fail that check. + # Produces "chore(deps): bump the github-actions group with 3 updates". + # + # "chore" is deliberate, not cosmetic. Under release-please's default + # versioning strategy only feat, fix and breaking changes bump a version, so + # a chore commit cannot cause a release on its own -- merging a Dependabot PR + # will not mint a new firmware version. "chore" is also hidden in + # changelog-sections, so action bumps stay out of release notes that are read + # by people deciding whether to reflash a board. + # + # It also has to be *some* conventional prefix: without one, every Dependabot + # PR would fail the check in .github/workflows/commits.yml. commit-message: - prefix: ci + prefix: chore include: scope labels: diff --git a/.github/workflows/commits.yml b/.github/workflows/commits.yml index 47fbedf..d1d55ec 100644 --- a/.github/workflows/commits.yml +++ b/.github/workflows/commits.yml @@ -34,9 +34,10 @@ jobs: capture_output=True, text=True, check=True, ).stdout.split() - # Bots cannot sign off: Dependabot has no DCO option. Its commits still - # have to be conventional, because they land on main and appear in the - # changelog like any other. + # Bots cannot sign off: Dependabot has no DCO option. Their commits still + # have to be conventional -- Dependabot uses chore(deps), which keeps it + # out of the changelog and stops it triggering a release, but it still + # has to parse. BOTS = ("dependabot[bot]", "github-actions[bot]", "release-please[bot]") bad = [] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1e9dd29..ecd13d9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -144,10 +144,18 @@ Dependabot opens a **single grouped PR** for GitHub Actions, weekly on Monday. One PR for all actions rather than one per action; if a major bump breaks something, CI says so on that PR. -Its commits are prefixed `ci(deps):` so they satisfy the conventional-commit -check, and bot authors are exempt from the sign-off requirement because -Dependabot has no DCO option. They are *not* exempt from the format check — if -that prefix is ever removed from `.github/dependabot.yml`, CI will catch it. +Its commits are prefixed **`chore(deps):`**, which does three things: + +- **No release.** Only `feat`, `fix` and breaking changes bump a version, so + merging a Dependabot PR never mints a new firmware version on its own. +- **No changelog noise.** `chore` is hidden, so action bumps stay out of release + notes read by someone deciding whether to reflash a board. +- **It passes CI.** Without a conventional prefix every Dependabot PR would fail + the commit check. + +Bot authors are exempt from the sign-off requirement, because Dependabot has no +DCO option. They are *not* exempt from the format check — if that prefix is ever +removed from `.github/dependabot.yml`, CI will catch it. **⚠ The Arduino pins are not covered.** There is no Dependabot ecosystem for `arduino-cli`, so these, in `.github/workflows/*.yml`, have to be reviewed by From 699bdf3a4728951f99ae88dfb5d1a9f4f24e958a Mon Sep 17 00:00:00 2001 From: Lance Albertson Date: Sun, 26 Jul 2026 19:27:25 -0700 Subject: [PATCH 14/16] docs: describe the branch protection now enforced on main main is protected: changes go through a PR, all status checks must pass, force pushes and deletions are blocked, and conversations must be resolved. Two settings are deliberately off, and both would look like mistakes without an explanation, so they are written down: - Linear history is NOT required. Requiring it would reject the merge commits we switched to so release-please sees individual commits. - Approvals are NOT required (0). Requiring one would stop a solo maintainer landing a fix, which matters when the thing being fixed is the out-of-band access to a machine that is already down. Admin enforcement is also off, for a specific reason: release-please's PR is created with GITHUB_TOKEN, and a token-created PR does not trigger workflows, so its required checks never report and it shows as blocked forever. Admin bypass makes the release mergeable today; the proper fix is a fine-grained PAT or App token passed to the action, which is documented so nobody has to rediscover why the first release would not merge. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Lance Albertson --- CONTRIBUTING.md | 46 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ecd13d9..e140069 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,12 +62,50 @@ release-please owns both. Two things to know: -- Release PRs are created with `GITHUB_TOKEN`, so **CI does not run on them.** - That is a GitHub restriction, not a misconfiguration. The content is generated - from commits that already passed CI on `main`. +- Release PRs are created with `GITHUB_TOKEN`, so **CI does not run on them**, + which means they show as blocked by branch protection. See + [Branch protection](#branch-protection) for why and how to fix it properly. - **Nothing is hardware-tested by CI.** A green release means it compiles and fits. Flash a spare board before rolling a release out to a fleet. +## Branch protection + +`main` is protected: + +| Rule | Setting | +|---|---| +| Changes must go through a PR | yes, **0 approvals required** | +| Required status checks | all of them (see below) | +| Force pushes / deletions | blocked | +| Conversation resolution | required | +| Linear history | **not** required — merge commits are the workflow | +| Admin enforcement | **off** — see the release caveat below | + +Approvals are not required so a solo maintainer can still land a fix. Review +anyway when there is someone to review. + +### ⚠ The release PR will look blocked + +release-please's PR is created with `GITHUB_TOKEN`, and by GitHub's design a +token-created PR **does not trigger workflows**. So the release PR gets no CI +runs, its required checks never report, and it shows as blocked. + +Admin enforcement is deliberately off so a repo admin can merge it anyway. That +is a workaround, not a fix. + +The proper fix is to give release-please a token that isn't `GITHUB_TOKEN` — a +fine-grained PAT or a GitHub App installation token — stored as a secret and +passed to the action: + +```yaml +- uses: googleapis/release-please-action@v5.0.0 + with: + token: ${{ secrets.RELEASE_PLEASE_TOKEN }} +``` + +Then its PRs trigger CI like any other and admin bypass is no longer needed. +Worth doing once someone with org permissions can mint the token. + ## Merging **Merge commit or rebase — never squash.** Squashing collapses a branch into one @@ -96,7 +134,7 @@ each time. ## Branch model -- `main` is the maintained line. It is protected and takes changes by PR. +- `main` is the maintained line — see [Branch protection](#branch-protection). - Work on a topic branch, open a PR, get CI green. - `upstream/main` tracks [`lshw/proc`](https://github.com/lshw/proc). From b425e67bad128db54ee1483856e3b07285a9a249 Mon Sep 17 00:00:00 2001 From: Lance Albertson Date: Sun, 26 Jul 2026 21:07:17 -0700 Subject: [PATCH 15/16] chore: release the fork baseline as 0.1.0 Every commit on this branch is a fix, documentation or CI, so release-please would compute a patch bump and cut 0.0.1. That undersells what this is: the point where the fork became something OSL maintains -- English documentation, a production bug fix, CI and a release pipeline. 0.1.0 says "first usable release of the fork" while leaving 1.0.0 for when the firmware has actually been verified on hardware, which none of it has been yet. Using the footer rather than relabelling a commit as feat: nothing here adds firmware capability, and the changelog should stay honest about that. The override applies to this release only; the next version is computed normally. Release-As: 0.1.0 Signed-off-by: Lance Albertson From b1e2193ee25cc60842c6119af7b13d6e302e8ee7 Mon Sep 17 00:00:00 2001 From: Lance Albertson Date: Sun, 26 Jul 2026 21:15:46 -0700 Subject: [PATCH 16/16] ci: let release-please use a PAT when one is configured The action now reads secrets.RELEASE_PLEASE_TOKEN and falls back to github.token when it is unset, so this changes nothing until the secret exists and needs no coordination to land. Why it matters: a PR created with GITHUB_TOKEN does not trigger workflows, so release-please's PR gets no CI runs, its required checks never report, and branch protection shows it as blocked. Admin enforcement is currently off to work around that. With a real token the release PR runs CI like any other and the bypass can be turned back on. CONTRIBUTING.md gains the full procedure: which permissions the token needs (contents and pull-requests, read and write, nothing else), how to store it once at the org for both repos, the command to re-enable admin enforcement afterwards, and the caveat that a user PAT makes release PRs appear authored by that user. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Lance Albertson --- .github/workflows/release-please.yml | 7 ++++ CONTRIBUTING.md | 51 ++++++++++++++++++++++------ 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 1a35b29..539414e 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -16,6 +16,13 @@ jobs: - uses: googleapis/release-please-action@v5.0.0 id: release with: + # Falls back to GITHUB_TOKEN when the secret is not set, so this + # workflow works either way. But a PR created with GITHUB_TOKEN does + # not trigger workflows, so its required checks never report and it + # shows as blocked by branch protection -- mergeable only by an admin. + # Setting RELEASE_PLEASE_TOKEN (see CONTRIBUTING.md) makes release PRs + # run CI like any other and removes the need for that bypass. + token: ${{ secrets.RELEASE_PLEASE_TOKEN || github.token }} config-file: release-please-config.json manifest-file: .release-please-manifest.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e140069..70dbdf6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,18 +93,49 @@ runs, its required checks never report, and it shows as blocked. Admin enforcement is deliberately off so a repo admin can merge it anyway. That is a workaround, not a fix. -The proper fix is to give release-please a token that isn't `GITHUB_TOKEN` — a -fine-grained PAT or a GitHub App installation token — stored as a secret and -passed to the action: - -```yaml -- uses: googleapis/release-please-action@v5.0.0 - with: - token: ${{ secrets.RELEASE_PLEASE_TOKEN }} +The workflow already reads `secrets.RELEASE_PLEASE_TOKEN` and falls back to +`GITHUB_TOKEN` when it is unset, so setting the secret is all that is needed. + +**Creating the token** (needs org owner/admin): + +1. — a *fine-grained* + PAT. +2. Resource owner: **osuosl**. Repository access: **only** `osuosl/proc` and + `osuosl/prc`. +3. Repository permissions — exactly two: + - **Contents: Read and write** (branches, commits, tags, releases) + - **Pull requests: Read and write** (open and update the release PR) +4. Expiry: pick a date and put a calendar reminder on it. An expired token + fails silently — release PRs simply stop appearing. +5. If the org requires approval for fine-grained PATs, approve it at + *Organization settings → Personal access tokens → Pending requests*. + +**Storing it** — set it once at the org so both repos share it: + +```bash +gh secret set RELEASE_PLEASE_TOKEN --org osuosl --repos proc,prc +# paste the token at the prompt; it is read from stdin and never hits your shell history +``` + +Per-repo instead, if you prefer: + +```bash +gh secret set RELEASE_PLEASE_TOKEN --repo osuosl/proc +gh secret set RELEASE_PLEASE_TOKEN --repo osuosl/prc +``` + +**After it works** — confirm a release PR shows CI runs, then close the loop by +turning admin enforcement back on, which is the whole point of the exercise: + +```bash +gh api -X PATCH repos/osuosl/proc/branches/main/protection/enforce_admins +gh api -X PATCH repos/osuosl/prc/branches/main/protection/enforce_admins ``` -Then its PRs trigger CI like any other and admin bypass is no longer needed. -Worth doing once someone with org permissions can mint the token. +**A note on authorship.** A user PAT makes release PRs appear authored by that +user, and they will not be able to approve their own PR if approvals are ever +required. A GitHub App installation token or a dedicated machine account avoids +both, at the cost of more setup. For two low-traffic repos a PAT is proportionate. ## Merging