diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..20913a5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,45 @@ +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 "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: chore + include: scope + + labels: + - dependencies + open-pull-requests-limit: 3 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..eaf2d1a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,183 @@ +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 + + 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 + # 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 + + - 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/commits.yml b/.github/workflows/commits.yml new file mode 100644 index 0000000..d1d55ec --- /dev/null +++ b/.github/workflows/commits.yml @@ -0,0 +1,72 @@ +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() + + # 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 = [] + 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 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}") + 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/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..539414e --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,31 @@ +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: + # 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 + + 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 new file mode 100644 index 0000000..ce569ed --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,146 @@ +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: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: "Existing tag to rebuild and re-upload" + 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: + 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: ${{ steps.tag.outputs.tag }} + fetch-depth: 0 + + - name: Compose the version string + id: ver + run: | + SHA="$(git rev-parse --short=7 HEAD)" + # 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 + + - 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; 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}" + 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 )) + 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-${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 ./* > SHA256SUMS ) + + - name: Build summary + run: | + { + echo "### ${{ steps.tag.outputs.tag }} — \`PROC-V1-${{ steps.ver.outputs.git_ver }}\`" + echo "" + 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 "| variant | flash used | max | free |" + echo "|---|---:|---:|---:|" + cat sizes.md + echo "" + echo "Flash \`stock\` unless the board has the V1.1 PWM pad populated." + echo "" + echo '```bash' + 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.tag.outputs.version }}-stock.hex:i" + echo '```' + echo "" + 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 + 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/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..88d2eec --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,496 @@ +# AGENTS.md — `proc`: PROC-V1 production firmware + +**This is the repository to change if the task is "improve the PROC software".** + +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. + +--- + +## 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. + +> **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 +> +> 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 [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. +- `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 [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. + +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`. + +> **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 | +| 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 | + +### 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. +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 [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 + necessarily HEAD. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..005d5cd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,42 @@ +# Changelog + +**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. + + + +## Fork baseline + +Context for everything above, written by hand before release automation existed. + +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`. + +The fork exists because OSL runs several of these boards and hit a bug in +production. Two fixes landed before automation: + +- **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 new file mode 100644 index 0000000..70dbdf6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,274 @@ +# 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. + +**⚠ 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 + +```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. + +## 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**, + 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 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 +``` + +**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 + +**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 +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 — 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). + +## 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. + +## 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 **`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 +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 | +|---|---|---| +| 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 | yes | run `tools/format.sh` before pushing | +| 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: + +```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. diff --git a/README.md b/README.md index a118e26..9f1ee97 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,115 @@ -# proc -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) -ide使用arduino, 型号选择 arduino uno -把板子上的串口接到电脑上,就可以编译下载了, +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. -用命令行进行编译: -执行build.sh, 会使用arduino-cli进行编译下载。如果没有arduino-cli,会自动下载arduino-cli, 如果缺库, 脚本会自动下载库, 完成编译后, rom会在build.sh所在的目录prc.hex +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. -用命令行进行升级: -把设备接到usb串口上,如果使用被控机进行升级, 根据串口设备编号,修改脚本,然后,linux下要退出串口login程序对串口的占用。 -执行update.sh, 然后给在倒计时到1时, 给设备上电, 升级过程就会开始。 -如果升级失败,调整一下给设备上电的时机,再试一下。 +``` +#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). 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/doc/node-926-software-manual.md b/doc/node-926-software-manual.md new file mode 100644 index 0000000..7a8c2f4 --- /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](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 +> 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`](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 +[../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) diff --git a/prc/prc.ino b/prc/prc.ino index 600e1d2..d825568 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" @@ -25,11 +29,14 @@ 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 -//定时器最长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 +133,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 +174,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 +199,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 +221,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,36 +254,54 @@ 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(); } } +//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)) ) { 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 + //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秒延迟 + 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,27 +311,27 @@ 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; } 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; //密码错误, 5秒惩罚 + clientn[i].ms = millis() + 5000; //wrong password: 5 second penalty break; } break; - case 2: //认证失败等待惩罚时间到期 - if (clientn[i].ms < millis()) { + case 2: //auth failed: wait for the penalty to expire + if (ms_expired(clientn[i].ms)) { clientn[i].host.println(F("auth fail!")); clientn[i].proc = 0; clientn[i].host.stop(); @@ -316,7 +341,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; @@ -330,6 +355,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; } @@ -337,7 +370,7 @@ void loop() { dogcount = 0; new_link(); if (alreadyConnected) { - if (!client.connected()) {//连接断开 + if (!client.connected()) {//connection dropped client.stop(); alreadyConnected = false; } @@ -367,14 +400,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 +431,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; @@ -457,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; @@ -493,7 +524,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 +610,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 +639,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 +661,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 +749,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 +815,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 +852,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 +1223,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 +1270,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 +1283,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 +1347,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); diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..183e027 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,59 @@ +{ + "$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 + } + ], + "bootstrap-sha": "a8f458f3726aa54334e9205a058a9431f81abf98" +} 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 + 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