Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .clang-format
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
# sources: Google, wrapped at 100 columns.
#
# It applies to src/pinside/firmware/templates/, which is where the C actually
# lives a generated project is a copy of those files plus one table, so
# lives: a generated project is a copy of those files plus one table, so
# formatting the templates is what keeps every generated fixture consistent.
# There is nothing to format in a generated directory; regenerate instead.
BasedOnStyle: Google
ColumnLimit: 100

# The compact lookup tables this code base uses throughout one case label,
# one assignment, one return read better than the exploded form Google
# The compact lookup tables this code base uses throughout (one case label,
# one assignment, one return) read better than the exploded form Google
# defaults to.
AllowShortCaseLabelsOnASingleLine: true
29 changes: 29 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
name: Bug report
about: Something pinside got wrong
labels: bug
---

## What happened

<!-- The command you ran and what it printed. Findings are quotable verbatim;
their codes are stable. -->

```
$ pinside ...
```

## What you expected

## The board or config

pinside reads local files, so the fastest fix usually starts from the input.
If the `.kicad_pcb` or the fixture config cannot be shared, the relevant part
usually can: a footprint, an `Edge.Cuts` shape, one channel out of the config.

## Versions

- pinside: <!-- pinside --version -->
- Python: <!-- python3 --version -->
- KiCad, if `project` is involved:
- OS:
29 changes: 29 additions & 0 deletions .github/ISSUE_TEMPLATE/check_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
name: New check
about: A way a fixture goes wrong that pinside does not catch yet
labels: check
---

## The mistake

<!-- What goes wrong on a real fixture, and at what point somebody finds out.
Checks are ordered by how expensive the mistake is, so this is the part
that decides where it lands. -->

## What the board file shows

<!-- Which primitive carries the evidence: a pad, an Edge.Cuts shape, a net
name, a footprint's courtyard. A check needs something in the .kicad_pcb
to key off. -->

## The finding it should produce

A finding says what is wrong, which references it applies to, and what to do.

- Summary:
- References:
- What to do:
- Severity: error / warning / info

<!-- error means the fixture would be scrap; warning means it would be
unreliable; info means it could be better. -->
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
blank_issues_enabled: true
25 changes: 25 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## What this changes

<!-- One or two sentences. If it adds a finding code, say which and why it is
actionable: what is wrong, which references, and what to do about it. -->

## Checks

- [ ] `scripts/lint.sh && scripts/test.sh` pass.
- [ ] New or changed behaviour has a test.
- [ ] `CHANGELOG.md` has an entry under `[Unreleased]`, if this is user-visible.

CONTRIBUTING.md asks two questions that CI cannot answer for you. Answer the
ones that apply, and delete the rest.

- [ ] **Touched anything under `src/pinside/kicad/`?** The KiCad tests run in
CI in a container, but a real KiCad install is the only place `project`
output gets opened. Say which KiCad version you ran against.
- [ ] **Touched anything under `src/pinside/firmware/templates/`?** CI compiles
the host tests with the mock HAL and cross-compiles against the Pico SDK.
Say whether you flashed it and what the fixture did.
- [ ] **Added or changed a target in `targets.py`?** Name the datasheet and the
table you checked the function map against. A wrong map produces a config
that validates and does not work.
- [ ] **Changed a probe in `pogo.py`?** Cite the supplier drawing. These numbers
end up as drill sizes on a board somebody orders.
187 changes: 186 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ jobs:
- name: Run tests
run: scripts/test.sh --verbose

# Once, not on every interpreter: the number is about which code paths
# the suite reaches, and that does not vary by Python version.
- name: Coverage
if: matrix.python == '3.13'
run: scripts/test.sh --coverage

# The client tests run against a fake port either way. With pyserial here
# two more run against pyserial's own loop:// port, which is what caught
# the client accepting an echoed request as a response.
- name: The client, against real pyserial
if: matrix.python == '3.13'
run: |
python -m pip install 'pyserial>=3.5'
scripts/test.sh --verbose

example:
name: End to end
runs-on: ubuntu-latest
Expand All @@ -65,6 +80,20 @@ jobs:
- name: Install
run: python -m pip install .

# The core must work with nothing else installed. If this ever needs a
# dependency to run, the claim on the front of the README is false.
- name: Nothing but the standard library is installed
run: |
python - <<'PY'
import importlib.metadata as md
installed = {d.metadata["Name"].lower() for d in md.distributions()}
# pip and its own bundled bits are the venv, not pinside's dependencies.
allowed = {"pinside", "pip", "setuptools", "wheel", "pkg-resources"}
extra = sorted(installed - allowed)
assert not extra, f"pinside pulled in {extra}"
print("clean:", sorted(installed))
PY

- name: Check the example board
run: pinside check examples/demo-board.kicad_pcb --strict

Expand Down Expand Up @@ -99,7 +128,27 @@ jobs:
pinside generate examples/demo-fixture.json --out /tmp/demo-firmware
/tmp/demo-firmware/test/run.sh

- name: The generated contract is valid JSON and names the config hash
# `probe` is the one command that needs an optional dependency. Both
# halves are worth asserting: that it says how to install it, and that it
# works once installed.
- name: probe explains its missing dependency, then works with it
run: |
set +e
pinside probe examples/demo-fixture.json 2>/tmp/probe.err
status=$?
set -e
cat /tmp/probe.err
test "$status" -ne 0
grep -q "pinside\[client\]" /tmp/probe.err
python -m pip install '.[client]'
pinside probe examples/demo-fixture.json --port loop:// --timeout 1 2>/tmp/probe2.err \
&& { echo "a loopback should not pass as a fixture" >&2; exit 1; }
cat /tmp/probe2.err

# The contract's real validation lives in tests/test_contract.py, which checks it against
# the firmware's own dispatch table. What is left for this job is the thing only an
# installed wheel can show: that openrpc.json is shipped and generated at all.
- name: The installed package generates a usable contract
run: |
python - <<'PY'
import json, pathlib
Expand All @@ -111,6 +160,142 @@ jobs:
len(contract["x-pinside"]["channels"]), "channels")
PY

kicad:
name: KiCad project generation
runs-on: ubuntu-latest
# tests/test_kicad.py generates a fixture project and runs KiCad's own ERC
# and DRC over it. Without KiCad it skips, which meant the whole emitter
# under src/pinside/kicad/ -- the code that has to produce a file KiCad will
# actually open -- was only ever checked on a developer's machine.
#
# --user root because the image declares USER kicad, and the runner mounts
# its own /__w into the container owned by root. As the kicad user,
# actions/checkout cannot write /__w/_temp/_runner_file_commands and dies
# with EACCES before a single step of ours runs.
container:
image: kicad/kicad:10.0
options: --user root
steps:
- uses: actions/checkout@v7
# Still needed: the checkout is owned by a different uid than git expects,
# and git refuses to look at a tree it considers someone else's.
- name: Trust the checkout
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Versions
run: |
kicad-cli version
python3 --version

# A KiCad install has libraries on disk and a *profile* that maps library
# nicknames to them. The profile is created on the first GUI launch, from
# KiCad's own templates; kicad-cli never creates one. So in a fresh
# container every `MCU_Module:` and `power:` reference is unresolvable and
# ERC reports 44 warnings about configuration rather than about the
# schematic.
#
# Seeding the tables is what a first launch does. Verified by reproducing
# both halves on a workstation with KICAD_CONFIG_HOME pointed at an empty
# directory: 44 violations without them, 0 with.
- name: Seed the library tables a first launch would create
run: |
# The default tables sit directly in the template directory. KiCad
# also ships project templates one level below it, each with its own
# pair, and seeding from one of those configures nothing: an earlier
# `find | head -1` picked template/Edgeberry_Cartridge and died on its
# missing fp-lib-table. Requiring both files in one directory, from a
# known location, is what makes that unambiguous.
for dir in /usr/share/kicad/template /usr/local/share/kicad/template; do
if [ -f "$dir/sym-lib-table" ] && [ -f "$dir/fp-lib-table" ]; then
template="$dir"
break
fi
done
test -n "${template:-}" || { echo "no default library tables" >&2; exit 1; }

version=$(kicad-cli version | grep -oE '[0-9]+\.[0-9]+' | head -1)
config="$HOME/.config/kicad/${version:-10.0}"
mkdir -p "$config"
cp "$template/sym-lib-table" "$template/fp-lib-table" "$config/"

# A wrong table copies exactly as successfully as a right one, so
# check the result names the libraries the generated schematic uses.
for lib in MCU_Module power Device; do
grep -q "\"$lib\"" "$config/sym-lib-table" ||
{ echo "the seeded table does not name $lib" >&2; exit 1; }
done
echo "seeded $config from $template" \
"($(grep -c '(lib ' "$config/sym-lib-table") libraries)"
# Not scripts/test.sh: the image's Python is the one KiCad ships with, and
# the script's venv handling is for the tooling, not the interpreter.
#
# PINSIDE_REQUIRE_KICAD turns this job's reason for existing into an
# assertion. Without it, a container that stopped shipping kicad-cli, or
# a symbol path that moved, would make the tests skip and the job pass:
# green, and covering nothing.
- name: Run the suite, KiCad tests included
env:
PINSIDE_REQUIRE_KICAD: "1"
run: PYTHONPATH=src python3 -m unittest discover -s tests --verbose

firmware:
name: Firmware against the Pico SDK
runs-on: ubuntu-latest
# The host tests in the `test` job compile fixture_core.c against the mock
# HAL, which never touches fixture_hal_rp2350.c. That file is the half of
# the firmware that talks to real hardware, and until this job existed the
# only thing checking it was clang-format.
env:
PICO_SDK_VERSION: "2.1.1"
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.13"

- name: Install the ARM toolchain
run: |
sudo apt-get update
sudo apt-get install -y gcc-arm-none-eabi cmake ninja-build

- name: Cache the SDK
id: sdk
uses: actions/cache@v4
with:
path: pico-sdk
key: pico-sdk-${{ env.PICO_SDK_VERSION }}

# Pinned to a tag, not to master: an SDK that moves under the templates
# turns a template bug and an upstream change into the same red job.
- name: Fetch the SDK
if: steps.sdk.outputs.cache-hit != 'true'
run: |
git clone --branch "$PICO_SDK_VERSION" --depth 1 \
https://github.com/raspberrypi/pico-sdk.git pico-sdk
git -C pico-sdk submodule update --init --depth 1

- name: Install
run: python -m pip install .

- name: Generate the demo firmware
run: pinside generate examples/demo-fixture.json --out /tmp/fw

- name: Cross-compile it
env:
PICO_SDK_PATH: ${{ github.workspace }}/pico-sdk
run: |
cmake -S /tmp/fw -B /tmp/fw/build -G Ninja
cmake --build /tmp/fw/build
# A UF2 is the artefact you actually flash. CMake will happily produce
# an ELF and no UF2 if the SDK's post-build step was never wired up.
find /tmp/fw/build -name '*.uf2' -print | grep -q . \
|| { echo "no .uf2 was produced" >&2; exit 1; }

- uses: actions/upload-artifact@v7
with:
name: demo-firmware-uf2
path: /tmp/fw/build/**/*.uf2
if-no-files-found: error

package:
name: Package
runs-on: ubuntu-latest
Expand Down
61 changes: 57 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ on:
workflow_dispatch:

permissions:
contents: write
contents: read

jobs:
release:
name: Build and publish
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand All @@ -37,7 +37,60 @@ jobs:
python -m pip install --upgrade build
python -m build

- name: Publish the release
# The same assertion CI makes on every push. Repeated here because this is
# the artefact people install, and a wheel that lost the templates still
# imports and fails at the moment somebody runs `generate`.
- name: The wheel carries the firmware templates
run: |
python - <<'PY'
import pathlib, zipfile
wheel = next(pathlib.Path("dist").glob("*.whl"))
names = zipfile.ZipFile(wheel).namelist()
templates = [n for n in names if "firmware/templates/" in n]
assert len(templates) >= 9, f"only {len(templates)} templates in {wheel.name}"
assert any(n.endswith("py.typed") for n in names), "py.typed missing"
PY

- uses: actions/upload-artifact@v7
with:
name: dist
path: dist/
if-no-files-found: error

pypi:
name: Publish to PyPI
needs: build
runs-on: ubuntu-latest
# A trusted publisher: PyPI verifies this workflow's OIDC identity, so there
# is no long-lived token in the repository to leak or rotate. Configure it at
# pypi.org/manage/project/pinside/settings/publishing with
# owner=bitcrushtesting, repository=pinside, workflow=release.yml,
# environment=pypi.
environment:
name: pypi
url: https://pypi.org/p/pinside
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v7
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1

github:
name: Publish the GitHub release
needs: pypi
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v7
with:
name: dist
path: dist/
- name: Create the release
env:
GH_TOKEN: ${{ github.token }}
run: |
Expand Down
Loading
Loading