An adversarial re-read of master..HEAD, checking the new text rather than
trusting it. The substantive work held up -- all eight dead-code removals prove
out, SAFE-MODE->MOD_UI is complete, A3's reordering is correct -- but eleven
claims were wrong, and one made a document worse than it found it:
* docs/notes/10-hardware.md said `temperature_sensor motor_value` on
eboard:PA1 is "not in anything this repo ships". It is:
printer.motor.cfg:37 declares it and that file is in the package, which is
also why ff-toolchange.cfg can name MOTOR_STOP. The real nuance is that
printer.base.cfg does not include printer.motor.cfg. A correct claim in
25-app-vs-klipper-ownership.md was "corrected" into a false denial.
* docs/toolchange.md pointed at a worked example that reaches 0.125 mm. The
only example is NOZZLE=220 BED=80, which gives 0.045; the 0.08 bed term
needs a bed at 100 or above. Both numbers now stated, no forward reference.
* docs/testing.md said 48 tests; pytest collects 53.
* test_config_ownership.py cited bin/patch.sh:80/:88/:128 -- correct on
master, shifted by two when this branch deleted MARK_BEGIN/MARK_END. The
line numbers are dropped rather than re-pinned, so they cannot rot again.
* test_includes.py invented a reason for ff-legacy being last ("so its import
sees every [ff_tool] already declared"). ff_legacy resolves tools at
klippy:ready; order does not matter.
* fetch-assets.sh's replacement usage text omitted BUILD_MOONRAKER and was
less accurate than the vague line it replaced.
* 49-runout's sensor table still claimed `extruder: extruderN` after the A1
fix made that true only of our override, not of the stock file it describes.
* firmwareExe kept "newest version directory" after the same claim was
corrected in how-it-works.md.
* 25-app inverted the preflight call order and omitted that an M23 print gets
neither call.
* creator5-safe-moves.gcode called the prepare's G1 Z10 "the one move" below
Z50; the G28 and offset move before it also go below.
* DOC-DRIFT-AUDIT.md asserted both "Every finding has been applied" and
"Nothing in this file has been applied", and cited five commits after a
hash the rebase had rewritten.
53 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5.7 KiB
Hardware & MCU topology
MCUs (Klipper multi-MCU)
| Klipper MCU | Serial | Role |
|---|---|---|
mcu |
(main board, see printer.base.cfg on device) | X/Y/Z steppers, bed heater PD2, bed thermistor PC3 |
eboard |
/dev/ttyS5 |
Carriage extruder board — ONE stepper driver shared by whichever head is mounted (PB14/PB15/PB12) |
eheaterboard |
/dev/ttyS4 |
4 hotend heaters PC6..PC9, 4 thermistors PC0/PA4/PA5/PA7, 24V rail PA3 |
levelboard |
/dev/ttyS7 |
The fixed inductive "cylinder" under the bed — all three [e_stop X/Y/Z] sit on levelboard:PD0 and drive the nozzle-offset calibration. The carriage eddy used for G28 Z and bed mesh is [probe] on eboard:PG0 — see 25-app-vs-klipper-ownership.md |
The app pokes ttyS4/S5/S7 (sends a char, waits for "Ready") before starting Klipper.
Other serial: /dev/ttyS3 = drying box (own protocol, not a Klipper MCU).
The bootloader handshake, as decompiled
Not reconstructed from behaviour — this is _Z17bootSerialHeatMcuv,
_Z23bootSerialMainEboardMcuv and _Z23bootSerialLevelBoardMcuv disassembled out
of usr/prog/PROGRAM/software/firmwareExe (1.9.7). The three are the same function
with a different device path and different printf strings; usr/prog/klipper/checkEboard
is a separate, older -O2 build of the eboard one.
open(dev, O_RDWR) # no O_NOCTTY, no O_NONBLOCK
fcntl(fd, F_SETFL, 0)
tcgetattr; cfmakeraw
c_cc[VMIN]=0; c_cc[VTIME]=1 # MIPS: VMIN=4, VTIME=5
cfsetispeed/cfsetospeed(B115200) # 0x1002 on MIPS
c_iflag &= ~(IGNBRK|ICRNL|IXON) # &= ~0x501
c_oflag &= ~OPOST
c_cflag = (c_cflag & ~(CRTSCTS|PARODD|CSTOPB|CSIZE)) | CLOCAL|CREAD|CS8
c_lflag &= ~(ISIG|ICANON|ECHO|ECHOE)
tcsetattr(fd, 0x540E, &tio) # see below
isReady = 0; isAck = 1
for (i = 0; i < 50; i++) { # ~5s ceiling at VTIME=1
n = read(fd, buf, 50)
if (n == 0) continue
if (find(buf, "Ready") != npos) { isReady = 1; break }
else { isReady = 1 } # ttyS5/ttyS7 only
}
send = 'A'
if (isReady)
for (j = 0; j < 50; j++) {
write(fd, &send, 1)
read(fd, &recv, 1)
if (recv == 6) { isAck = 1; break }
}
close(fd)
return (isReady == 1 && isAck == 1) ? 0 : -1
Four things worth keeping in mind, all of which bit us:
'A'goes out fifty times, not once. The board gets fifty chances.- A non-0x06 reply is ignored.
isAckis initialised to 1 and is never assigned 0 anywhere in the function, so the ack byte cannot fail the handshake — it only decides whether the loop stops early. The level board answers0x01every time. Refusing the handshake over that byte is what stranded ttyS7. - The ready phase differs by board. On
ttyS4only a literal"Ready"setsisReady. OnttyS5andttyS7any byte does, banner or not — which is whycheckEboardhappily sends 'A' at an eboard that is already running Klipper and is just returning garbage at the wrong baud.ff-mcu-bringup.pydeliberately does not copy that: only a banner earns a write. tcsetattr(fd, 0x540E, ...)— that isTCSETS, where POSIX wantsTCSANOW/TCSADRAIN/TCSAFLUSH. Both binaries do it, so it is in FlashForge's source, not a build artefact. It evidently does not fail on the printer (the routines go on to print their read loop), butff-mcu-bringup.pypassesTCSANOW, which is correct either way.
The ready phase is only ~5s and it does not wait for a second banner, so stock only ever worked when it ran immediately after the board powered up. The bootloader re-sends its banner on a period longer than that, which is why a bring-up run later in the boot can see nothing at all and still be looking at a board in its bootloader.
Extruder config (embedded default printer.cfg)
All four [extruderN] sections share the same step/dir/enable pins — only the mounted
head is electrically active; the fork tolerates the duplicate pin claims (vanilla
Klipper would refuse). Gear ratio 6.5:1, rotation_distance 19.15, Generic 3950
sensors, max_temp 350.
Klipper objects the UI expects (all present in the stock config)
- Heaters/sensors:
extruder..extruder3,heater_bed,heater_generic chamber_heater(Pro only — it lives inprinter.chamber.cfg, and the plain Creator 5 gets[temperature_sensor chamber]instead),temperature_sensor ptcTemp,temperature_sensor extruder_servo_value(eboard:PA0),temperature_sensor adc_current_value(eheaterboard:PC1).temperature_sensor motor_value(sensor_type: motor_current_sensor,eboard:PA1) is real and ships, but inprinter.motor.cfg, notprinter.base.cfg— soprinter.base.cfgdoes not include it and the machine's ownprinter.cfgmust.MOTOR_STOP,MOTOR_GRABand the rest of the lock macros live in that same file, which is whyff-toolchange.cfgcan name them. - Fans:
fan_generic fanM106(part fan, via customSET_FAN_M106),chamber_fan,chamber_cool_fan,chamber_heat_fan,chamber_loop_fan. - Buttons (
gcode_button):extruder_pos1..4(head present in dock N),extruder_grab1..4(head on carriage),frontDoor,topDoor. That is the whole family — confirmed against this printer'sprinter.base.cfg(seeff-toolchange.cfg's sensor note): there is no bareextruder_grab, noextruder_check_pos, noservo_min/servo_max, andmotor_pinis commented out inprinter.motor.cfg. - Filament:
filament_switch_sensor fd_ex0..3(presence), custom motion sensorsfm_ex0..3. - Steppers:
manual_stepper gear_stepper(the tool LOCK motor, not a feeder; its endstop doubles as the lock status signal — see30-toolchange.md). - Fork's
virtual_sdcardextra status fields:channel,refuelling,after_channel_g1,doingChangeEx.