Conversation
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) <noreply@anthropic.com>
Signed-off-by: Lance Albertson <lance@osuosl.org>
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) <noreply@anthropic.com>
Signed-off-by: Lance Albertson <lance@osuosl.org>
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) <noreply@anthropic.com>
Signed-off-by: Lance Albertson <lance@osuosl.org>
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=<pin> 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) <noreply@anthropic.com>
Signed-off-by: Lance Albertson <lance@osuosl.org>
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<sha>. 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) <noreply@anthropic.com>
Signed-off-by: Lance Albertson <lance@osuosl.org>
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) <noreply@anthropic.com>
Signed-off-by: Lance Albertson <lance@osuosl.org>
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) <noreply@anthropic.com> Signed-off-by: Lance Albertson <lance@osuosl.org>
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) <noreply@anthropic.com> Signed-off-by: Lance Albertson <lance@osuosl.org>
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) <noreply@anthropic.com>
Signed-off-by: Lance Albertson <lance@osuosl.org>
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) <noreply@anthropic.com> Signed-off-by: Lance Albertson <lance@osuosl.org>
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) <noreply@anthropic.com> Signed-off-by: Lance Albertson <lance@osuosl.org>
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) <noreply@anthropic.com>
Signed-off-by: Lance Albertson <lance@osuosl.org>
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) <noreply@anthropic.com>
Signed-off-by: Lance Albertson <lance@osuosl.org>
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) <noreply@anthropic.com>
Signed-off-by: Lance Albertson <lance@osuosl.org>
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 <lance@osuosl.org>
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) <noreply@anthropic.com> Signed-off-by: Lance Albertson <lance@osuosl.org>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Turns this from a clone of upstream into something OSL can maintain.
Production bug fix
The telnet server went permanently deaf after abandoned sessions. This was
hitting a live node: TCP connected, no
passwd:prompt, silence until timeout,and only a reboot cleared it.
new_link()returned early wheneverserver.available()yielded no client — butEthernet3 only returns a socket that has unread bytes waiting, and the
pending-slot timeout handling lived after that return. Three abandoned sessions
pinned all three authentication slots forever. Also fixed: slots are reclaimed as
soon as a peer disconnects, deadlines survive the ~49-day
millis()rollover, anda connection arriving with every slot busy is refused rather than left unread
where it would mask every later connection.
-DPWM=5builds again —pwmwas defined twice at namespace scope, so thatvariant had been unbuildable since 2024.
-DPWM=5CI and releases
Built around the one constraint that matters — ~1.6 KB of headroom. Both
variants compile with pinned toolchain versions, flash/RAM deltas are posted on
PRs, the build fails below 512 bytes free, and every commit produces a flashable
.hexartifact so testing a branch needs no local toolchain.Tagging
vX.Y.Zbuilds both variants and attaches.hex,.elf,SHA256SUMSand
toolchain.txt. The banner becomesPROC-V1-X.Y.Z-g<sha>, so a banner readoff an otherwise-unreachable machine maps to an exact commit.
English documentation
All Chinese translated in place — comments,
build.sh, README. Comments only:the compiled image is byte-identical apart from the embedded
__TIME__. Plus areal README, CONTRIBUTING, CHANGELOG,
AGENTS.md, and the vendor software manualtranslated and annotated wherever it no longer matches the code.
Review notes
astylejob is advisory — the tree isclean under 3.1, but the runner ships a newer one. If it passes, flip it to
blocking.
new_link()fix is the one that wants a real node.