16 Commits
Author SHA1 Message Date
Oleksandr Shyshatskyi 80a4d64ffd Prepare next firmware release
Add Fluidd and custom boot scripts, migrate DC24V control, update the web UIs and filament macros, improve startup status reporting, and expand upgrade and replica coverage.
2026-09-24 11:37:28 +03:00
MonstrofilandClaude Opus 5 5718603f1c Document how a spool's material reaches OrcaSlicer (#25)
OrcaSlicer 2.4.0's Moonraker agent offers to map a project's filaments onto
the printer's slots, and it learns those slots from the `lane_data` namespace
in Moonraker's database. Nothing in the tree said where that data comes from,
so the question "what does this need to work?" had to be answered by reading
three codebases: ff_toolchange's status surface, HelixScreen's AMS backend
and override store, and MoonrakerPrinterAgent.cpp.

The note is that answer as one diagram plus the reasoning around it. Klipper
supplies the slot count and nothing else -- it cannot sense what filament is
in a tool; the operator supplies the identity through HelixScreen; Moonraker's
database is the only durable record; OrcaSlicer reads it once at connect and
never writes back.

It also records the two things that surprised us. The mapping is not applied
at print time -- start_local_print() ignores ams_mapping, so it selects
presets and temperatures at slice time and does not rewrite tool numbers. And
the reason this does not work on our build yet is not a missing feature but a
missing merge: AmsBackendToolChanger was the last AMS backend without a
FilamentSlotOverrideStore, upstream gave it one on 2026-08-24, and our last
catch-up from upstream is four days older than that.


Claude-Session: https://claude.ai/code/session_01PU5nCo85xuFQuub4kJzWt9

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 13:15:19 +03:00
Oleksandr Shyshatskyi fc137ec59d Prepare beta release documentation 2026-09-02 13:41:27 +03:00
MonstrofilandClaude Sonnet 5 7c74f025ae Document pressure-advance calibration (#15)
* Document automatic pressure-advance calibration

FF_PA_CALIBRATE/FF_PA_PROBE and HelixScreen's calibration-screen
integration shipped without owner/experienced-user documentation --
only the reverse-engineering notes covered it. Adds a table row and
full section to how-calibration-works.md, and links back to it from
the notes page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147AQiPYgtafuy1x5NjqJuy

* Add a simple pressure-advance calibration page

Owner-facing procedure alongside XYZ tool calibration and bed mesh --
FF_PA_CALIBRATE with a temp and tool number, what the output means,
and how to save it. how-calibration-works.md's mechanism section now
links here instead of duplicating the command walkthrough.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147AQiPYgtafuy1x5NjqJuy

* Point pressure-advance results at the slicer, not printer.cfg

PA is a filament property, not a printer one -- a value baked into
printer.cfg is wrong the moment you switch material. Recommend the
slicer's per-filament Pressure Advance field instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147AQiPYgtafuy1x5NjqJuy

* Correct pressure-advance page: there is no factory value

Unlike tool offsets and bed mesh, PA calibration doesn't import a
factory number on first boot -- FF_PA_CALIBRATE only measures and
reports one. Say that instead of implying it's pre-populated.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147AQiPYgtafuy1x5NjqJuy

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 13:03:36 +03:00
Monstrofil e5d6e5025a Merge pull request #12 from Klipper4FlashForge/relax-max-residual
Relax max_residual to 0.5 mm
2026-08-31 23:26:06 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 b55461cd6b Cancelling an object leaves the SD reader with nothing to pace it
EXCLUDE_OBJECT installs the ExcludeObject move transform, and inside a
cancelled object _ignore_move updates Python state without ever calling
next_transform.move(). Nothing reaches the toolhead, so the lookahead
buffer that normally blocks gcode.run_script never fills, and
virtual_sdcard.work_handler runs on its own yields alone -- one
reactor.pause per 8192-byte read, about 330 lines apart. The eboard's
soft-PWM refresh misses its deadline and the MCU shuts down with
"Timer too close".

Measured on a Creator 5 Pro: 37 s from "Excluding object" to the
shutdown, with 1 Hz status queries answered in 1.30-1.78 s throughout
while the main MCU sat at mcu_awake=0.000 and gcodein=0.

EXCLUDE_OBJECT_DEFINE is not the cost. The line costs 115 ms through the
live klippy and nothing thereafter; three prints of the same file
without a cancel complete with bytes_retransmit=0. Removing it removes
the object there is to cancel, not the work.

tools/gcode-bench holds the harnesses, including the shlex fast path
that takes that DEFINE from 91.6 ms to 5.9 ms on the printer -- worth
having for large plates, but not this bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVUMfbNuKUWWV32PBcydwb
2026-08-31 15:25:51 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 f8b925e48f Replace opkg with apk, and sign the feed
docs/notes/85-packaging.md chose opkg in 2026-08 on one argument: every package
is cross-built here anyway, so the winner is whichever format is cheapest to
PRODUCE -- and .ipk had an upstream tool where .apk meant abuild, a keypair and
an Alpine host. Two lines of that comparison were wrong.

apk mkpkg is an applet of apk itself: -I key:value metadata, -F directory, no
keypair, no abuild, no Alpine host. The "making a package" row described
apk-tools 2. And the on-device client was counted a wash when it is better than
one -- openssl and zlib were already cross-built for CPython, and libarchive
existed in this tree only for opkg, so adopting apk REMOVES a cross-build.

What made it possible at all is that apk resolves its database relative to
--root and uses the same root as the file destination, while / here is a
read-only squashfs -- measured: neither /lib/apk nor /etc/apk can be created.
pkgs/3rdparty/apk-tools/prefix.patch splits the two. Upstream already carries
root_fd and dest_fd separately and merely aliases them in context.c, so the
patch defaults the database root to $MODDIR and leaves files extracting at /.
That is opkg's compile-time VARDIR plus `dest root /`, in apk's spelling.

THE FEED IS SIGNED NOW, which it never was: opkg signed the index and
bin/build-packages.sh signed nothing, so nothing was verified on a printer.
apk verifies with raw PEM public keys, not X.509 -- measured on the machine,
where every certificate directory is empty. ./bin/apk-keygen.sh makes an RSA
pair, APK_SIGN_KEY in config.env points at the private half, and the public
half both seeds the USB stick (so the first install can verify) and ships in
anvil-core (so the printer keeps trusting). An empty APK_SIGN_KEY still builds
an unsigned feed, deliberately: `make packages` has to run on a bare checkout
or the packaging lane stops being a CI gate.

WHAT IT COST is a carried patch, against an internal with one consumer, on a
branch mid v2->v3 transition, on an architecture with no upstream apk port to
share the rebasing. That is the standing risk and it does not go away.

Four bugs this shook out, all silent, all found by a gate rather than by
reading:

  * The prune deleted seventeen packages it had just built. The all -> noarch
    mapping went into the emitter and not into pkg_pkgfile, so the prune looked
    for a name nothing wrote. `make packages` exited 0.
  * Whitespace-separated Depends dropped nineteen packages. opkg's pkg_parse.c
    splits that field on ',' and nothing else, so the whole list became one
    dependency whose name contained spaces and resolved to nothing -- and opkg
    neither warned nor failed. `make build` shipped a payload with no Tornado
    in it. test_the_payload_is_dependency_closed is what catches that now.
  * Ownership was recorded as `nobody`. apk resolves an owner to a NAME against
    <--root>/etc/passwd and the build lane runs --user with no passwd entry, so
    every file in every package would have installed as uid 65534, with no
    error at build or install time. build-packages.sh writes a two-line
    synthetic passwd naming the build user root and points --root at it.
  * Four recipes carried versions apk cannot parse -- two spelled a commit
    +git<sha> where the grammar's hash slot is ~, x264 had a snapshot name
    whose -2245 reads as a malformed release, and helixscreen had
    0.99.115.creator5.3 where apk allows one trailing letter and then only a
    suffix from a closed set of nine words. All four were valid opkg versions,
    which is why nothing had noticed. pkg_version_ok gates every recipe now.

A v3 .apk begins with ADBd magic and is not a tar stream, so there is no apk
equivalent of the `ar -p | gunzip | tar` that hand-unpacked anvil-opkg: you
cannot open a package without a package manager. bin/build-payload.py puts the
cross-built binary on the stick at /mnt/apk instead. It is the same build
anvil-apk-tools contains, that package installs normally, and the bootstrap
copy never lands.

Two gates retired rather than moved. apk's installed database is plain text
(F: sets a directory, R: names a file in it), so the payload-ownership check
reads one file where it used to glob info/*.list. And it carries no clock at
all -- apk records no install time and mkpkg sets no build-time -- so the
Installed-Time normalisation case-build-payload.sh did on every build is gone,
and the test asserts the ABSENCE of a t: line instead. Clock-free by
construction rather than by repair.

Also removed: PKG_SECTION, which 45 recipes declared and nothing read once
apk's schema had no field for it, and the `make apk-host` target -- the
packager is built on demand by bin/build-packages.sh and stamped on the pin,
the way pkg_toolchain and pkg_buildpython already work.

Verified from a cold work/host and work/packages: 47 packages built and signed,
`make build` installs 35 and verifies them against the shipped public key,
`make qa-static` 195 passed, `make qa-replica` 135 passed -- the printer
installing off a simulated USB stick and booting. The installed set was checked
against the declared closure of the eleven roots: 35 expected, 35 installed,
nothing extra and nothing missing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rbKEyLXrzSE6wm3iNApVp
2026-08-31 14:32:07 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 6a4067e863 VFA calibration is one import away, and that import stops klippy booting
The stock app's "magic" VFA calibration is not app-side math at all. Traced
from CommMgr::calibration_vfa (0x0075e694), it sends five G-code strings and
the last one is STEPPER_RESONANCE_FACTORY_CALIBRATE -- a macro in FlashForge's
printer.vibration.cfg over klippy/extras/stepper_resonance_tester.py, which
searches amplitude and phase for the motor torque-ripple compensation the
closed-loop driver applies at 1x/2x/4x the electrical frequency. Every piece is
already here: the module is byte-identical in our vendored fork, the mclib
sections are in our printer.base.cfg, and mclib_set_resonance_damp is in the
stock mainBoardGD dictionary we never rebuild.

WHAT THE TRACE TURNED UP ON THE WAY. Two comments in this repo said the numpy
gap was a small item because the module guards its own import. It does not:
line 1 of stepper_resonance_tester.py is a bare `import numpy as np`, and
klippy.py:122 loads EVERY config section through a load_object that does not
catch ImportError -- the default=None argument only covers a missing file.
printer.base.cfg:19 includes printer.vibration.cfg, which declares
[stepper_resonance_tester]. So on the CPython 3.13 klippy has run on since
53292b4 this is not a lost feature, it is a klippy that never reaches ready.
Confirmed by importing the module with numpy blocked at sys.meta_path.

Both comments are corrected rather than deleted, because the claim they made is
the one a reader would otherwise make again.

Documentation only -- no behaviour changes here. The note lays out the chain
address by address and the three ways out (package numpy, drop numpy from the
module, or guard the import), and says which to reach for first. Nothing should
be built on it before a real printer.log confirms the boot failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr2x7aDaY889qwSxbdLt8
2026-08-31 13:42:57 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 42d876d5eb The auto-PA sweep is host code; only the per-line verdict is in the eBoard
firmwareExe runs a real PA calibration at print start, inside the nozzle clean.
CommMgr::paTestMgr @0x791640 prints one slow-fast-slow line per candidate at
Y = 50 + 5*i for seven fixed PA values, asks the eBoard to score each line, keeps
the smallest that scores 9, repeats until three sweeps have produced a winner and
averages those three. All of that is host code and is recovered here in full.

Only the verdict crosses into the closed toolhead MCU, over two commands that
pa_adjust.py already forwards -- and pa_adjust.py is in our tree byte-identical
to FlashForge's, with [pa_adjust] already in printer.base.cfg. So PA_ACTION and
PA_GET should answer on a Reforge machine today; that is step one of the port.

This supersedes "sensor unknown" in 60-background.md and "callable, not
reproducible" in 25-app-vs-klipper-ownership.md. What stays unknown is narrower:
which sensor the eBoard samples, what ACTION=11 and PC=666 select, and what the
non-9 verdicts mean.

Two findings that shape the port. PA_GET only respond_info()s its value, so the
sweep cannot be a G-code macro -- it needs an extra that queries the eBoard
directly. And PA_ACTION is an immediate MCU command, so FlashForge never
synchronises the measurement window with motion; the port must copy that timing
rather than improve it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kw2rehvQJ7LSc28hPikfqk
2026-08-30 23:03:37 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 f2e0e5fcb2 The manifest protected three files, and none of them needed it
Proposal: on update, rm -rf $MODDIR and extract into the empty
directory. No manifest, no selective delete, no compatibility branch.

The audit is the note. Against a full wipe .install-manifest was
protecting backup/stock, .prev-root-hash and config-installed, and
each one falls: nothing restores from backup/stock and the documented
recovery is a stock reflash; .prev-root-hash is removed by the
password branch; config-installed is a twelve-file snapshot serving
one md5, for a compare whose stated rationale -- "there is no
include-and-override seam" -- stopped being true when
moonraker-custom.conf got included last, and which inverts its own
purpose by keeping the webcam block and API lockdown off any printer
that trips it.

So moonraker.conf joins ff-*.cfg and is simply overwritten, owner-
dropped files under $MODDIR become an accepted loss, and phase 2's
on-printer half in 85-packaging.md is cancelled rather than owed: a
printer that never deletes selectively needs no file database for it.

Phases 1 and 2 in 80-s6-migration.md and 85-packaging.md point here.
No code has moved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXKk3zz8LAUPUDZkVEZ9fR
2026-08-30 15:38:25 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 4850be432c Docs: say what is there now, not what the last three commits removed
Deleting test/ and anvil.conf left claims behind in nine files. Every one of
these was present tense about something that no longer exists.

THE CHELPER SYMBOL GATE, in three places that each described it as live:

  notes/20-klipper-fork.md   said pkgs/klipper gates the .so on "both the ELF
                             flags and test-chelper.py's symbol check". It
                             gates on neither now: the ABI is asked once of the
                             installed filesystem in qa/replica/test_abi.py,
                             and the single-source rule -- the .so is compiled
                             from the chelper sources of the tree it ships -- is
                             the whole defence. The page says that instead.
  notes/85-packaging.md      described the KLIPPER_FORK fallback the script no
                             longer had; the script itself is gone too.
  qa-migration.md            "What survives in test/" listed both the 28 unit
                             tests and test-chelper.py as surviving. It is
                             "What survived in test/, and then did not", and it
                             names what went uncovered rather than leaving that
                             to be discovered.

ANVIL.CONF, in the two notes that still read it at runtime:

  notes/80-s6-migration.md   is a dated plan, so it keeps its phases and gains
                             a "Where this ended up" note: anvil.conf and every
                             MOD_* switch were removed, so the phases that
                             source it describe an intermediate state, and the
                             case-*.sh gates they name are qa/replica/ modules.
  notes/85-packaging.md      predicted seed/ emptying via a postinst that seeds
                             anvil.conf.default. It emptied by anvil.conf being
                             deleted instead. Its payload allowlist said six
                             entries including anvil.conf and bin/busybox; the
                             real one in test_ipk.py has five and neither.

THE TWO STANDALONE TREES under tools/, stale before any of this and now fixed:
tools/python/README.md and tools/supervisor/README.md both told the reader to
run their gate with `test/integration/printer-exec.py` over a case-*.sh, and
one offered a `make test-python` target. None of the three has existed for
several commits. Both now point at qa/replica/, which installs the real package
and asks more than hand-feeding a tarball to a case script could. The four
places tools/python/README.md narrates what case-python.sh proves are past
tense, since that is now a record of how the measurements were taken.
versions.env's sqlite3 note pointed at the same dead target.

NAV: every notes/ page was listed except 85-packaging.md, and qa-migration.md
was reachable only by following a link out of Testing, Building or How it
works. Both are in the nav now, which also clears the two "not included in the
nav" lines from every build. .venv/ is gitignored -- docs/requirements.txt
tells you to make one right there in the tree.

Verified: `mkdocs build --strict` is clean and no longer reports pages outside
the nav. qa/static is 123 passed, its 110 errors the same missing
vendor/opkg-utils as before. No .md file is read by any lane, so the check that
matters here is the build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bLGYpVAeVzAX6NeAxcU42
2026-08-30 01:16:54 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 133e43e05e The layout that took twenty rounds to find was written down nowhere.
The docs were restructured into three sections yesterday. The reasoning lived
in one conversation: which section a page belongs to, what may appear in it,
and which things were removed on purpose. The next session would have opened
mkdocs.yml, seen a nav, and guessed.

.claude/skills/reforge-docs is the map. Three audiences with the test that
decides between them, what belongs in each section and what must never appear
there, and where a new page goes. The rules in it are the ones review actually
produced: an owner page assumes the machine works, says when a step is
optional, and names a file only when the reader has to type it -- which is why
[ff_legacy], SAVE_CONFIG, dropbear, config inventories, theory sections,
expected-output dumps and symptom tables all came out of pages they had been
written into.

It also records what cost time rather than only what was decided. A gain claim
has to be checked against notes/25-app-vs-klipper-ownership.md, because tool
calibration and input shaping are the touchscreen's own sequences reimplemented
and were credited to the mod anyway. --strict validates markdown links and
nothing else, which is how three render bugs reached master through raw HTML,
images and task lists. The README hook is imported once, so mkdocs serve keeps
running stale code after it is edited. Edits anchor on unique strings, because
a file rewritten earlier in a session invalidates every line number -- that one
was a real bug, not a hypothetical. And docs get written against the branch
whose code ships: pages assembled on qa-suite-bridgehead described s6-rc
services that master does not have.

docs-writer.md comes back with it. It was written at the start of that session,
never committed, and swept into a stash by `git stash -u` during a branch move;
it holds the generic craft -- who the reader is, the voice, claim nothing you
have not checked -- and now defers to the skill for the layout instead of
describing a two-section nav that no longer exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3XZh4SqPdXAowVndz6tMr
2026-08-29 00:47:54 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 65a7528e09 The manual assumed a printer that arrives broken. It arrives calibrated.
The pages written on top of the old README inherited its premise: that after
flashing you face a gauntlet -- go/no-go checks, a safety net, a mandatory
calibration -- before the machine can be trusted with a model. That is not
what happens. The factory numbers import themselves on the first boot, a bed
mesh loads at the start of every print, and the mod is built to run the files
the stock firmware ran. A printer that printed before the flash prints after
it. "Your first print" now says so in its first line, and everything under it
is optional, ordered by what you would actually reach for: check the offsets
are applied, and calibrate only if that disagrees with the machine.

Three audiences now, because owner and contributor was not the real split.
"For experienced users" sits between them: How a print runs, and How
calibration works. Someone who knows Klipper and wants to change what happens
around their file has a place to go that is neither a walkthrough nor the
build system.

What was cut is most of what changed. Pages that were cryptic to the reader
they were aimed at -- Printing, Upgrading -- are out of the nav and kept on
disk to be rewritten rather than deleted. Within the pages that stayed:
Expected output, Probe sampling, What is being measured, the symptom table,
the dark-screen diagnosis, TOOL_Z_ADJUST, PLATE_CHECK, the ff-*.cfg
inventory, [ff_legacy] and SAVE_CONFIG in a paragraph about factory
calibration. Filenames and internals earn their place on an owner page only
when the reader has to type them or look at them; the rest moved to the
experienced pages or to Internals, where the audience wants them.

Calibration became a section rather than a page, because tool offsets are not
the only calibration: bed mesh has its own page now, and its own gotcha
written down -- BED_MESH_CALIBRATE writes `default`, a print loads
`MESH_DATA`, so a mesh probed by hand is a mesh no print will use. Input
shaping is described but has no procedure page yet.

Several claims came out because they were not true. "Is this for you" listed
calibration and input shaping as things the mod gives you; both are the
touchscreen's own sequences, recovered and reimplemented, and the notes say
so plainly. What the mod actually gives you is that anything speaking
Moonraker can drive the machine -- on stock, FlashForge's Klipper does not
toolchange at all, it raises a flag and waits for an application that is not
watching a Mainsail print, and the print hangs at the first T2. The page now
opens by telling most owners not to flash it.

Contributors are still deferred. Their pages are regrouped, not rewritten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3XZh4SqPdXAowVndz6tMr
2026-08-28 23:33:29 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 0af15a549a The front page shipped with broken images and literal [x] boxes.
Three defects, all in the site machinery rather than the prose, all visible
on the first page anyone opens.

`r"\1=\""` is not an escaped quote. In a raw string that backslash is a
character, so every `src="docs/…"` the README hook rewrote came out as
`src=\"helixscreen-print-status.webp"` and the browser asked for a file whose
name began with a quote. Both screenshots on the front page 404'd.

`- [x]` is python-markdown's plain list syntax unless pymdownx.tasklist is
enabled, so the Status checkboxes -- the list this project leads with, and
the thing a reader decides on -- rendered as the literal text `[x]`. Enabled,
with custom_checkbox so they get the theme's own mark. The checklists in
is-this-for-you.md and after-the-flash.md were the same bug.

And mkdocs rewrites markdown links but not raw HTML attributes, which is
invisible until a page with an `<img>` in it is not the front page: with
directory URLs, is-this-for-you.md is served from /is-this-for-you/, one level
below the image it names. The hook now rewrites relative src/href in raw HTML
too, prefixing `../` per level of the page's own URL, so an `<img>` works from
wherever the page ends up -- and it points at a file that must exist, so a
typo stays a broken link the build can still catch.

Found by serving the site and looking at it, which is the only way any of
these three surface: --strict validates markdown links, and every one of
these was in HTML or in text it had no reason to read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3XZh4SqPdXAowVndz6tMr
2026-08-28 22:24:39 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 a6098c7f64 The README was the manual. It is a front door now, and the manual is nine pages.
Fourteen sections, install through troubleshooting, in the one file that
existed when there was nowhere else to put them. The site gave the manual a
home; this moves it there and orders it by what the reader is doing rather
than by which subsystem owns the answer. Someone whose printer will not boot
should not have to know that the symptom table lives in a file called
hardware-testing.md.

Three topics were documented two and three times over, and each copy was free
to drift: install in README and toolchange.md, calibration in README,
calibration.md and toolchange.md, slicer setup in README and toolchange.md,
"before you start" in all three. Each is now in one place.

The owner path is Is this for you -> Installing -> After the flash -> Your
first print -> Nozzle-offset calibration -> Printing -> Upgrading -> When
something goes wrong, with error codes, versions and how-it-works behind a
Reference group. The README keeps what a front door keeps: what this is, the
status checkboxes, what you give up, which file to download, and where to go
next.

toolchange.md was four documents in one 38 KB file. The owner half -- slicer
setup, filament, runout, the offset check before a first print -- is in
Printing and Your first print; the design notes, input shaper, roadmap,
HelixScreen and reverse-engineering notes stay, and the page moves to
Internals. hardware-testing.md was split down the middle by audience the same
way: the go/no-go checks and the symptom table were the owner's, and only
keeping the two verification files honest as the macros change is ours.

Two things this fixes rather than moves. The error-code table was
notes/70-error-codes.md, an internals note the README sent owners to; it is
docs/error-codes.md now, a page. And toolchange.md still opened "Not yet run
on hardware" while the README's checkboxes described a Creator 5 Pro that has
been printing for weeks -- the two pages have disagreed since the first
hardware run.

Contributors are deferred, as agreed: those pages are regrouped, not
rewritten. The nav says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3XZh4SqPdXAowVndz6tMr
2026-08-28 22:20:19 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 7347f56418 docs/ rendered on GitHub and nowhere else. It is a built, versioned site now.
The alternative was a wiki, and a wiki is a separate git repo: a page would
stop moving in the same commit and the same review as the code it describes,
with nothing able to catch the drift, and there would be exactly one live
version of it. This project ships a package per tag and tells owners which
checkboxes have touched hardware; the docs for the build someone flashed have
to be readable, so Read the Docs builds this repo per tag and `latest` off
master, with the version switcher that follows from that.

Nothing moved. The audience split that the pages already have -- owner pages
against build-and-test pages -- is the nav in mkdocs.yml, so a reader lands in
their own section and never falls out of it, while the 30+ relative links
between README.md, docs/*.md and docs/notes/ keep working in the repo view and
the links people have already shared keep resolving. notes/70-error-codes.md
sits in the owner section because README.md was already sending owners there.

Two things had to hold for that to be true, and hooks/mkdocs_readme.py is
both. Links out of docs/ -- `../payload/klipper/extras/ff_tool.py` and 18
others in toolchange.md -- are written relative because that is what works
where a contributor reads them, in the repo; they become GitHub URLs at build
time, and a link whose target does not exist is left alone so the build still
fails on it. README.md is the front door and stays it: docs/index.md is
generated from it on every build and gitignored, rather than being a second
front page free to drift from the first.

Anchors use GitHub's slug rules for the same reason. `#runout--clog` is what
GitHub makes of `## Runout / clog`, python-markdown would have made
`#runout-clog`, and toolchange.md:340 already pointed at the first.

CI builds it with --strict, which is the cheap half of keeping docs true: a
rename in payload/ now breaks the build instead of quietly leaving a page
pointing at nothing. It cannot check that the prose is still right. Only a
reader can.

The site is not up yet -- the project has to be created on readthedocs.org and
its versions activated -- so nothing links to it from README.md. The slug is
free; mkdocs.yml claims it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3XZh4SqPdXAowVndz6tMr
2026-08-28 22:02:43 +03:00