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

ItemState
Default branchOpenFIRE-dev, at 8b651a25, identical to the v6.2 tag
Releasesv6.0 (24 May 2025), v6.1 (2 January 2026), v6.2 (19 April 2026), each tagged on OpenFIRE-dev
Other branchesPerspectiveImport (last commit June 2024), test (November 2024), g4a-orig (April 2024), Prow7-orig (November 2021): history, not active work
Last push19 April 2026, the v6.2 release; none in the six months since
Submodulesthe OpenFIRE-Boards header, and four Adafruit libraries (NeoPixel, SSD1306, GFX, BusIO)
Activity167 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

Figure 1 — The fork's branches against upstream. Upstream's OpenFIRE-dev and its release tags are fetched read-only. A vendor branch moves only from one upstream tag to the next. The fork's main branch starts…
Figure 1 — The fork's branches against upstream. Upstream's OpenFIRE-dev and its release tags are fetched read-only. A vendor branch moves only from one upstream tag to the next. The fork's main branch starts at v6.2 and takes each upstream release by a merge from vendor. Each change is developed on its own branch named for its decision-log row and merged into main after its bench test; research branches (ES-1, ES-2) never merge until they prove a gain. Release tags on main carry the upstream base and the fork's own number. The ESP32 port is outside this repository, on its own track. Own-work diagram.

Table 2 — 4.3 The branches

BranchWhat it holdsWho moves it
upstream/OpenFIRE-dev (remote)TeamOpenFIRE’s trunkfetched only
vendorExactly an upstream release tag: v6.2 nowfast-forwarded to the next tag, never committed to
mainv6.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 3the developer; deleted after merge
research/<row>-<slug> (for example research/es-2-bar-lock)Experiments that may never shipnever merged until the experiment’s test passes

Rules that keep the merges cheap:

  1. 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.
  2. 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).
  3. 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.
  4. 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 .gitmodules entry 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 (or rpipico2) 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

ItemUpstream’s workflowThe fork
arduino-cliarduino/setup-arduino-cli@v2, which installs the current release: not pinnedOne named arduino-cli version, recorded in the build script
Corerp2040:[email protected] from TeamOpenFIRE’s package indexThe 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 extrasThe same plus the fork’s configuration define (4.7)
Boardsone matrix entry per board, eleven UF2s in the v6.2 releasetwo: rpipico and rpipico2
HostGitHub’s ubuntu-latestthe 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:

  1. 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.
  2. Build upstream as it is. Fast-forward vendor to 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.
  3. Merge vendor into main. A merge, not a rebase (4.10). Conflicts concentrate in the four hot files of Volume 1 §1.4 and the board header.
  4. 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.
  5. 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).
  6. 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_VERSION stays 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-1a2b3c4 style, 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

Figure 2 — What one fork release produces. The tagged source with its submodule commits, the pinned arduino-cli and core, and the build script go in; out come a UF2 for each board, a manifest recording every …
Figure 2 — What one fork release produces. The tagged source with its submodule commits, the pinned arduino-cli and core, and the build script go in; out come a UF2 for each board, a manifest recording every version and hash and the exact compile line, a source archive including the submodules, and the change log and test record for the release. The source archive and the manifest are what a GPL source offer would hand over. Own-work diagram.

Table 4 — 4.8 What a release is

ArtefactContentsWhy
UF2, per boardrpipico, rpipico2What gets flashed
Manifestbase tag, fork tag, commit, submodule commits, arduino-cli and core versions, compile line, build dateReproducing the build; the record a source offer needs
Source archivethe tagged tree with submodules checked out, the build script, the licence filesThe Corresponding Source, ready the day a kit ships (Volume 6)
Change logrows merged since the last release, rows fixed upstream, upgrade notesReading what changed without the diff
Test recordthe Volume 5 checklist with resultsNo 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)

OptionVerdictReason
Private duplicate in the project’s GitHub organisation, upstream as a second remotePickPrivate (D12); full history; GitHub’s documented method
GitHub forkRejectedCannot be private: “Public repository forks are public”
Local repository onlyRejectedOne disk; Michael cannot pull; no off-site copy
Another host (self-hosted Git, another service)Not neededAdds a system to run; the organisation is already private and backed up

FW-2, branching and merging:

Table 6 — FW-2, branching and merging:

OptionVerdictReason
Vendor branch + merge each upstream tag into mainPickReleased 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 tagRejectedRewrites 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 nowClean 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 releasesRejectedUpstream 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 rpipico while 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

  1. GitHub Docs, “About forks”, https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks — read 2026-10-19.
  2. GitHub Docs, “Duplicating a repository”, https://docs.github.com/en/repositories/creating-and-managing-repositories/duplicating-a-repository — read 2026-10-19.
  3. 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.
  4. TeamOpenFIRE, OpenFIRE-App v3.0.3, src/appmainwindow.cpp (version label, lines ~855–865) — read 2026-10-19.
  5. TeamOpenFIRE, OpenFIRE-Boards, OpenFIREshared.h — read 2026-10-19.
  6. Adafruit, Adafruit_TinyUSB_Arduino issue #293 (open, updated 2026-08-29); Earle F. Philhower III, arduino-pico release 6.3.0 (2026-10-08); TeamOpenFIRE arduino-pico release 4.5.4-tusb (2025-05-23) — GitHub API, 2026-10-19.
  7. Alessandro Satanassi, OpenFIRE-Firmware-ESP32, releases v6.2.1 and v7.0.0-rc1 to rc5 — GitHub API, 2026-10-19.
  8. Open-Source Firmware dive, Volume 7 §7.8–7.11, and Volume 1 §1.6 — read 2026-10-19.

Comments (0)

  1. Loading…

Comments are held for moderation — nothing appears until approved.