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: 4 additions & 2 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,10 @@ git checkout -B _sync2 origin/alpha && git merge --no-ff origin/beta && git push
same change. Its config section arrives with the same update, and a section whose module is missing
is not a missing feature — klippy refuses the whole config.
- Changing `config-templates/AddOn.cfg.template`? Read the header of `scripts/addon_merge.py` first.
That file is never regenerated on a printer that already has one, so only whole `#@FEAT` blocks
reach existing machines. A change *inside* an existing block reaches nobody.
That file is never regenerated on a printer that already has one, so a new `#@FEAT` block is what
normally reaches existing machines. A change *inside* a block reaches nobody unless the template
also names that block in a `#@REVISE`, which replaces the printer's copy of it in place — read what
that costs before reaching for it.

## Version numbers

Expand Down
123 changes: 123 additions & 0 deletions .github/ci/check-addon-markers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""Every marker in AddOn.cfg.template does something, or the build stops.

addon_merge.py guards itself well at RUN time: an unpaired #@DROP is refused, a #@RETIRE for a
feature the template still ships is ignored, a #@REVISE naming a block that is not there is skipped.
Each of those is the right thing to do on a printer -- and each is silent to whoever wrote the
marker, because the message is printed on a machine in somebody's workshop rather than in the pull
request that got it wrong. The P114 gate ran two versions out of date for months for exactly this
kind of reason: the delivery looked like it was working.

So the same rules are checked here, where a typo is still cheap.
"""
import re
import sys
import os

TPL = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..",
"config-templates", "AddOn.cfg.template")

FEAT = re.compile(r'^#@FEAT\s+(\S+)\s*\|\s*(.*)$')
RETIRE = re.compile(r'^#@RETIRE\s+(\S+)\s*\|\s*(.*)$')
REVISE = re.compile(r'^#@REVISE\s+(\S+)\s*\|\s*(.*)$')
DROP = re.compile(r'^#@DROP\s+(.+?)\s*\|\s*(.*)$')
END = "#@ENDFEAT"
OFF = "#:off:"
SECTION = re.compile(r'^(?:' + re.escape(OFF) + r')?\[([^\]]+)\]\s*$')


def main():
path = sys.argv[1] if len(sys.argv) > 1 else TPL
lines = open(path, encoding="utf-8").read().split("\n")
bad = []

# 1. Every block is closed, and closed before the next one opens. addon_merge refuses to touch an
# unterminated block precisely because removing it would swallow the neighbour -- which in this
# file means the two print macros.
feats, open_at, ids = {}, None, []
for n, l in enumerate(lines, 1):
m = FEAT.match(l)
if m:
if open_at is not None:
bad.append("line %d: #@FEAT %s opens while %s is still open (a missing #@ENDFEAT)"
% (n, m.group(1), open_at[1]))
open_at = (n, m.group(1))
ids.append(m.group(1))
feats[m.group(1)] = [n, None]
elif l.startswith(END):
if open_at is None:
bad.append("line %d: #@ENDFEAT with no #@FEAT above it" % n)
else:
feats[open_at[1]][1] = n
open_at = None
if open_at is not None:
bad.append("line %d: #@FEAT %s is never closed" % (open_at[0], open_at[1]))

dupes = sorted({f for f in ids if ids.count(f) > 1})
for d in dupes:
bad.append("#@FEAT %s is declared more than once — the merge would only ever see the first" % d)

shipped = set(feats)

# 2. A #@RETIRE for a feature the template still ships is ignored at run time (addon_merge line
# ~231). Silently: the block stays on every printer and the author believes it went.
for n, l in enumerate(lines, 1):
m = RETIRE.match(l)
if m and m.group(1) in shipped:
bad.append("line %d: #@RETIRE %s, but the template still ships that #@FEAT — "
"the retirement would be ignored" % (n, m.group(1)))

# 3. A #@REVISE is the mirror image: it can only act on a block the template also ships, because
# the replacement body comes from that block.
revised = set()
for n, l in enumerate(lines, 1):
m = REVISE.match(l)
if not m:
continue
revised.add(m.group(1))
if m.group(1) not in shipped:
bad.append("line %d: #@REVISE %s, but no #@FEAT %s in this template — "
"there is nothing to revise it to" % (n, m.group(1), m.group(1)))
for f in sorted(revised & {m.group(1) for m in (RETIRE.match(l) for l in lines) if m}):
bad.append("#@REVISE and #@RETIRE both name %s — the merge would do neither" % f)

# 4. A #@DROP must be paired with a block that hands the section back, or it is a delete
# instruction with nothing behind it.
in_block = set()
for fid, (a, b) in feats.items():
if b is None:
continue
for l in lines[a - 1:b]:
m = SECTION.match(l)
if m:
in_block.add(m.group(1).strip())
for n, l in enumerate(lines, 1):
m = DROP.match(l)
if m and m.group(1) not in in_block:
bad.append("line %d: #@DROP %s, but no #@FEAT block in this template declares that "
"section — the merge would refuse to remove it" % (n, m.group(1)))

# 5. Two sections of the same name in one template is a config klippy refuses, and it would be
# delivered to every fresh install.
seen = {}
for n, l in enumerate(lines, 1):
m = SECTION.match(l)
if not m:
continue
name = m.group(1).strip()
if name in seen:
bad.append("line %d: [%s] is declared twice (first at line %d)" % (n, name, seen[name]))
seen[name] = n

if bad:
print("AddOn.cfg.template markers:")
for b in bad:
print(" ERROR %s" % b)
return 1
print("AddOn.cfg.template: %d features, %d revisions, every marker resolves."
% (len(shipped), len(revised)))
return 0


if __name__ == "__main__":
sys.exit(main())
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ jobs:
- name: Anchors and step numbers
run: python3 .github/ci/check-docs.py

# A marker that resolves to nothing is refused on the printer and silent to whoever wrote it:
# the message goes to a journal in somebody's workshop, not to the pull request. The P114 gate
# ran two versions out of date for months for that kind of reason. Same rules, checked here.
- name: AddOn.cfg markers
run: python3 .github/ci/check-addon-markers.py

# The documents overlap on purpose and drift for free. This fails on text that has quietly
# acquired a second owner, and -- when there is a base commit to compare against -- points out
# sentences elsewhere that closely matched the wording just changed here. The second half is a
Expand Down
2 changes: 1 addition & 1 deletion README.html
Original file line number Diff line number Diff line change
Expand Up @@ -985,7 +985,7 @@ <h2 id="orcaslicer-multicolor-ams-auto-mode">OrcaSlicer — multicolor
cell. <strong><code>G30</code></strong> then loads your saved
<code>phrozen</code> mesh instantly — calibrating and saving it the
first time, if none exists yet. Homing is automatic. It needs the
<code>auto_mode2</code> and <code>ams</code> features in
<code>auto_mode</code> and <code>ams</code> features in
<code>AddOn.cfg</code>, both on by default, and the
<code>gcode_shell_command</code> extension.</p>
</details>
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ colour** — otherwise a long print changes shade partway through, and nothing w
The start G-code heats the **bed straight to print temperature** and holds the **nozzle at a probe-safe
140 °C** through home and probe, so it cannot ooze onto the load cell. **`G30`** then loads your saved
`phrozen` mesh instantly — calibrating and saving it the first time, if none exists yet. Homing is
automatic. It needs the `auto_mode2` and `ams` features in `AddOn.cfg`, both on by default, and the
automatic. It needs the `auto_mode` and `ams` features in `AddOn.cfg`, both on by default, and the
`gcode_shell_command` extension.

</details>
Expand Down
22 changes: 14 additions & 8 deletions config-templates/AddOn.cfg.template
Original file line number Diff line number Diff line change
Expand Up @@ -773,14 +773,20 @@ gcode:
#@RETIRE ams | AMS on/off switch - the AMS is detected now, so there is nothing to declare
#

# PHROZEN_TOOLCHANGE lives inside this block, and a change *inside* a block reaches fresh flashes
# and nowhere else -- the merge adds and removes whole blocks, it never rewrites one a printer
# already has. So the corrected macro ships under a new id and the old block is retired in the same
# run, under the same backup, exactly as the g30 -> g30_meshdone replacement did. The pairing is
# enforced by addon_merge, not trusted: if this block cannot be added, the retirement does not run,
# so the two print macros this block carries can never go missing.
#@RETIRE auto_mode | replaced by auto_mode2, which fixes a retract that assumed relative E
#@FEAT auto_mode2 | Orca auto AMS-mode: reads the 'ams' flag -> P0 M1/M2/M3 at print start
# PHROZEN_TOOLCHANGE lives inside this block. Blocks are the unit the merge moves, so an edit made
# inside one used to reach fresh flashes and nowhere else -- which is why this marker exists. It says:
# this block is the kit's, bring an existing printer's copy up to the version below. The owner's
# on/off toggle is carried over, a block that already matches is left alone and not even mentioned,
# and a block the owner deleted by hand is NOT put back -- that stays the seeded record's decision.
# One printer generation carried this block as `auto_mode2`, from the short-lived attempt to deliver
# the retract fix by renaming the block instead of revising it. #@REVISE cannot reach those: they have
# no `auto_mode` to revise. They get `auto_mode` through the normal add path, and this line takes the
# old copy out in the same run -- without it such a printer would end up with both, which is two
# [gcode_macro PHROZEN_TOOLCHANGE] sections and a config klippy refuses.
#@RETIRE auto_mode2 | folded back into auto_mode, which is the name it should never have lost

#@REVISE auto_mode | the toolchange retract assumed relative E; see the M83 note in the macro
#@FEAT auto_mode | Orca auto AMS-mode: reads the 'ams' flag -> P0 M1/M2/M3 at print start
# ───── Flag-driven AMS mode for OrcaSlicer (Arco Unleashed) ─────
# The 'ams' flag follows the AMS device node, kept in step by [arco_tool_gate] -- nobody sets
# it, and there is nothing to remember to set. This macro reads it at print start and applies
Expand Down
Loading