Our Firmware · Volume 4
Running the Fork — A Private Copy, Its Branches, the Pinned Toolchain, Upstream Merges and Releases
Why the fork cannot be a GitHub fork, how its branches mirror TeamOpenFIRE's single-trunk releases, what to do with the board-header submodule, the build pinned down to the command line, how an upstream release is merged in, how fork versions are numbered without breaking the App, and what a release contains
4.1 Where the fork lives
The obvious move, pressing GitHub’s Fork button on TeamOpenFIRE/OpenFIRE-Firmware, does not give a private fork. GitHub’s documentation is explicit: “Public repository forks are public, and private repository forks are private”, and “You cannot change the visibility of a fork by itself.” A fork of a public repository is public, and decision D12 keeps the firmware private until there is a working model.
The method GitHub documents for this case is a duplicate: “To maintain a mirror of a repository without forking it, you can run a special clone command, then mirror-push to the new repository” (a bare clone of the original, then a mirror push into a new, empty private repository). The result is an ordinary private repository in the project’s GitHub organisation with the whole upstream history, all tags and all branches. Upstream is then added to a working clone as a second remote, read-only by habit, and fetched whenever a release appears.
Two consequences. The duplicate carries no visible link to upstream on GitHub, so the README of the private repository says in its first line what it is, which tag it came from, and that the OpenFIRE name and credits stand (the authors’ request, Volume 1 §1.3). And GitHub’s pull-request machinery between the two does not exist; when a fix goes upstream (4.9) it goes from a separate, public fork made for that purpose, carrying only that fix.
Access is the owner and Michael. The private repository holds firmware only; CAD and STEP stay in their own private repositories (D12).
4.2 How upstream works
Read through the GitHub API on 19 October 2026, TeamOpenFIRE’s firmware repository has a simple shape:
Table 1 — Read through the GitHub API on 19 October 2026, TeamOpenFIRE's firmware repository has a simple shape
| Item | State |
|---|---|
| Default branch | OpenFIRE-dev, at 8b651a25, identical to the v6.2 tag |
| Releases | v6.0 (24 May 2025), v6.1 (2 January 2026), v6.2 (19 April 2026), each tagged on OpenFIRE-dev |
| Other branches | PerspectiveImport (last commit June 2024), test (November 2024), g4a-orig (April 2024), Prow7-orig (November 2021): history, not active work |
| Last push | 19 April 2026, the v6.2 release; none in the six months since |
| Submodules | the OpenFIRE-Boards header, and four Adafruit libraries (NeoPixel, SSD1306, GFX, BusIO) |
| Activity | 167 stars, 35 forks, 11 open issues; Volume 1 of Open-Source Firmware counts 721 of the commits to one maintainer |
One trunk, tagged releases a few months apart, one maintainer. That makes tracking easy (one branch to follow, a tag to merge) and sets the main risk: upstream can go quiet for long periods, and a fork should never depend on a fix arriving.
The ESP32-S3 port is the opposite: one main branch with commits most days, a v6.2.1 release on 30 June 2026 and five v7.0.0 release candidates between 24 September and 16 October 2026. Decision D30 keeps it on its own track: the wireless trial runs a pinned release of the port as published, recorded by tag and commit, and nothing from this fork is ported to it. The fork borrows from the port (Volume 2), crediting each function taken.
4.3 The branches

Table 2 — 4.3 The branches
| Branch | What it holds | Who moves it |
|---|---|---|
upstream/OpenFIRE-dev (remote) | TeamOpenFIRE’s trunk | fetched only |
vendor | Exactly an upstream release tag: v6.2 now | fast-forwarded to the next tag, never committed to |
main | v6.2 plus every merged change; always builds; always passes the regression tests (Volume 5) | merges from feature branches and from vendor |
change/<row>-<slug> (for example change/am-1-offset-map) | One change from Volumes 2 and 3 | the developer; deleted after merge |
research/<row>-<slug> (for example research/es-2-bar-lock) | Experiments that may never ship | never merged until the experiment’s test passes |
Rules that keep the merges cheap:
- One row, one branch, one merge. Each change’s commits carry its row ID (AM-1, CI-5, OSF-3) in the message. When upstream fixes the same thing, the fork’s version can be found and dropped.
- Fixes before features, so the pure bug fixes sit at the bottom of the history and are the easiest to report upstream later (4.9).
- No reformatting of upstream files. Every changed line in a hot file is a potential conflict at the next upstream release; whitespace and style changes are not worth it.
- New code in new files where possible. The PixArt driver, the IMU bridge and the hold-open state machine each live in their own file and are called from upstream’s code at one or two points. A conflict then touches the call site, not the feature.
4.4 The board-header submodule
The pin functions and setting names the fork adds (Volume 1 §1.5) live in OpenFIREshared.h, which is not in the firmware repository: it is the OpenFIRE-Boards repository, pulled in as the OpenFIREmain/boards submodule and compiled into the App as well. A fork that changes it needs its own copy.
- Duplicate OpenFIRE-Boards privately in the same way as the firmware (4.1), and repoint the fork’s
.gitmodulesentry at it. Without this the fork’s header changes would live in a detached submodule commit that exists nowhere else. - Append, never insert. The header’s own instruction is that new non-button slots go “at the bottom, above the ‘count’ line”. The fork’s entries go after upstream’s last entry, and when upstream adds one in the same place, the merge keeps upstream’s first and the fork’s after it.
- Keep the board identity. The App recognises boards by the identifier the firmware reports and offers presets for known ones. The fork reports the stock
rpipico(orrpipico2) identity, so the App’s board view, pin previewer and tests keep working, and applies the carrier’s extra pins from the fork’s compile-time configuration (Volume 1 §1.6). Inventing a new board identifier would need an App change. - The Adafruit submodules stay pointed at Adafruit, pinned by commit as upstream pins them. They are not changed, so there is nothing to keep private.
4.5 The toolchain, pinned to the command line
Open-Source Firmware chose the release toolchain (row OF-2, Volume 7 §7.11): arduino-cli on TeamOpenFIRE’s patched arduino-pico core, version 4.5.4-tusb, with the continuous-integration workflow’s compile line. The fork adopts it and pins the remaining loose ends:
Table 3 — Open-Source Firmware chose the release toolchain (row OF-2, [Volume 7 §7.11](/light-guns/building-the-electronics/open-source-firmware/vol-7/)): arduino-cli on TeamOpenFIRE's patched arduino-pico core, version 4.5.4-tusb, with the continuous-integration workflow's compile line. The fork adopts it and pins the remaining loose ends
| Item | Upstream’s workflow | The fork |
|---|---|---|
| arduino-cli | arduino/setup-arduino-cli@v2, which installs the current release: not pinned | One named arduino-cli version, recorded in the build script |
| Core | rp2040:[email protected] from TeamOpenFIRE’s package index | The same, and a copy of the core’s release archive kept with the fork’s releases, in case the index or release disappears |
| Libraries | --libraries libraries (in-tree plus submodules) | The same; submodule commits recorded per release |
| Build flags | -DGIT_HASH=<short hash> plus per-board extras | The same plus the fork’s configuration define (4.7) |
| Boards | one matrix entry per board, eleven UF2s in the v6.2 release | two: rpipico and rpipico2 |
| Host | GitHub’s ubuntu-latest | the lab’s Linux workstation, by a script; Actions on the private repository optional |
Pinning arduino-cli matters for the same reason the core is pinned: a release has to be rebuildable later, byte for byte if possible, and the GPL source offer (Volume 6) promises the “scripts to control” the build.
Moving to the current core. Upstream arduino-pico was at 6.3.0 (8 October 2026) while the fork’s core dates from May 2025. The reason for the pin, Adafruit TinyUSB issue #293 (“Serial crash when receiving incoming data faster than handled”), was still open on 19 October 2026. The fork stays pinned. The test that would release it is the serial flood of Open-Source Firmware Vol 7 §7.8, run on a research/core-6.3 branch, now with an extra reason: the PixArt driver’s only published Pico builds use the newer core (Volume 2 §2.6).
4.6 Merging an upstream release
When TeamOpenFIRE tags a release, the sequence is:
- Read before merging. The release notes and the diff from the previous tag. v6.0’s notes warned “Expect to lose ALL PREVIOUSLY SAVED DATA on upgrade”; a note like that changes the fork’s upgrade instructions.
- Build upstream as it is. Fast-forward
vendorto the new tag and build it with the pinned toolchain. Run the regression tests (Volume 5) on the unmodified build to record upstream’s own behaviour. - Merge
vendorintomain. A merge, not a rebase (4.10). Conflicts concentrate in the four hot files of Volume 1 §1.4 and the board header. - Look for overlap. For each row in the fork, check whether upstream fixed the same thing. If it did, the fork’s version is reverted in the merge and the row is marked “fixed upstream” in the fork’s change log.
- Check the saved-data path. Flash the old fork release, set every fork setting, flash the new build, and confirm the settings survive (or that the upgrade note says they will not).
- Run the full release tests (Volume 5 §5.6) and tag.
If upstream changes the toolchain pin (a move off 4.5.4-tusb, say), the fork follows it only after its own serial flood test passes.
4.7 Version numbers the App can live with
The firmware reports its version to the App when it docks, built from OPENFIRE_VERSION (6.2) and, in release builds, - plus the git hash. App v3.0.3 treats everything after the dash as a commit hash and turns it into a link to github.com/TeamOpenFIRE/OpenFIRE-Firmware/commit/<hash> for an RP2040 board. A fork build’s hash exists only in the private repository, so that link is dead. It is cosmetic, but it also suggests the right scheme:
OPENFIRE_VERSIONstays upstream’s number, so the App and anyone reading a bug report see the true base.- The fork’s own release number goes after the dash with the hash:
6.2-lg3-1a2b3c4style, the dead link accepted while the firmware is private. - The full identification (base, fork release, hash, build date, toolchain) is printed on the serial debug output and recorded in the release’s manifest (4.8).
- The product name the gun reports stays the App-settable “FIRECon” default, or a per-gun name set in the App (15 characters); the manufacturer string stays “OpenFIRE” (Volume 3 §3.7).
Upgrade notes travel with each release. The first one has to say that AM-2 changes what calibration stores, so every profile must be recalibrated after moving from stock v6.2 to the fork.
4.8 What a release is

Table 4 — 4.8 What a release is
| Artefact | Contents | Why |
|---|---|---|
| UF2, per board | rpipico, rpipico2 | What gets flashed |
| Manifest | base tag, fork tag, commit, submodule commits, arduino-cli and core versions, compile line, build date | Reproducing the build; the record a source offer needs |
| Source archive | the tagged tree with submodules checked out, the build script, the licence files | The Corresponding Source, ready the day a kit ships (Volume 6) |
| Change log | rows merged since the last release, rows fixed upstream, upgrade notes | Reading what changed without the diff |
| Test record | the Volume 5 checklist with results | No release without it |
Releases live as releases of the private repository. They go to the shared Downloads/ store only after the owner’s publish decision, which the category plan ties to a working model (D12).
4.9 Reporting upstream, later
Decision D10 defers it; it does not forbid it. When the time comes, the order is the pure bug fixes first (OSF-1, OSF-2, OSF-3, AM-6), because they are small, testable and uncontroversial, then the maths (AM-1, AM-2, AM-7), then anything else. The repository’s contribution rules, as Open-Source Firmware read them, ask that a pull request explain its reasoning and that larger changes come on a separate branch; the change cards of Volumes 2 and 3 are already written that way. Each fix that upstream takes is one fewer change to carry through every future merge, which is the economic case for reporting.
4.10 Why this, and why not the others
FW-1, hosting (proposed here):
Table 5 — FW-1, hosting (proposed here)
| Option | Verdict | Reason |
|---|---|---|
| Private duplicate in the project’s GitHub organisation, upstream as a second remote | Pick | Private (D12); full history; GitHub’s documented method |
| GitHub fork | Rejected | Cannot be private: “Public repository forks are public” |
| Local repository only | Rejected | One disk; Michael cannot pull; no off-site copy |
| Another host (self-hosted Git, another service) | Not needed | Adds a system to run; the organisation is already private and backed up |
FW-2, branching and merging:
Table 6 — FW-2, branching and merging:
| Option | Verdict | Reason |
|---|---|---|
| Vendor branch + merge each upstream tag into main | Pick | Released history never rewritten, so any old release can be rebuilt and its source handed over exactly; merges happen a few times a year |
| Rebase the fork onto each upstream tag | Rejected | Rewrites the history that released UF2s were built from, breaking the link between a shipped binary and its source |
| Patch queue (a series of patch files applied to clean upstream) | Rejected for now | Clean to report upstream, but every conflict becomes a hand-edited patch, and two people working on it need a tool for it |
| Track upstream’s trunk between releases | Rejected | Upstream releases are the tested points; the trunk has been idle for six months anyway |
FW-3, toolchain: OF-2 as published, plus a pinned arduino-cli version (4.5).
FW-4, versioning: upstream base kept in OPENFIRE_VERSION, fork release number in the suffix (4.7). Rejected: bumping OPENFIRE_VERSION itself (a fork “6.3” would collide with upstream’s next release and confuse anyone comparing bug reports).
FW-5, board identity: report the stock board identifier and carry the carrier’s pins in fork configuration (4.4). Rejected: a new board identifier (the stock App would not know it).
4.11 What could not be verified
- That the stock App’s board view behaves with a gun reporting
rpipicowhile using fork pins it does not list; read from the code, not run. - GitHub Actions pricing and minutes for a private repository were not checked; the plan does not depend on Actions.
- Upstream’s future cadence: three releases in the last 17 months is the only evidence.
Sources
- GitHub Docs, “About forks”, https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks — read 2026-10-19.
- GitHub Docs, “Duplicating a repository”, https://docs.github.com/en/repositories/creating-and-managing-repositories/duplicating-a-repository — read 2026-10-19.
- TeamOpenFIRE,
OpenFIRE-Firmware: branches, releases and repository metadata through the GitHub API;.gitmodules;.github/workflows/arduino-cli-builds.yml;OpenFIREmain/OpenFIREDefines.h,OpenFIREmain.ino(version string) — read 2026-10-19. - TeamOpenFIRE,
OpenFIRE-Appv3.0.3,src/appmainwindow.cpp(version label, lines ~855–865) — read 2026-10-19. - TeamOpenFIRE,
OpenFIRE-Boards,OpenFIREshared.h— read 2026-10-19. - Adafruit,
Adafruit_TinyUSB_Arduinoissue #293 (open, updated 2026-08-29); Earle F. Philhower III,arduino-picorelease 6.3.0 (2026-10-08); TeamOpenFIREarduino-picorelease 4.5.4-tusb (2025-05-23) — GitHub API, 2026-10-19. - Alessandro Satanassi,
OpenFIRE-Firmware-ESP32, releases v6.2.1 and v7.0.0-rc1 to rc5 — GitHub API, 2026-10-19. - Open-Source Firmware dive, Volume 7 §7.8–7.11, and Volume 1 §1.6 — read 2026-10-19.
Comments (0)