From 237378741d5024f3532a36746503421b77e32c7a Mon Sep 17 00:00:00 2001 From: suchmememanyskill <38142618+suchmememanyskill@users.noreply.github.com> Date: Tue, 5 May 2026 23:45:14 +0200 Subject: [PATCH 01/19] Beginnings of AFC --- .../files/0003-drv8833-motor-control.patch | 1198 +++++++++++++++++ .../klipper/kalico_2026.02.00.inc | 1 + .../mcu-flasher/mcu-flasher_0.1.0.bb | 2 +- .../recipes-data/klipper-afc-addon/afc_0.1.bb | 44 + .../klipper-afc-addon/files/afc.cfg | 110 ++ .../klipper-afc-addon/files/canvas.cfg | 231 ++++ 6 files changed, 1585 insertions(+), 1 deletion(-) create mode 100644 meta-opencentauri/recipes-apps/klipper/files/0003-drv8833-motor-control.patch create mode 100644 meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb create mode 100644 meta-opencentauri/recipes-data/klipper-afc-addon/files/afc.cfg create mode 100644 meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg diff --git a/meta-opencentauri/recipes-apps/klipper/files/0003-drv8833-motor-control.patch b/meta-opencentauri/recipes-apps/klipper/files/0003-drv8833-motor-control.patch new file mode 100644 index 00000000..896124fc --- /dev/null +++ b/meta-opencentauri/recipes-apps/klipper/files/0003-drv8833-motor-control.patch @@ -0,0 +1,1198 @@ +diff --git a/klippy/extras/drv8833.py b/klippy/extras/drv8833.py +new file mode 100644 +index 00000000..c6271960 +--- /dev/null ++++ b/klippy/extras/drv8833.py +@@ -0,0 +1,786 @@ ++# Support for DRV8833 based CANVAS-style motor control ++# ++# This file may be distributed under the terms of the GNU GPLv3 license. ++import math ++import os ++ ++HALL_POLL_TIME = 0.001 ++CONTROL_INTERVAL = 0.010 ++MCU_REPORT_INTERVAL = 0.050 ++DEBUG_LOG_INTERVAL = 0.500 ++DEFAULT_DUTY = 50.0 ++MAX_DUTY_STEP = 4.0 ++DEFAULT_PID_KP = 1.2 ++DEFAULT_PID_KI = 0.8 ++DEFAULT_PID_KD = 0.02 ++HALL_RESOLUTION_SCALE = 1000000 ++SPEED_SCALE = 1000 ++DUTY_SCALE = 10 ++PID_PARAM_SCALE = 1000 ++AUTOTUNE_SETTLE_TIME = 0.8 ++AUTOTUNE_SAMPLE_TIME = 0.4 ++AUTOTUNE_BAND_RATIO = 0.05 ++AUTOTUNE_MIN_BAND = 1.0 ++AUTOTUNE_RELAY_RATIO = 0.15 ++AUTOTUNE_MIN_RELAY_DUTY = 4.0 ++AUTOTUNE_MAX_RELAY_DUTY = 20.0 ++AUTOTUNE_BASELINE_STEPS = 6 ++AUTOTUNE_REQUIRED_PAIRS = 3 ++AUTOTUNE_TIMEOUT = 45.0 ++ ++ ++def _clamp(value, min_value, max_value): ++ return max(min_value, min(max_value, value)) ++ ++ ++class Drv8833Interface: ++ def drv8833_set_speed(self, speed: float): ++ raise NotImplementedError() ++ ++ def drv8833_move(self, speed: float, distance: float): ++ raise NotImplementedError() ++ ++ ++class MCUDrv8833Controller: ++ def __init__(self, config, motor_fwd_params, motor_rwd_params, hall_params): ++ self.printer = config.get_printer() ++ self.reactor = self.printer.get_reactor() ++ self.mcu = motor_fwd_params["chip"] ++ self.oid = self.mcu.create_oid() ++ self.motor_fwd_pin = motor_fwd_params["pin"] ++ self.motor_rwd_pin = motor_rwd_params["pin"] ++ self.hall_pin = hall_params["pin"] ++ self.hall_pullup = hall_params["pullup"] ++ self.cycle_time = config.getfloat("motor_cycle_time", 0.002, above=0.0) ++ self.hall_resolution = config.getfloat( ++ "motor_hall_resolution", above=0.0 ++ ) ++ self.default_duty = config.getfloat( ++ "default_duty", DEFAULT_DUTY, minval=0.0, maxval=100.0 ++ ) ++ self.max_duty_step = config.getfloat( ++ "max_duty_step", MAX_DUTY_STEP, above=0.0, maxval=100.0 ++ ) ++ self.pid_kp = config.getfloat("pid_kp", DEFAULT_PID_KP, minval=0.0) ++ self.pid_ki = config.getfloat("pid_ki", DEFAULT_PID_KI, minval=0.0) ++ self.pid_kd = config.getfloat("pid_kd", DEFAULT_PID_KD, minval=0.0) ++ self.last_active = False ++ self.last_manual = False ++ self.last_count = 0 ++ self.last_speed_mm_s = 0.0 ++ self.last_duty_cycle = 0.0 ++ self.last_status_time = 0.0 ++ self.status_sequence = 0 ++ self.status_callback = lambda: None ++ self._set_cmd = None ++ self._manual_cmd = None ++ self._set_pid_cmd = None ++ self.mcu.register_response( ++ self._handle_status, "drv8833_status", self.oid ++ ) ++ self.mcu.register_config_callback(self._build_config) ++ ++ def _pid_to_mcu(self, value): ++ return int(round(value * PID_PARAM_SCALE * DUTY_SCALE)) ++ ++ def _build_config(self): ++ pwm_max = int(self.mcu.get_constant_float("PWM_MAX") + 0.5) ++ cycle_ticks = self.mcu.seconds_to_clock(self.cycle_time) ++ poll_ticks = self.mcu.seconds_to_clock(HALL_POLL_TIME) ++ control_ticks = self.mcu.seconds_to_clock(CONTROL_INTERVAL) ++ report_ticks = self.mcu.seconds_to_clock(MCU_REPORT_INTERVAL) ++ hall_resolution = int(round(self.hall_resolution * HALL_RESOLUTION_SCALE)) ++ default_duty = int(round(self.default_duty * DUTY_SCALE)) ++ max_duty_step = int(round(self.max_duty_step * DUTY_SCALE)) ++ pid_kp = self._pid_to_mcu(self.pid_kp) ++ pid_ki = self._pid_to_mcu(self.pid_ki) ++ pid_kd = self._pid_to_mcu(self.pid_kd) ++ self.mcu.add_config_cmd( ++ "config_drv8833 oid=%d motor_fwd_pin=%s motor_rwd_pin=%s " ++ "cycle_ticks=%d hall_pin=%s hall_pullup=%d poll_ticks=%d " ++ "control_ticks=%d report_ticks=%d hall_resolution=%d " ++ "pwm_max=%d default_duty=%d max_duty_step=%d" ++ % ( ++ self.oid, ++ self.motor_fwd_pin, ++ self.motor_rwd_pin, ++ cycle_ticks, ++ self.hall_pin, ++ self.hall_pullup, ++ poll_ticks, ++ control_ticks, ++ report_ticks, ++ hall_resolution, ++ pwm_max, ++ default_duty, ++ max_duty_step, ++ ) ++ ) ++ self.mcu.add_config_cmd( ++ "drv8833_set_pid oid=%d kp=%d ki=%d kd=%d" ++ % (self.oid, pid_kp, pid_ki, pid_kd), ++ is_init=True, ++ ) ++ self.mcu.add_config_cmd( ++ "drv8833_set oid=%d enable=0 direction=1 target_speed=0 stop_ticks=0" ++ % (self.oid,), ++ on_restart=True, ++ ) ++ self._set_cmd = self.mcu.lookup_command( ++ "drv8833_set oid=%c enable=%c direction=%c target_speed=%u stop_ticks=%u" ++ ) ++ self._manual_cmd = self.mcu.lookup_command( ++ "drv8833_manual oid=%c enable=%c direction=%c duty=%hu" ++ ) ++ self._set_pid_cmd = self.mcu.lookup_command( ++ "drv8833_set_pid oid=%c kp=%u ki=%u kd=%u" ++ ) ++ ++ def _handle_status(self, params): ++ self.last_active = bool(params["active"]) ++ self.last_manual = bool(params["manual"]) ++ self.last_count = params["count"] ++ self.last_speed_mm_s = params["speed"] / float(SPEED_SCALE) ++ self.last_duty_cycle = params["duty"] / float(DUTY_SCALE) ++ self.last_status_time = self.reactor.monotonic() ++ self.status_sequence += 1 ++ self.status_callback() ++ ++ def start(self, direction, target_speed): ++ self.start_move(direction, target_speed, 0) ++ ++ def start_move(self, direction, target_speed, stop_ticks): ++ set_cmd = self._set_cmd ++ if set_cmd is None: ++ raise self.printer.command_error("drv8833 controller not configured") ++ set_cmd.send( ++ [ ++ self.oid, ++ 1, ++ 1 if direction > 0 else 0, ++ int(round(target_speed * SPEED_SCALE)), ++ int(stop_ticks), ++ ] ++ ) ++ ++ def manual_start(self, direction, duty_cycle): ++ manual_cmd = self._manual_cmd ++ if manual_cmd is None: ++ raise self.printer.command_error("drv8833 controller not configured") ++ manual_cmd.send( ++ [ ++ self.oid, ++ 1, ++ 1 if direction > 0 else 0, ++ int(round(_clamp(duty_cycle, 0.0, 100.0) * DUTY_SCALE)), ++ ] ++ ) ++ ++ def set_pid(self, kp, ki, kd): ++ set_pid_cmd = self._set_pid_cmd ++ if set_pid_cmd is None: ++ raise self.printer.command_error("drv8833 controller not configured") ++ self.pid_kp = kp ++ self.pid_ki = ki ++ self.pid_kd = kd ++ set_pid_cmd.send( ++ [ ++ self.oid, ++ self._pid_to_mcu(kp), ++ self._pid_to_mcu(ki), ++ self._pid_to_mcu(kd), ++ ] ++ ) ++ ++ def stop(self): ++ set_cmd = self._set_cmd ++ if set_cmd is None: ++ raise self.printer.command_error("drv8833 controller not configured") ++ set_cmd.send([self.oid, 0, 1, 0, 0]) ++ ++ ++class Drv8833PIDAutoTune: ++ def __init__(self, lane, gcmd): ++ self.lane = lane ++ self.gcmd = gcmd ++ self.reactor = lane.reactor ++ self.controller = lane.hall_controller ++ self.target_speed = gcmd.get_float("SPEED", 40.0, above=0.0) ++ direction = gcmd.get("DIRECTION", "forwards").strip().lower() ++ if direction not in ("forwards", "forward", "backwards", "backward"): ++ raise gcmd.error("DIRECTION must be forwards or backwards") ++ self.direction = 1 if direction.startswith("for") else -1 ++ self.band = gcmd.get_float( ++ "BAND", ++ max(AUTOTUNE_MIN_BAND, self.target_speed * AUTOTUNE_BAND_RATIO), ++ above=0.0, ++ ) ++ self.settle_time = gcmd.get_float( ++ "SETTLE_TIME", AUTOTUNE_SETTLE_TIME, above=0.0 ++ ) ++ self.sample_time = gcmd.get_float( ++ "SAMPLE_TIME", AUTOTUNE_SAMPLE_TIME, above=0.0 ++ ) ++ self.timeout = gcmd.get_float("TIMEOUT", AUTOTUNE_TIMEOUT, above=0.0) ++ self.write_file = gcmd.get_int("WRITE_FILE", 0) ++ self.eventtime = self.reactor.monotonic() ++ self.data = [] ++ self.peaks = [] ++ self.switch_times = [] ++ ++ def _pause(self, duration): ++ self.eventtime = self.reactor.pause(self.eventtime + duration) ++ return self.eventtime ++ ++ def _wait_for_status(self, timeout=1.0): ++ start_seq = self.controller.status_sequence ++ deadline = self.eventtime + timeout ++ while self.controller.status_sequence == start_seq: ++ self.eventtime = self.reactor.pause( ++ min(deadline, self.eventtime + MCU_REPORT_INTERVAL) ++ ) ++ if self.eventtime >= deadline and self.controller.status_sequence == start_seq: ++ raise self.gcmd.error("Timed out waiting for drv8833 status update") ++ sample = ( ++ self.controller.last_status_time or self.eventtime, ++ self.controller.last_speed_mm_s, ++ self.controller.last_duty_cycle, ++ ) ++ self.data.append(sample + (self.target_speed,)) ++ return sample ++ ++ def _measure_speed(self, duty_cycle): ++ self.lane._start_manual(self.eventtime, self.direction, duty_cycle) ++ self._pause(self.settle_time) ++ endtime = self.eventtime + self.sample_time ++ samples = [] ++ while self.eventtime < endtime: ++ _, speed, _ = self._wait_for_status() ++ samples.append(speed) ++ if not samples: ++ samples.append(self.controller.last_speed_mm_s) ++ avg_speed = sum(samples) / float(len(samples)) ++ self.gcmd.respond_info( ++ "drv8833 %s tune sample: duty=%.2f%% speed=%.3fmm/s" ++ % (self.lane.name, duty_cycle, avg_speed) ++ ) ++ return avg_speed ++ ++ def _find_baseline_duty(self): ++ max_speed = self._measure_speed(100.0) ++ if max_speed < self.target_speed: ++ raise self.gcmd.error( ++ "Target speed %.3fmm/s is not reachable; max observed %.3fmm/s" ++ % (self.target_speed, max_speed) ++ ) ++ low = 0.0 ++ high = 100.0 ++ baseline = self.controller.default_duty ++ for _ in range(AUTOTUNE_BASELINE_STEPS): ++ baseline = 0.5 * (low + high) ++ speed = self._measure_speed(baseline) ++ if speed < self.target_speed: ++ low = baseline ++ else: ++ high = baseline ++ return 0.5 * (low + high) ++ ++ def _check_peak(self, sample_time, speed, target): ++ peak_time, peak_speed, peak_kind = self.peaks[-1] if self.peaks else (0.0, target, None) ++ if not self.peaks: ++ peak_speed = target ++ peak_time = sample_time ++ peak_kind = None ++ if speed > target: ++ if peak_kind != "high" or speed > peak_speed: ++ peak_time, peak_speed, peak_kind = sample_time, speed, "high" ++ elif speed < target: ++ if peak_kind != "low" or speed < peak_speed: ++ peak_time, peak_speed, peak_kind = sample_time, speed, "low" ++ if not self.peaks or (peak_time, peak_speed, peak_kind) != self.peaks[-1]: ++ if self.peaks and self.peaks[-1][2] == peak_kind: ++ self.peaks[-1] = (peak_time, peak_speed, peak_kind) ++ elif peak_kind is not None: ++ self.peaks.append((peak_time, peak_speed, peak_kind)) ++ ++ def _collect_relay_response(self, low_duty, high_duty): ++ target_low = self.target_speed - self.band ++ target_high = self.target_speed + self.band ++ driving_high = True ++ self.lane._start_manual(self.eventtime, self.direction, high_duty) ++ self.switch_times = [(self.eventtime, high_duty)] ++ deadline = self.eventtime + self.timeout ++ while self.eventtime < deadline: ++ sample_time, speed, _ = self._wait_for_status() ++ self._check_peak(sample_time, speed, self.target_speed) ++ if driving_high and speed >= target_high: ++ driving_high = False ++ self.controller.manual_start(self.direction, low_duty) ++ self.switch_times.append((sample_time, low_duty)) ++ elif not driving_high and speed <= target_low: ++ driving_high = True ++ self.controller.manual_start(self.direction, high_duty) ++ self.switch_times.append((sample_time, high_duty)) ++ pairs = self._get_peak_pairs() ++ if len(pairs) >= AUTOTUNE_REQUIRED_PAIRS: ++ return pairs ++ raise self.gcmd.error("drv8833 PID tune timed out before converging") ++ ++ def _get_peak_pairs(self): ++ pairs = [] ++ last_low = None ++ for sample_time, speed, kind in self.peaks: ++ if kind == "low": ++ last_low = (sample_time, speed) ++ elif kind == "high" and last_low is not None: ++ pairs.append((last_low, (sample_time, speed))) ++ last_low = None ++ return pairs ++ ++ def _calc_pid(self, relay_duty, pairs): ++ pairs = pairs[-AUTOTUNE_REQUIRED_PAIRS:] ++ high_times = [high[0] for _, high in pairs] ++ if len(high_times) < 2: ++ raise self.gcmd.error("Not enough high peaks collected for PID tuning") ++ amplitudes = [0.5 * (high[1] - low[1]) for low, high in pairs] ++ amplitude = sum(amplitudes) / float(len(amplitudes)) ++ if amplitude <= 0.0: ++ raise self.gcmd.error("Measured relay oscillation amplitude is invalid") ++ periods = [ ++ high_times[index] - high_times[index - 1] ++ for index in range(1, len(high_times)) ++ ] ++ Tu = sum(periods) / float(len(periods)) ++ Ku = 4.0 * relay_duty / (math.pi * amplitude) ++ Kp = 0.6 * Ku ++ Ki = Kp / (0.5 * Tu) ++ Kd = Kp * (0.125 * Tu) ++ return Kp, Ki, Kd, Ku, Tu, amplitude ++ ++ def write_file_data(self, filename): ++ with open(filename, "w") as out: ++ out.write("time,speed,duty,target\n") ++ out.write( ++ "\n".join( ++ "%.5f,%.5f,%.5f,%.5f" % sample for sample in self.data ++ ) ++ ) ++ ++ def run(self): ++ baseline = self._find_baseline_duty() ++ relay_duty = self.gcmd.get_float( ++ "RELAY_DUTY", ++ _clamp( ++ baseline * AUTOTUNE_RELAY_RATIO, ++ AUTOTUNE_MIN_RELAY_DUTY, ++ AUTOTUNE_MAX_RELAY_DUTY, ++ ), ++ above=0.0, ++ maxval=50.0, ++ ) ++ low_duty = _clamp(baseline - relay_duty, 0.0, 100.0) ++ high_duty = _clamp(baseline + relay_duty, 0.0, 100.0) ++ relay_duty = 0.5 * (high_duty - low_duty) ++ if high_duty - low_duty < 1.0: ++ raise self.gcmd.error("Relay duty window is too small to tune") ++ self.gcmd.respond_info( ++ "drv8833 %s tune baseline=%.2f%% relay_low=%.2f%% relay_high=%.2f%% band=%.3fmm/s" ++ % (self.lane.name, baseline, low_duty, high_duty, self.band) ++ ) ++ pairs = self._collect_relay_response(low_duty, high_duty) ++ Kp, Ki, Kd, Ku, Tu, amplitude = self._calc_pid(relay_duty, pairs) ++ if self.write_file: ++ filename = os.path.join( ++ "/tmp", "drv8833_%s_pid_tune.csv" % (self.lane.name,) ++ ) ++ self.write_file_data(filename) ++ self.gcmd.respond_info("drv8833 tune data written to %s" % (filename,)) ++ return { ++ "pid_kp": Kp, ++ "pid_ki": Ki, ++ "pid_kd": Kd, ++ "ultimate_gain": Ku, ++ "ultimate_period": Tu, ++ "amplitude": amplitude, ++ "baseline_duty": baseline, ++ "relay_duty": relay_duty, ++ } ++ ++ ++class Drv8833CommandHelper: ++ cmd_MOVE_DEBUG_help = "Run a timed DRV8833 debug move" ++ cmd_DRV_SET_SPEED_help = "Set DRV8833 speed in mm/s" ++ cmd_DRV_MOVE_help = "Run a DRV8833 move for a target distance in mm" ++ cmd_DRV8833_PID_TUNE_help = "Run relay autotune for DRV8833 PID gains" ++ cmd_SET_DRV8833_PID_help = "Update DRV8833 PID gains" ++ ++ def __init__(self, printer): ++ self.printer = printer ++ self.gcode = printer.lookup_object("gcode") ++ self.gcode.register_command( ++ "MOVE_DEBUG", self.cmd_MOVE_DEBUG, desc=self.cmd_MOVE_DEBUG_help ++ ) ++ self.gcode.register_command( ++ "DRV_SET_SPEED", ++ self.cmd_DRV_SET_SPEED, ++ desc=self.cmd_DRV_SET_SPEED_help, ++ ) ++ self.gcode.register_command( ++ "DRV_MOVE", ++ self.cmd_DRV_MOVE, ++ desc=self.cmd_DRV_MOVE_help, ++ ) ++ self.gcode.register_command( ++ "DRV_PID_TUNE", ++ self.cmd_DRV8833_PID_TUNE, ++ desc=self.cmd_DRV8833_PID_TUNE_help, ++ ) ++ self.gcode.register_command( ++ "DRV8833_PID_TUNE", ++ self.cmd_DRV8833_PID_TUNE, ++ desc=self.cmd_DRV8833_PID_TUNE_help, ++ ) ++ self.gcode.register_command( ++ "DRV_SET_PID", ++ self.cmd_SET_DRV8833_PID, ++ desc=self.cmd_SET_DRV8833_PID_help, ++ ) ++ self.gcode.register_command( ++ "SET_DRV8833_PID", ++ self.cmd_SET_DRV8833_PID, ++ desc=self.cmd_SET_DRV8833_PID_help, ++ ) ++ ++ def _lookup_lane(self, gcmd): ++ lanes = dict(self.printer.lookup_objects("drv8833")) ++ if not lanes: ++ raise gcmd.error("No [drv8833] sections configured") ++ name = gcmd.get("NAME", None) ++ if name is not None: ++ lane = lanes.get("drv8833 %s" % (name,)) ++ if lane is None: ++ lane = lanes.get(name) ++ if lane is None: ++ raise gcmd.error("Unknown drv8833 NAME '%s'" % (name,)) ++ return lane ++ if len(lanes) == 1: ++ return list(lanes.values())[0] ++ names = [section.split(" ", 1)[-1] for section in sorted(lanes)] ++ raise gcmd.error( ++ "MOVE_DEBUG requires NAME=; available: %s" ++ % (", ".join(names),) ++ ) ++ ++ def cmd_MOVE_DEBUG(self, gcmd): ++ self._lookup_lane(gcmd).cmd_MOVE_DEBUG(gcmd) ++ ++ def cmd_DRV_SET_SPEED(self, gcmd): ++ self._lookup_lane(gcmd).cmd_DRV_SET_SPEED(gcmd) ++ ++ def cmd_DRV_MOVE(self, gcmd): ++ self._lookup_lane(gcmd).cmd_DRV_MOVE(gcmd) ++ ++ def cmd_DRV8833_PID_TUNE(self, gcmd): ++ self._lookup_lane(gcmd).cmd_DRV8833_PID_TUNE(gcmd) ++ ++ def cmd_SET_DRV8833_PID(self, gcmd): ++ self._lookup_lane(gcmd).cmd_SET_DRV8833_PID(gcmd) ++ ++ ++class PrinterDrv8833(Drv8833Interface): ++ def __init__(self, config): ++ self.printer = config.get_printer() ++ self.reactor = self.printer.get_reactor() ++ self.gcode = self.printer.lookup_object("gcode") ++ self.section_name = config.get_name() ++ section_parts = self.section_name.split(None, 1) ++ self.name = ( ++ section_parts[1] if len(section_parts) > 1 else self.section_name ++ ) ++ ppins = self.printer.lookup_object("pins") ++ motor_fwd_params = ppins.lookup_pin(config.get("motor_fwd")) ++ motor_rwd_params = ppins.lookup_pin(config.get("motor_rwd")) ++ hall_params = ppins.lookup_pin(config.get("motor_hall"), can_pullup=True) ++ self.mcu = motor_fwd_params["chip"] ++ if motor_rwd_params["chip"] is not self.mcu: ++ raise config.error( ++ "drv8833 motor_fwd and motor_rwd must be on the same mcu" ++ ) ++ if hall_params["chip"] is not self.mcu: ++ raise config.error( ++ "drv8833 motor_hall, motor_fwd, and motor_rwd must be on the same mcu" ++ ) ++ self.hall_controller = MCUDrv8833Controller( ++ config, motor_fwd_params, motor_rwd_params, hall_params ++ ) ++ self.hall_controller.status_callback = self._handle_hall_status ++ self.active = False ++ self.manual_mode = False ++ self.direction = 1 ++ self.direction_name = "forwards" ++ self.target_speed = 0.0 ++ self.debug_gcmd = None ++ self.debug_next_log_time = 0.0 ++ self.debug_hall_start = 0 ++ cmd_helper = self.printer.lookup_object("drv8833_command_helper", None) ++ if cmd_helper is None: ++ cmd_helper = Drv8833CommandHelper(self.printer) ++ self.printer.add_object("drv8833_command_helper", cmd_helper) ++ self.printer.register_event_handler( ++ "gcode:request_restart", self._handle_request_restart ++ ) ++ self.printer.register_event_handler("klippy:shutdown", self._handle_stop) ++ ++ def get_status(self, eventtime): ++ return { ++ "active": self.active, ++ "manual": self.manual_mode, ++ "direction": self.direction_name, ++ "target_speed": self.target_speed, ++ "duty_cycle": self.hall_controller.last_duty_cycle, ++ "hall_speed": self.hall_controller.last_speed_mm_s, ++ "pid_kp": self.hall_controller.pid_kp, ++ "pid_ki": self.hall_controller.pid_ki, ++ "pid_kd": self.hall_controller.pid_kd, ++ } ++ ++ def _handle_request_restart(self, print_time): ++ self._stop(self.reactor.monotonic()) ++ ++ def _handle_stop(self): ++ self._stop(self.reactor.monotonic()) ++ ++ def _handle_hall_status(self): ++ self.active = self.hall_controller.last_active ++ self.manual_mode = self.hall_controller.last_manual ++ if not self.active: ++ self.target_speed = 0.0 ++ if self.debug_gcmd is None: ++ return ++ eventtime = self.reactor.monotonic() ++ if eventtime < self.debug_next_log_time: ++ return ++ self.debug_next_log_time = eventtime + DEBUG_LOG_INTERVAL ++ msg = self._format_debug_message(eventtime) ++ gcmd = self.debug_gcmd ++ self.reactor.register_async_callback( ++ lambda eventtime, m=msg, g=gcmd: g.respond_info(m) ++ ) ++ ++ def _start(self, eventtime, direction, target_speed): ++ self._start_move(eventtime, direction, target_speed, 0) ++ ++ def _start_move(self, eventtime, direction, target_speed, stop_ticks): ++ if self.active: ++ raise self.printer.command_error( ++ "drv8833 %s is already running" % (self.name,) ++ ) ++ self.active = True ++ self.manual_mode = False ++ self.direction = direction ++ self.direction_name = "forwards" if direction > 0 else "backwards" ++ self.target_speed = target_speed ++ self.hall_controller.start_move(direction, target_speed, stop_ticks) ++ ++ def _start_manual(self, eventtime, direction, duty_cycle): ++ if not self.active: ++ self.active = True ++ self.manual_mode = True ++ self.direction = direction ++ self.direction_name = "forwards" if direction > 0 else "backwards" ++ self.target_speed = 0.0 ++ self.hall_controller.manual_start(direction, duty_cycle) ++ ++ def _stop(self, eventtime): ++ if not self.active: ++ return ++ self.active = False ++ self.manual_mode = False ++ self.target_speed = 0.0 ++ self.hall_controller.stop() ++ ++ def _save_pid_gains(self, kp, ki, kd): ++ configfile = self.printer.lookup_object("configfile") ++ configfile.set(self.section_name, "pid_kp", "%.6f" % (kp,)) ++ configfile.set(self.section_name, "pid_ki", "%.6f" % (ki,)) ++ configfile.set(self.section_name, "pid_kd", "%.6f" % (kd,)) ++ ++ def set_pid_gains(self, kp, ki, kd, save_to_config=False): ++ self.hall_controller.set_pid(kp, ki, kd) ++ if save_to_config: ++ self._save_pid_gains(kp, ki, kd) ++ ++ def _get_debug_totals(self): ++ hall_clicks = self.hall_controller.last_count - self.debug_hall_start ++ return { ++ "hall_clicks": hall_clicks, ++ "hall_mm": hall_clicks * self.hall_controller.hall_resolution, ++ } ++ ++ def _wait_for_move_complete(self, eventtime): ++ while self.active: ++ eventtime = self.reactor.pause(eventtime + MCU_REPORT_INTERVAL) ++ return eventtime ++ ++ def _format_debug_message(self, eventtime): ++ totals = self._get_debug_totals() ++ return ( ++ "drv8833 %s: direction=%s target=%.3fmm/s hall_clicks=%d " ++ "hall_speed=%.3fmm/s duty=%.1f%%" ++ % ( ++ self.name, ++ self.direction_name, ++ self.target_speed, ++ totals["hall_clicks"], ++ self.hall_controller.last_speed_mm_s, ++ self.hall_controller.last_duty_cycle, ++ ) ++ ) ++ ++ def drv8833_set_speed(self, speed: float): ++ eventtime = self.reactor.monotonic() ++ if speed == 0.0: ++ self._stop(eventtime) ++ return ++ direction = 1 if speed > 0.0 else -1 ++ target_speed = abs(speed) ++ if self.active: ++ self._stop(eventtime) ++ self._start(eventtime, direction, target_speed) ++ ++ def drv8833_move(self, speed: float, distance: float): ++ eventtime = self.reactor.monotonic() ++ if distance < 0.0: ++ speed *= -1.0 ++ distance = abs(distance) ++ if speed == 0.0 or distance == 0.0: ++ self._stop(eventtime) ++ return ++ direction = 1 if speed > 0.0 else -1 ++ target_speed = abs(speed) ++ stop_ticks = max( ++ 1, ++ int(round(distance / self.hall_controller.hall_resolution)), ++ ) ++ if self.active: ++ self._stop(eventtime) ++ self._start_move(eventtime, direction, target_speed, stop_ticks) ++ self._wait_for_move_complete(eventtime) ++ ++ cmd_MOVE_DEBUG_help = ( ++ "Run the DRV8833 lane for a fixed time while logging hall counts, " ++ "hall speed, and duty cycle" ++ ) ++ cmd_DRV_SET_SPEED_help = "Set the DRV8833 lane speed in mm/s" ++ cmd_DRV_MOVE_help = "Run the DRV8833 lane for a target distance in mm" ++ ++ def cmd_MOVE_DEBUG(self, gcmd): ++ duration = gcmd.get_float("TIME", 5.0, above=0.0) ++ direction = gcmd.get("DIRECTION", "forwards").strip().lower() ++ speed = gcmd.get_float("SPEED", 40.0, minval=0.0) ++ if direction not in ("forwards", "forward", "backwards", "backward"): ++ raise gcmd.error("DIRECTION must be forwards or backwards") ++ direction_value = 1 if direction.startswith("for") else -1 ++ eventtime = self.reactor.monotonic() ++ self.debug_gcmd = gcmd ++ self.debug_next_log_time = eventtime ++ # The MCU resets hall count to zero at the start of each move. ++ self.debug_hall_start = 0 ++ self._start(eventtime, direction_value, speed) ++ endtime = eventtime + duration ++ try: ++ while eventtime < endtime: ++ eventtime = self.reactor.pause(endtime) ++ finally: ++ self.debug_gcmd = None ++ self._stop(self.reactor.monotonic()) ++ totals = self._get_debug_totals() ++ gcmd.respond_info( ++ "drv8833 %s move complete: hall_clicks=%d hall_distance=%.3fmm" ++ % ( ++ self.name, ++ totals["hall_clicks"], ++ totals["hall_mm"], ++ ) ++ ) ++ ++ def cmd_DRV_SET_SPEED(self, gcmd): ++ speed = gcmd.get_float("SPEED") ++ self.drv8833_set_speed(speed) ++ gcmd.respond_info( ++ "drv8833 %s speed set to %.3fmm/s" % (self.name, speed) ++ ) ++ ++ def cmd_DRV_MOVE(self, gcmd): ++ speed = gcmd.get_float("SPEED") ++ distance = gcmd.get_float("DISTANCE") ++ self.drv8833_move(speed, distance) ++ totals = { ++ "hall_clicks": self.hall_controller.last_count, ++ "hall_mm": self.hall_controller.last_count ++ * self.hall_controller.hall_resolution, ++ } ++ gcmd.respond_info( ++ "drv8833 %s move complete: speed=%.3fmm/s distance=%.3fmm " ++ "hall_clicks=%d hall_distance=%.3fmm" ++ % ( ++ self.name, ++ speed, ++ distance, ++ totals["hall_clicks"], ++ totals["hall_mm"], ++ ) ++ ) ++ ++ def cmd_SET_DRV8833_PID(self, gcmd): ++ kp = gcmd.get_float("KP", self.hall_controller.pid_kp, minval=0.0) ++ ki = gcmd.get_float("KI", self.hall_controller.pid_ki, minval=0.0) ++ kd = gcmd.get_float("KD", self.hall_controller.pid_kd, minval=0.0) ++ save_to_config = bool(gcmd.get_int("SAVE", 0)) ++ self.set_pid_gains(kp, ki, kd, save_to_config=save_to_config) ++ msg = ( ++ "drv8833 %s PID updated: pid_kp=%.6f pid_ki=%.6f pid_kd=%.6f" ++ % (self.name, kp, ki, kd) ++ ) ++ if save_to_config: ++ msg += ( ++ "\nThe SAVE_CONFIG command will update the printer config file " ++ "and restart the printer." ++ ) ++ gcmd.respond_info(msg) ++ ++ def cmd_DRV8833_PID_TUNE(self, gcmd): ++ if self.active: ++ raise gcmd.error("drv8833 %s is already running" % (self.name,)) ++ tuner = Drv8833PIDAutoTune(self, gcmd) ++ try: ++ result = tuner.run() ++ finally: ++ self._stop(self.reactor.monotonic()) ++ self.set_pid_gains( ++ result["pid_kp"], ++ result["pid_ki"], ++ result["pid_kd"], ++ save_to_config=True, ++ ) ++ gcmd.respond_info( ++ "drv8833 %s PID tune at %.3fmm/s: pid_kp=%.6f pid_ki=%.6f pid_kd=%.6f\n" ++ "Ku=%.6f Tu=%.6fs amplitude=%.6fmm/s baseline_duty=%.2f%% relay_duty=%.2f%%\n" ++ "The SAVE_CONFIG command will update the printer config file and restart the printer." ++ % ( ++ self.name, ++ tuner.target_speed, ++ result["pid_kp"], ++ result["pid_ki"], ++ result["pid_kd"], ++ result["ultimate_gain"], ++ result["ultimate_period"], ++ result["amplitude"], ++ result["baseline_duty"], ++ result["relay_duty"], ++ ) ++ ) ++ ++ ++def load_config_prefix(config): ++ return PrinterDrv8833(config) +\ No newline at end of file +diff --git a/src/Kconfig b/src/Kconfig +index d679cef9..00da2118 100644 +--- a/src/Kconfig ++++ b/src/Kconfig +@@ -145,6 +145,10 @@ config WANT_PULSE_COUNTER + bool + depends on HAVE_GPIO + default y ++config WANT_DRV8833 ++ bool ++ depends on HAVE_GPIO && WANT_HARD_PWM ++ default y + config WANT_ST7920 + bool + depends on HAVE_GPIO +diff --git a/src/Makefile b/src/Makefile +index 3e7754af..19421fed 100644 +--- a/src/Makefile ++++ b/src/Makefile +@@ -13,6 +13,7 @@ src-$(CONFIG_WANT_BUTTONS) += buttons.c + src-$(CONFIG_WANT_TMCUART) += tmcuart.c + src-$(CONFIG_WANT_NEOPIXEL) += neopixel.c + src-$(CONFIG_WANT_PULSE_COUNTER) += pulse_counter.c ++src-$(CONFIG_WANT_DRV8833) += drv8833.c + src-$(CONFIG_WANT_ST7920) += lcd_st7920.c + src-$(CONFIG_WANT_HD44780) += lcd_hd44780.c + src-$(CONFIG_WANT_SOFTWARE_SPI) += spi_software.c +diff --git a/src/drv8833.c b/src/drv8833.c +new file mode 100644 +index 00000000..1101992c +--- /dev/null ++++ b/src/drv8833.c +@@ -0,0 +1,371 @@ ++// MCU-side DRV8833 hall-based speed controller ++// ++// Copyright (C) 2026 ++// ++// This file may be distributed under the terms of the GNU GPLv3 license. ++ ++#include "autoconf.h" // CONFIG_CLOCK_FREQ ++#include "basecmd.h" // oid_alloc ++#include "board/gpio.h" // struct gpio_in / struct gpio_pwm ++#include "board/irq.h" // irq_disable ++#include "board/misc.h" // timer_read_time / timer_is_before ++#include "command.h" // DECL_COMMAND / sendf ++#include "sched.h" // DECL_TASK / sched_add_timer ++ ++#define DUTY_SCALE 10 ++#define DUTY_MAX (100 * DUTY_SCALE) ++#define SPEED_SMOOTH_DIV 8 ++#define SPEED_SCALE 1000 ++#define PID_PARAM_SCALE 1000 ++ ++struct drv8833 { ++ struct timer timer; ++ struct gpio_in hall_pin; ++ struct gpio_pwm motor_fwd; ++ struct gpio_pwm motor_rwd; ++ uint32_t poll_ticks; ++ uint32_t control_ticks; ++ uint32_t report_ticks; ++ uint32_t next_control_time; ++ uint32_t next_report_time; ++ uint32_t count; ++ uint32_t stop_count; ++ uint32_t last_edge_time; ++ uint32_t last_interval_ticks; ++ uint32_t hall_resolution; ++ uint32_t speed_mm_s_x1000; ++ uint32_t target_speed_mm_s_x1000; ++ int64_t integral; ++ int32_t previous_error; ++ uint32_t pid_kp; ++ uint32_t pid_ki; ++ uint32_t pid_kd; ++ uint16_t duty_x10; ++ uint16_t default_duty_x10; ++ uint16_t max_duty_step_x10; ++ uint16_t pwm_max; ++ uint8_t flags; ++ uint8_t direction; ++ uint8_t last_hall_value; ++}; ++ ++enum { ++ DF_ACTIVE = 1 << 0, ++ DF_PENDING = 1 << 1, ++ DF_MANUAL = 1 << 2, ++}; ++ ++static struct task_wake drv8833_wake; ++ ++static uint32_t ++drv8833_calc_speed(const struct drv8833 *d, uint32_t delta_count, ++ uint32_t delta_ticks) ++{ ++ if (!delta_count || !delta_ticks) ++ return 0; ++ uint64_t numer = ++ (uint64_t)delta_count * d->hall_resolution * CONFIG_CLOCK_FREQ; ++ return numer / ((uint64_t)delta_ticks * 1000); ++} ++ ++static uint32_t ++drv8833_calc_measured_speed(const struct drv8833 *d, uint32_t time) ++{ ++ if (!d->last_edge_time) ++ return 0; ++ uint32_t age_ticks = time - d->last_edge_time; ++ uint32_t measured_interval = d->last_interval_ticks ++ ? d->last_interval_ticks : age_ticks; ++ if (age_ticks > measured_interval) ++ measured_interval = age_ticks; ++ return drv8833_calc_speed(d, 1, measured_interval); ++} ++ ++static uint32_t ++drv8833_update_speed(struct drv8833 *d, uint32_t time) ++{ ++ uint32_t measured_speed = drv8833_calc_measured_speed(d, time); ++ if (!d->speed_mm_s_x1000) ++ d->speed_mm_s_x1000 = measured_speed; ++ else ++ d->speed_mm_s_x1000 += ++ ((int32_t)measured_speed - (int32_t)d->speed_mm_s_x1000) ++ / SPEED_SMOOTH_DIV; ++ return d->speed_mm_s_x1000; ++} ++ ++static int64_t ++drv8833_integral_limit(const struct drv8833 *d) ++{ ++ if (!d->pid_ki) ++ return 0x7fffffffffffffffLL; ++ return ((int64_t)DUTY_MAX * PID_PARAM_SCALE * SPEED_SCALE ++ * CONFIG_CLOCK_FREQ) / d->pid_ki; ++} ++ ++static void ++drv8833_apply_pwm(struct drv8833 *d) ++{ ++ uint32_t pwm_value = 0; ++ if ((d->flags & DF_ACTIVE) && d->duty_x10) { ++ pwm_value = ((uint32_t)d->duty_x10 * d->pwm_max + DUTY_MAX / 2) ++ / DUTY_MAX; ++ } ++ if (d->direction) { ++ gpio_pwm_write(d->motor_fwd, pwm_value); ++ gpio_pwm_write(d->motor_rwd, 0); ++ } else { ++ gpio_pwm_write(d->motor_fwd, 0); ++ gpio_pwm_write(d->motor_rwd, pwm_value); ++ } ++} ++ ++static void ++drv8833_control_update(struct drv8833 *d, uint32_t time) ++{ ++ if (!(d->flags & DF_ACTIVE) || (d->flags & DF_MANUAL)) ++ return; ++ ++ if (!d->target_speed_mm_s_x1000) ++ return; ++ ++ int32_t error = (int32_t)d->target_speed_mm_s_x1000 ++ - (int32_t)d->speed_mm_s_x1000; ++ int32_t derivative = error - d->previous_error; ++ int64_t next_integral = d->integral + (int64_t)error * d->control_ticks; ++ int64_t integral_limit = drv8833_integral_limit(d); ++ if (next_integral < -integral_limit) ++ next_integral = -integral_limit; ++ else if (next_integral > integral_limit) ++ next_integral = integral_limit; ++ ++ int32_t p_term = (int64_t)d->pid_kp * error ++ / (PID_PARAM_SCALE * SPEED_SCALE); ++ int32_t i_term = (int64_t)d->pid_ki * next_integral ++ / ((int64_t)PID_PARAM_SCALE * SPEED_SCALE ++ * CONFIG_CLOCK_FREQ); ++ int32_t d_term = 0; ++ if (d->control_ticks) { ++ d_term = (int64_t)d->pid_kd * derivative * CONFIG_CLOCK_FREQ ++ / ((int64_t)PID_PARAM_SCALE * SPEED_SCALE ++ * d->control_ticks); ++ } ++ ++ int32_t target_duty = d->default_duty_x10 + p_term + i_term + d_term; ++ if (target_duty < 0) ++ target_duty = 0; ++ else if (target_duty > DUTY_MAX) ++ target_duty = DUTY_MAX; ++ ++ if (!((target_duty == 0 && error < 0) ++ || (target_duty == DUTY_MAX && error > 0))) ++ d->integral = next_integral; ++ ++ int32_t delta_duty = target_duty - (int32_t)d->duty_x10; ++ if (delta_duty < -(int32_t)d->max_duty_step_x10) ++ delta_duty = -(int32_t)d->max_duty_step_x10; ++ else if (delta_duty > (int32_t)d->max_duty_step_x10) ++ delta_duty = d->max_duty_step_x10; ++ ++ d->duty_x10 += delta_duty; ++ d->previous_error = error; ++ drv8833_apply_pwm(d); ++} ++ ++static void ++drv8833_reset_state(struct drv8833 *d) ++{ ++ d->count = 0; ++ d->stop_count = 0; ++ d->last_edge_time = 0; ++ d->last_interval_ticks = 0; ++ d->speed_mm_s_x1000 = 0; ++ d->integral = 0; ++ d->previous_error = 0; ++} ++ ++static uint_fast8_t ++drv8833_event(struct timer *timer) ++{ ++ struct drv8833 *d = container_of(timer, struct drv8833, timer); ++ uint32_t time = d->timer.waketime; ++ ++ uint8_t hall_value = gpio_in_read(d->hall_pin); ++ if (hall_value != d->last_hall_value) { ++ d->count++; ++ if (d->last_edge_time) ++ d->last_interval_ticks = time - d->last_edge_time; ++ d->last_edge_time = time; ++ d->last_hall_value = hall_value; ++ if ((d->flags & DF_ACTIVE) && d->stop_count && d->count >= d->stop_count) { ++ d->flags &= ~(DF_ACTIVE | DF_MANUAL); ++ d->target_speed_mm_s_x1000 = 0; ++ d->stop_count = 0; ++ d->integral = 0; ++ d->previous_error = 0; ++ d->duty_x10 = 0; ++ drv8833_apply_pwm(d); ++ d->flags |= DF_PENDING; ++ sched_wake_task(&drv8833_wake); ++ } ++ } ++ ++ if (!timer_is_before(time, d->next_control_time)) { ++ d->next_control_time = time + d->control_ticks; ++ drv8833_update_speed(d, time); ++ drv8833_control_update(d, time); ++ } ++ if ((d->flags & DF_ACTIVE) && !timer_is_before(time, d->next_report_time)) { ++ d->next_report_time = time + d->report_ticks; ++ d->flags |= DF_PENDING; ++ sched_wake_task(&drv8833_wake); ++ } ++ ++ d->timer.waketime += d->poll_ticks; ++ return SF_RESCHEDULE; ++} ++ ++void ++command_config_drv8833(uint32_t *args) ++{ ++ struct drv8833 *d = oid_alloc(args[0], command_config_drv8833, ++ sizeof(*d)); ++ d->motor_fwd = gpio_pwm_setup(args[1], args[3], 0); ++ d->motor_rwd = gpio_pwm_setup(args[2], args[3], 0); ++ d->hall_pin = gpio_in_setup(args[4], args[5]); ++ d->poll_ticks = args[6]; ++ d->control_ticks = args[7]; ++ d->report_ticks = args[8]; ++ d->hall_resolution = args[9]; ++ d->pwm_max = args[10]; ++ d->default_duty_x10 = args[11]; ++ d->max_duty_step_x10 = args[12]; ++ d->pid_kp = 0; ++ d->pid_ki = 0; ++ d->pid_kd = 0; ++ d->last_hall_value = gpio_in_read(d->hall_pin); ++ d->direction = 1; ++ d->timer.func = drv8833_event; ++} ++DECL_COMMAND(command_config_drv8833, ++ "config_drv8833 oid=%c motor_fwd_pin=%u motor_rwd_pin=%u" ++ " cycle_ticks=%u hall_pin=%u hall_pullup=%c poll_ticks=%u" ++ " control_ticks=%u report_ticks=%u hall_resolution=%u" ++ " pwm_max=%hu default_duty=%hu max_duty_step=%hu"); ++ ++void ++command_drv8833_set(uint32_t *args) ++{ ++ struct drv8833 *d = oid_lookup(args[0], command_config_drv8833); ++ sched_del_timer(&d->timer); ++ d->flags &= ~DF_PENDING; ++ ++ if (!args[1]) { ++ d->flags &= ~(DF_ACTIVE | DF_MANUAL); ++ d->target_speed_mm_s_x1000 = 0; ++ drv8833_reset_state(d); ++ d->duty_x10 = 0; ++ drv8833_apply_pwm(d); ++ return; ++ } ++ ++ uint32_t now = timer_read_time(); ++ d->flags = (d->flags & ~DF_MANUAL) | DF_ACTIVE | DF_PENDING; ++ d->direction = !!args[2]; ++ d->target_speed_mm_s_x1000 = args[3]; ++ drv8833_reset_state(d); ++ d->stop_count = args[4]; ++ d->duty_x10 = d->default_duty_x10; ++ d->last_hall_value = gpio_in_read(d->hall_pin); ++ d->next_control_time = now; ++ d->next_report_time = now; ++ drv8833_apply_pwm(d); ++ d->timer.waketime = now + d->poll_ticks; ++ sched_add_timer(&d->timer); ++ sched_wake_task(&drv8833_wake); ++} ++DECL_COMMAND(command_drv8833_set, ++ "drv8833_set oid=%c enable=%c direction=%c target_speed=%u" ++ " stop_ticks=%u"); ++ ++void ++command_drv8833_manual(uint32_t *args) ++{ ++ struct drv8833 *d = oid_lookup(args[0], command_config_drv8833); ++ ++ if (args[1] && (d->flags & DF_ACTIVE) && (d->flags & DF_MANUAL)) { ++ d->direction = !!args[2]; ++ d->duty_x10 = args[3] > DUTY_MAX ? DUTY_MAX : args[3]; ++ drv8833_apply_pwm(d); ++ return; ++ } ++ ++ sched_del_timer(&d->timer); ++ d->flags &= ~DF_PENDING; ++ ++ if (!args[1]) { ++ d->flags &= ~(DF_ACTIVE | DF_MANUAL); ++ d->target_speed_mm_s_x1000 = 0; ++ drv8833_reset_state(d); ++ d->duty_x10 = 0; ++ drv8833_apply_pwm(d); ++ return; ++ } ++ ++ uint32_t now = timer_read_time(); ++ d->flags |= DF_ACTIVE | DF_PENDING | DF_MANUAL; ++ d->direction = !!args[2]; ++ d->target_speed_mm_s_x1000 = 0; ++ drv8833_reset_state(d); ++ d->duty_x10 = args[3] > DUTY_MAX ? DUTY_MAX : args[3]; ++ d->last_hall_value = gpio_in_read(d->hall_pin); ++ d->next_control_time = now; ++ d->next_report_time = now; ++ drv8833_apply_pwm(d); ++ d->timer.waketime = now + d->poll_ticks; ++ sched_add_timer(&d->timer); ++ sched_wake_task(&drv8833_wake); ++} ++DECL_COMMAND(command_drv8833_manual, ++ "drv8833_manual oid=%c enable=%c direction=%c duty=%hu"); ++ ++void ++command_drv8833_set_pid(uint32_t *args) ++{ ++ struct drv8833 *d = oid_lookup(args[0], command_config_drv8833); ++ irq_disable(); ++ d->pid_kp = args[1]; ++ d->pid_ki = args[2]; ++ d->pid_kd = args[3]; ++ d->integral = 0; ++ d->previous_error = 0; ++ irq_enable(); ++} ++DECL_COMMAND(command_drv8833_set_pid, ++ "drv8833_set_pid oid=%c kp=%u ki=%u kd=%u"); ++ ++void ++drv8833_task(void) ++{ ++ if (!sched_check_wake(&drv8833_wake)) ++ return; ++ ++ uint8_t oid; ++ struct drv8833 *d; ++ foreach_oid(oid, d, command_config_drv8833) { ++ irq_disable(); ++ uint8_t flags = d->flags; ++ uint32_t count = d->count; ++ uint32_t speed = d->speed_mm_s_x1000; ++ uint16_t duty = d->duty_x10; ++ d->flags = flags & ~DF_PENDING; ++ irq_enable(); ++ if (!(flags & DF_PENDING)) ++ continue; ++ sendf("drv8833_status oid=%c active=%c manual=%c count=%u speed=%u" ++ " duty=%hu", ++ oid, !!(flags & DF_ACTIVE), !!(flags & DF_MANUAL), ++ count, speed, duty); ++ } ++} ++DECL_TASK(drv8833_task); +\ No newline at end of file diff --git a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc index 674ceef0..808267da 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc +++ b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc @@ -8,6 +8,7 @@ SRC_URI = "git://github.com/OpenCentauri/kalico.git;protocol=https;branch=rpmsg- file://0001-remove-save-config-subfile-check.patch \ file://0002-reduce-calibration-difference-tolerance.patch \ file://0001-Reduce-log-rotate-threshold.patch \ + file://0003-drv8833-motor-control.patch \ " SRCREV = "afe7178d0859f3cbc80e591473f86ee64183122b" diff --git a/meta-opencentauri/recipes-apps/mcu-flasher/mcu-flasher_0.1.0.bb b/meta-opencentauri/recipes-apps/mcu-flasher/mcu-flasher_0.1.0.bb index 8c0d46ce..64fb7437 100644 --- a/meta-opencentauri/recipes-apps/mcu-flasher/mcu-flasher_0.1.0.bb +++ b/meta-opencentauri/recipes-apps/mcu-flasher/mcu-flasher_0.1.0.bb @@ -8,7 +8,7 @@ LIC_FILES_CHKSUM = "file://../LICENSE;md5=0a18a528575a965515cdd877f88b3c4c" SRC_URI += " \ git://github.com/OpenCentauri/OpenCentauri.git;protocol=https;nobranch=1;branch=main \ " -SRCREV = "fb829bd0a469e1c54cb9d2aa1bd98e14afd9f351" +SRCREV = "4b179ce5fa70309f24ac047b6d9e2c5c2a0e1c7c" PR = "r1" S = "${WORKDIR}/git/mcu-flasher" diff --git a/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb b/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb new file mode 100644 index 00000000..a16db6da --- /dev/null +++ b/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb @@ -0,0 +1,44 @@ +HOMEPAGE = "https://github.com/suchmememanyskill/AFC-Klipper-Add-On" +LICENSE = "GPL-3.0-only" +LIC_FILES_CHKSUM = "file://LICENSE;md5=db95b6e40dc7d26d8308b6b7375637b6" +SUMMARY = "AFC-Klipper-Add-On" +DESCRIPTION = "Automated Filament Changer Software." + +FILESEXTRAPATHS:prepend := "${THISDIR}/files:" + +SRC_URI = " \ + git://github.com/suchmememanyskill/AFC-Klipper-Add-On.git;protocol=https;branch=DEV \ + file://afc.cfg \ + file://canvas.cfg \ +" + +SRCREV = "9b5291954d27fdcfb5913a79c72e15884a9935b7" + +S = "${WORKDIR}/git" + +DEPENDS = " \ + python3-native \ +" + +RDEPENDS:${PN} = " \ + klipper \ +" + +do_configure[noexec] = "1" +do_compile[noexec] = "1" + +do_install() { + # Install klippy extras + install -d ${D}${datadir}/klipper/extras + cp -r ${S}/extras/* ${D}${datadir}/klipper/extras/ + + # Install config files + install -d ${D}${sysconfdir}/klipper/config/extras-readonly + install -m 0644 ${WORKDIR}/canvas.cfg ${WORKDIR}/afc.cfg ${D}${sysconfdir}/klipper/config/extras-readonly +} + +FILES:${PN} = " \ + ${datadir}/klipper/AFC* \ + ${sysconfdir}/klipper/config/extras-readonly/afc.cfg \ + ${sysconfdir}/klipper/config/extras-readonly/canvas.cfg \ +" \ No newline at end of file diff --git a/meta-opencentauri/recipes-data/klipper-afc-addon/files/afc.cfg b/meta-opencentauri/recipes-data/klipper-afc-addon/files/afc.cfg new file mode 100644 index 00000000..a5044a72 --- /dev/null +++ b/meta-opencentauri/recipes-data/klipper-afc-addon/files/afc.cfg @@ -0,0 +1,110 @@ +# Eject Loaded Lane +[gcode_macro BT_TOOL_UNLOAD] +description: Unloads the currently loaded lane +gcode: + TOOL_UNLOAD + +[gcode_macro BT_CHANGE_TOOL] +description: Switch to a new lane by ejecting the previously loaded one and then load the lane specified by LANE parameter. Lane parameter is an integer and defaults to 1. ex. `BT_CHANGE_TOOL LANE=2` +gcode: + {% set lane_num = params.LANE|default(1)|int %} + {% set stepper = printer['gcode_macro _AFC_GLOBAL_VARS'].stepper_name|string %} + + CHANGE_TOOL LANE={stepper ~ lane_num} + +[gcode_macro BT_LANE_EJECT] +description: Fully eject the filament from the lane so spool can be removed. Lane parameter is an integer and defaults to 1. ex. `BT_LANE_EJECT LANE=2` +gcode: + {% set lane_num = params.LANE|default(1)|int %} + {% set stepper = printer['gcode_macro _AFC_GLOBAL_VARS'].stepper_name|string %} + + LANE_UNLOAD LANE={stepper ~ lane_num} + +[gcode_macro BT_LANE_MOVE] +description: Move the specified lane the specified amount. Lane parameter is an integer and defaults to 1. Distance parameter is and integer and defaults to 20. Distance over 200 uses long load speeds. ex `BT_LANE_MOVE LANE=2 DISTANCE=100` +gcode: + {% set lane_num = params.LANE|default(1)|int %} + {% set dist_str = params.DISTANCE|default('20')|string %} + {% set stepper = printer['gcode_macro _AFC_GLOBAL_VARS'].stepper_name|string %} + + {% set valid_chars = '0123456789' %} + {% set is_valid = true %} + + {% if dist_str|length > 0 %} + {% if dist_str[-1] not in valid_chars %} + {% set is_valid = false %} + {% endif %} + {% else %} + {% set is_valid = false %} + {% endif %} + + {% if is_valid %} + {% set dist = dist_str|float %} + LANE_MOVE LANE={stepper ~ lane_num} DISTANCE={dist} + {% else %} + { action_respond_info("Error: Invalid DISTANCE parameter: " ~ dist_str) } + {% endif %} + + +[gcode_macro BT_RESUME] +description: Resume the print after an error, using normal resume will also call AFC_RESUME +gcode: + {% if not printer.pause_resume.is_paused %} + RESPOND MSG="Print is not paused. Resume ignored" + {% else %} + AFC_RESUME + {% endif %} + +[gcode_macro BT_PREP] +description: Run the AFC PREP sequence +gcode: + PREP + +[gcode_macro AFC_DISABLE_SKEW] +description: Disable skew correction temporarily +gcode: + {% set gVars = printer['gcode_macro _AFC_GLOBAL_VARS'] %} + {% set verbose = gVars['verbose']|int %} + {% set disable_skew = gVars['disable_skew_correction']|default(false)|lower == 'true' %} + + # Disable skew correction if defined + {% if disable_skew %} + SET_GCODE_VARIABLE MACRO=AFC_ENABLE_SKEW VARIABLE=skew_profile VALUE='"{printer.skew_correction.current_profile_name}"' + {% if verbose > 1 %} + RESPOND TYPE=command MSG='AFC: Disabling skew correction' + {% endif %} + SET_SKEW CLEAR=1 + {% if verbose > 1 %} + GET_CURRENT_SKEW + {% endif %} + {% endif %} + +[gcode_macro AFC_ENABLE_SKEW] +description: Re-enable skew correction if it was previously disabled +variable_skew_profile: "my_skew_profile" # Widely used as documented in Klipper docs. +gcode: + {% set gVars = printer['gcode_macro _AFC_GLOBAL_VARS'] %} + {% set verbose = gVars['verbose']|int %} + {% set disable_skew = gVars['disable_skew_correction']|default(false)|lower == 'true' %} + + # Restore skew correction if defined + {% if disable_skew %} + {% if verbose > 1 %} + { action_respond_info("AFC: Restoring skew correction profile " ~ skew_profile) } + {% endif %} + SKEW_PROFILE LOAD={skew_profile} + {% if verbose > 1 %} + GET_CURRENT_SKEW + {% endif %} + {% endif %} + +[AFC_prep] +enable: True +delay_time: 0.5 +disable_unload_filament_remapping: False +capture_td1_data: False + +[delayed_gcode welcome_afc] +initial_duration: 1 +gcode: + PREP \ No newline at end of file diff --git a/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg b/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg new file mode 100644 index 00000000..4e8a8bbf --- /dev/null +++ b/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg @@ -0,0 +1,231 @@ +[include afc.cfg] + +[mcu canvas] +serial: /dev/serial/by-path/platform-4200000.usb-usb-0:1.4:1.0 +restart_method: command + +[drv8833 canvas_lane0] +motor_fwd: canvas:PA6 +motor_rwd: canvas:PA5 +motor_cycle_time: 0.0001 +motor_hall: canvas:PB6 +motor_hall_resolution: 0.27335 + +[drv8833 canvas_lane1] +motor_fwd: canvas:PA7 +motor_rwd: canvas:PB0 +motor_cycle_time: 0.0001 +motor_hall: canvas:PB7 +motor_hall_resolution: 0.27335 + +[drv8833 canvas_lane2] +motor_fwd: canvas:PB8 +motor_rwd: canvas:PB9 +motor_cycle_time: 0.0001 +motor_hall: canvas:PB5 +motor_hall_resolution: 0.27335 + +[drv8833 canvas_lane3] +motor_fwd: canvas:PB1 +motor_rwd: canvas:PB10 +motor_cycle_time: 0.0001 +motor_hall: canvas:PB4 +motor_hall_resolution: 0.27335 + +[board_pins canvas] +mcu: canvas +aliases: + ENABLE_9V=PC2 , ENABLE_24V=PC3, + + MOTOR0_FWD=PA6 , MOTOR0_RWD=PA5, + MOTOR1_FWD=PB0 , MOTOR1_RWD=PA7, + MOTOR2_FWD=PB8 , MOTOR2_RWD=PB9, + MOTOR3_FWD=PB10 , MOTOR3_RWD=PB1, + + HALL0=PB6 , HALL1=PB7, + HALL2=PB5 , HALL3=PB4, + + LED0_WHITE=PC9 , LED0_RED=PC8, + LED1_WHITE=PC6 , LED1_RED=PB15, + LED2_WHITE=PC10 , LED2_RED=PA15, + LED3_WHITE=PC1 , LED3_RED=PC12, + + FILAMENT0=PA8 , FILAMENT1=PC7, + FILAMENT2=PC11 , FILAMENT3=PC0, + + ODOMETER0=PA2 , ODOMETER1=PA3, + ODOMETER2=PA1 , ODOMETER3=PA0 + +[AFC_hub toolhead_4way_hub] +switch_pin: !hotend:PB2 +enable_hub_runout: False + +[AFC_extruder extruder] +pin_tool_start: !hotend:PB2 +tool_stn: 94 +tool_stn_unload: 31 +tool_sensor_after_extruder: 0 +tool_unload_speed: 15 +tool_load_speed: 8 + +[AFC_canvas CANVAS_1] +extruder: extruder +enable_9v_pin: canvas:ENABLE_9V +enable_24v_pin: canvas:ENABLE_24V +tangle_pin: !hotend:PB0 +short_moves_speed: 30 +long_moves_speed: 70 +tool_unload_lane_extra_speed: 40 # Unused +prep_distance: 120 +prep_speed: 60 +tool_load_sync_speed_offset: 2 +tool_load_lane_extra_distance: 6.5 +tool_unload_lane_extra_distance: 40 # Unused + +[AFC_canvas_lane canvas_lane0] +unit: CANVAS_1:1 +hub: toolhead_4way_hub +map: T0 +dist_hub: 1200 +prep: !canvas:FILAMENT0 +drv8833: canvas_lane0 +odometer_pin: canvas:ODOMETER0 +odometer_resolution: 3.15786 +led_white_pin: canvas:LED0_WHITE +led_red_pin: canvas:LED0_RED + +[AFC_canvas_lane canvas_lane1] +unit: CANVAS_1:2 +hub: toolhead_4way_hub +map: T1 +dist_hub: 1200 +prep: !canvas:FILAMENT1 +drv8833: canvas_lane1 +odometer_pin: canvas:ODOMETER1 +odometer_resolution: 3.15786 +led_white_pin: canvas:LED1_WHITE +led_red_pin: canvas:LED1_RED + +[AFC_canvas_lane canvas_lane2] +unit: CANVAS_1:3 +hub: toolhead_4way_hub +map: T2 +dist_hub: 1200 +prep: !canvas:FILAMENT2 +drv8833: canvas_lane2 +odometer_pin: canvas:ODOMETER2 +odometer_resolution: 3.15786 +led_white_pin: canvas:LED2_WHITE +led_red_pin: canvas:LED2_RED + +[AFC_canvas_lane canvas_lane3] +unit: CANVAS_1:4 +hub: toolhead_4way_hub +map: T3 +dist_hub: 1200 +prep: !canvas:FILAMENT3 +drv8833: canvas_lane3 +odometer_pin: canvas:ODOMETER3 +odometer_resolution: 3.15786 +led_white_pin: canvas:LED3_WHITE +led_red_pin: canvas:LED3_RED + +[gcode_macro CUT] +gcode: + {% if "xyz" not in printer.toolhead.homed_axes %} + G28 ; Home if necessary + {% endif %} + + G90 ; Absolute positioning + G0 Z14.5 F600 ; Ensure we have some Z clearance + G0 Y30 F15000 ; Get some clearance for Y + G0 X255 F8000 ; Move to cutting position + G0 Y3 F1200 ; Cut + G0 Y30 F8000 ; Retract from cutting + M400 ; Wait for movements to complete + +[gcode_macro POOP] +gcode: + {% if params.PURGE_LENGTH %} + M83 + G1 E{params.PURGE_LENGTH} F600 + {% endif %} + +# Wipe Nozzle, from new firmware printer.cfg +[gcode_macro M729] +gcode: + SAVE_GCODE_STATE NAME=m729_saved_state + + G90 ; Absolute positioning + + ; === Approach === + G1 X202 F6000 ; Move to wiper X start position + G1 Y264.5 F6000 ; Move to wiper Y position (top of wiper) + + ; === Pass 1: X sweeps at top of wiper === + G1 X165 F8000 ; Wipe left (165 vs 170 in old — 5mm wider sweep) + G1 X190 ; Wipe right + G1 X165 ; Wipe left + + ; === Full depth plunge === + G1 Y245 F6000 ; Drop to full depth (19.5mm below top — 5mm deeper than old) + + ; === Full width sweep at depth === + G1 X202 F7200 ; Full width sweep right across entire wiper + + ; === Extract === + G1 Y264.5 F1200 ; Slow lift up through wiper (clean exit) + + M400 ; Wait for all moves to complete + + RESTORE_GCODE_STATE NAME=m729_saved_state + +[AFC] +VarFile: /etc/klipper/config/AFC.var +enable_sensors_in_gui: True +ignore_spoolman_material_temps: False +load_to_hub: False +moonraker_port: 80 +debug: True +z_hop: 5 +resume_speed: 120 +resume_z_speed: 25 +led_fault: 1,0,0,0 # Fault color +led_ready: 0,0,0,1 # Ready color +led_not_ready: 1,0,0,0 # Not ready color +led_loading: 1,0,0,1 # Loading color +led_tool_loaded: 1,0,0,1 # Loaded color +led_buffer_advancing: 0,0,1,0 # Buffer advancing color +led_buffer_trailing: 0,1,0,0 # Buffer trailing color +led_buffer_disable: 0,0,0,0.25 # Buffer disable color +led_spool_illuminate: 1,1,1,0 # Loading color to illuminate spool, currently only for QuattroBox units and can be overridden in AFC_QuattroBox section +unload_on_runout: False +auto_home: True +enable_assist: False +spool_ratio: 2 +enable_hub_runout: False +enable_tool_runout: True +homing_enabled: False + +# TOOL Cutting Settings +tool_cut: True +tool_cut_cmd: CUT +tool_cut_threshold: 10000 + +# Park Settings +park: True +park_cmd: MOVE_TO_TRAY + +# Poop Settings +poop: True +poop_cmd: POOP + +# Kick Settings +kick: False + +# Wipe Settings +wipe: True +wipe_cmd: M729 + +# Form Tip Settings +form_tip: False \ No newline at end of file From 4245eb071f8296cacf4b68cfa0ea83904236c457 Mon Sep 17 00:00:00 2001 From: suchmememanyskill <38142618+suchmememanyskill@users.noreply.github.com> Date: Tue, 5 May 2026 23:55:48 +0200 Subject: [PATCH 02/19] Forgor to actually include AFC in the build --- meta-opencentauri/images/opencentauri-image-base.bb | 1 + 1 file changed, 1 insertion(+) diff --git a/meta-opencentauri/images/opencentauri-image-base.bb b/meta-opencentauri/images/opencentauri-image-base.bb index 9f111c98..f17d972f 100644 --- a/meta-opencentauri/images/opencentauri-image-base.bb +++ b/meta-opencentauri/images/opencentauri-image-base.bb @@ -43,6 +43,7 @@ CORE_IMAGE_EXTRA_INSTALL += "\ v4l-utils \ iproute2 \ chrony \ + afc \ " INITRAMFS_IMAGE = "core-image-tiny-initramfs" From b0ba5d636125094dfb70250dc328e4b1683301b6 Mon Sep 17 00:00:00 2001 From: suchmememanyskill <38142618+suchmememanyskill@users.noreply.github.com> Date: Wed, 6 May 2026 00:15:56 +0200 Subject: [PATCH 03/19] Build CANVAS firmware, fix build issues in afc bb --- .../canvas-bootloader-upgrade_1.0.0.bb | 21 +++ ...> 0002-add-support-for-48kib-offset.patch} | 0 .../0003-add-support-for-64kib-offset.patch | 21 +++ .../recipes-apps/katapult/files/config.canvas | 79 +++++++++ .../recipes-apps/katapult/katapult_1.0.0.inc | 5 +- .../recipes-apps/klipper/files/config.canvas | 159 ++++++++++++++++++ .../kalico-firmware-canvas_2026.02.00.bb | 35 ++++ .../recipes-apps/klipper/kalico_2026.02.00.bb | 1 + .../recipes-data/klipper-afc-addon/afc_0.1.bb | 31 +++- 9 files changed, 349 insertions(+), 3 deletions(-) create mode 100644 meta-opencentauri/recipes-apps/katapult/canvas-bootloader-upgrade_1.0.0.bb rename meta-opencentauri/recipes-apps/katapult/files/{01-add-support-for-48kib-offset.patch => 0002-add-support-for-48kib-offset.patch} (100%) create mode 100644 meta-opencentauri/recipes-apps/katapult/files/0003-add-support-for-64kib-offset.patch create mode 100644 meta-opencentauri/recipes-apps/katapult/files/config.canvas create mode 100644 meta-opencentauri/recipes-apps/klipper/files/config.canvas create mode 100644 meta-opencentauri/recipes-apps/klipper/kalico-firmware-canvas_2026.02.00.bb diff --git a/meta-opencentauri/recipes-apps/katapult/canvas-bootloader-upgrade_1.0.0.bb b/meta-opencentauri/recipes-apps/katapult/canvas-bootloader-upgrade_1.0.0.bb new file mode 100644 index 00000000..fdd8d5be --- /dev/null +++ b/meta-opencentauri/recipes-apps/katapult/canvas-bootloader-upgrade_1.0.0.bb @@ -0,0 +1,21 @@ +require katapult_${PV}.inc + +SUMMARY = "Katapult Canvas Bootloader Deployer" +DESCRIPTION = "Builds the Katapult deployer binary for upgrading the canvas bootloader." + +SRC_URI += " \ + file://config.canvas \ +" + +DEPENDS += "gcc-arm-none-eabi-native" + +EXTRA_OEMAKE += "KCONFIG_CONFIG=../config.canvas" + +do_install() { + install -d ${D}/lib/firmware + install -m 0644 ${S}/out/deployer.bin ${D}/lib/firmware/katapult-deployer-canvas.bin +} + +FILES:${PN} = " \ + /lib/firmware/katapult-deployer-canvas.bin \ +" diff --git a/meta-opencentauri/recipes-apps/katapult/files/01-add-support-for-48kib-offset.patch b/meta-opencentauri/recipes-apps/katapult/files/0002-add-support-for-48kib-offset.patch similarity index 100% rename from meta-opencentauri/recipes-apps/katapult/files/01-add-support-for-48kib-offset.patch rename to meta-opencentauri/recipes-apps/katapult/files/0002-add-support-for-48kib-offset.patch diff --git a/meta-opencentauri/recipes-apps/katapult/files/0003-add-support-for-64kib-offset.patch b/meta-opencentauri/recipes-apps/katapult/files/0003-add-support-for-64kib-offset.patch new file mode 100644 index 00000000..b3304ee3 --- /dev/null +++ b/meta-opencentauri/recipes-apps/katapult/files/0003-add-support-for-64kib-offset.patch @@ -0,0 +1,21 @@ +diff --git a/src/stm32/Kconfig b/src/stm32/Kconfig +index d45234c..e4a5f1a 100644 +--- a/src/stm32/Kconfig ++++ b/src/stm32/Kconfig +@@ -496,6 +496,8 @@ choice + prompt "Application start offset" + config STM32_APP_START_20000 + bool "128KiB offset" if MACH_STM32H7 && !MACH_STM32H750 ++ config STM32_APP_START_10000 ++ bool "64KiB offset" if MACH_STM32F4 + config STM32_APP_START_8000 + bool "32KiB offset" if MACH_STM32F2 || MACH_STM32F4 || MACH_STM32H750 + config STM32_APP_START_4000 +@@ -509,6 +511,7 @@ endchoice + config LAUNCH_APP_ADDRESS + hex + default 0x8020000 if STM32_APP_START_20000 ++ default 0x8010000 if STM32_APP_START_10000 + default 0x8008000 if STM32_APP_START_8000 + default 0x8004000 if STM32_APP_START_4000 + default 0x8002000 if STM32_APP_START_2000 diff --git a/meta-opencentauri/recipes-apps/katapult/files/config.canvas b/meta-opencentauri/recipes-apps/katapult/files/config.canvas new file mode 100644 index 00000000..b89231ca --- /dev/null +++ b/meta-opencentauri/recipes-apps/katapult/files/config.canvas @@ -0,0 +1,79 @@ +CONFIG_LOW_LEVEL_OPTIONS=y +# CONFIG_MACH_LPC176X is not set +CONFIG_MACH_STM32=y +# CONFIG_MACH_RPXXXX is not set +CONFIG_BOARD_DIRECTORY="stm32" +CONFIG_MCU="stm32f401xc" +CONFIG_CLOCK_FREQ=84000000 +CONFIG_FLASH_SIZE=0x40000 +CONFIG_FLASH_BOOT_ADDRESS=0x8000000 +CONFIG_RAM_START=0x20000000 +CONFIG_RAM_SIZE=0x10000 +CONFIG_STACK_SIZE=512 +CONFIG_FLASH_APPLICATION_ADDRESS=0x8010000 +CONFIG_FLASH_START=0x8000000 +CONFIG_LAUNCH_APP_ADDRESS=0x8010000 +CONFIG_BLOCK_SIZE=64 +CONFIG_STM32_SELECT=y +# CONFIG_MACH_STM32F103 is not set +# CONFIG_MACH_STM32F207 is not set +CONFIG_MACH_STM32F401=y +# CONFIG_MACH_STM32F405 is not set +# CONFIG_MACH_STM32F407 is not set +# CONFIG_MACH_STM32F429 is not set +# CONFIG_MACH_STM32F446 is not set +# CONFIG_MACH_STM32F031 is not set +# CONFIG_MACH_STM32F042 is not set +# CONFIG_MACH_STM32F070 is not set +# CONFIG_MACH_STM32F072 is not set +# CONFIG_MACH_STM32G0B0 is not set +# CONFIG_MACH_STM32G0B1 is not set +# CONFIG_MACH_STM32G431 is not set +# CONFIG_MACH_STM32H723 is not set +# CONFIG_MACH_STM32H743 is not set +CONFIG_MACH_STM32F4=y +CONFIG_HAVE_STM32_USBOTG=y +CONFIG_STM32_DFU_ROM_ADDRESS=0x1fff0000 +# CONFIG_STM32_FLASH_START_0000 is not set +# CONFIG_STM32_FLASH_START_8000 is not set +CONFIG_STM32_FLASH_START_C000=y +# CONFIG_STM32_FLASH_START_10000 is not set +# CONFIG_STM32_FLASH_START_4000 is not set +# CONFIG_STM32_CLOCK_REF_8M is not set +# CONFIG_STM32_CLOCK_REF_12M is not set +# CONFIG_STM32_CLOCK_REF_16M is not set +# CONFIG_STM32_CLOCK_REF_20M is not set +CONFIG_STM32_CLOCK_REF_24M=y +# CONFIG_STM32_CLOCK_REF_25M is not set +# CONFIG_STM32_CLOCK_REF_32M is not set +# CONFIG_STM32_CLOCK_REF_INTERNAL is not set +CONFIG_CLOCK_REF_FREQ=24000000 +CONFIG_STM32F0_TRIM=16 +CONFIG_STM32_USB_PA11_PA12=y +# CONFIG_STM32_SERIAL_USART1 is not set +# CONFIG_STM32_SERIAL_USART1_ALT_PB7_PB6 is not set +# CONFIG_STM32_SERIAL_USART2 is not set +# CONFIG_STM32_SERIAL_USART2_ALT_PD6_PD5 is not set +CONFIG_STM32_APP_START_C000=y +# CONFIG_STM32_APP_START_8000 is not set +# CONFIG_STM32_APP_START_4000 is not set +CONFIG_USBSERIAL=y +CONFIG_USB=y +CONFIG_USB_VENDOR_ID=0x1d50 +CONFIG_USB_DEVICE_ID=0x6177 +CONFIG_USB_SERIAL_NUMBER_CHIPID=y +CONFIG_USB_SERIAL_NUMBER="12345" + +# +# USB ids +# +# end of USB ids + +CONFIG_CANBUS_FREQUENCY=1000000 +CONFIG_INITIAL_PINS="" +CONFIG_ENABLE_DOUBLE_RESET=y +# CONFIG_ENABLE_BUTTON is not set +# CONFIG_ENABLE_LED is not set +CONFIG_BUILD_DEPLOYER=y +CONFIG_HAVE_CHIPID=y +CONFIG_KATAPULT_VERSION="b0bf421-dirty" diff --git a/meta-opencentauri/recipes-apps/katapult/katapult_1.0.0.inc b/meta-opencentauri/recipes-apps/katapult/katapult_1.0.0.inc index 1b2924ad..74e9a776 100644 --- a/meta-opencentauri/recipes-apps/katapult/katapult_1.0.0.inc +++ b/meta-opencentauri/recipes-apps/katapult/katapult_1.0.0.inc @@ -6,10 +6,11 @@ FILESEXTRAPATHS:prepend := "${THISDIR}/files:" SRC_URI = " \ git://github.com/Arksine/katapult.git;protocol=https;branch=master \ - file://01-add-support-for-48kib-offset.patch \ file://0001-Add-DEPLOYER_PAYLOAD-parameter.patch \ + file://0002-add-support-for-48kib-offset.patch \ + file://0003-add-support-for-64kib-offset.patch \ " -SRCREV = "32584cbbb66c4dc85fc87c0fa87ed508f7c2df52" +SRCREV = "ec59b9bb9ad6c2ec8d4dc6831fbc77f0b308e29e" S = "${WORKDIR}/git" diff --git a/meta-opencentauri/recipes-apps/klipper/files/config.canvas b/meta-opencentauri/recipes-apps/klipper/files/config.canvas new file mode 100644 index 00000000..88a28170 --- /dev/null +++ b/meta-opencentauri/recipes-apps/klipper/files/config.canvas @@ -0,0 +1,159 @@ +CONFIG_LOW_LEVEL_OPTIONS=y +# CONFIG_MACH_AVR is not set +# CONFIG_MACH_ATSAM is not set +# CONFIG_MACH_ATSAMD is not set +# CONFIG_MACH_LPC176X is not set +CONFIG_MACH_STM32=y +# CONFIG_MACH_HC32F460 is not set +# CONFIG_MACH_RPXXXX is not set +# CONFIG_MACH_PRU is not set +# CONFIG_MACH_AR100 is not set +# CONFIG_MACH_HIFI4 is not set +# CONFIG_MACH_LINUX is not set +# CONFIG_MACH_SIMU is not set +CONFIG_BOARD_DIRECTORY="stm32" +CONFIG_MCU="stm32f401xc" +CONFIG_CLOCK_FREQ=84000000 +CONFIG_USBSERIAL=y +CONFIG_FLASH_SIZE=0x40000 +CONFIG_FLASH_BOOT_ADDRESS=0x8000000 +CONFIG_RAM_START=0x20000000 +CONFIG_RAM_SIZE=0x10000 +CONFIG_STACK_SIZE=512 +CONFIG_FLASH_APPLICATION_ADDRESS=0x8010000 +CONFIG_STM32_SELECT=y +# CONFIG_MACH_AT32F403 is not set +# CONFIG_MACH_STM32F103 is not set +# CONFIG_MACH_STM32F207 is not set +CONFIG_MACH_STM32F401=y +# CONFIG_MACH_STM32F411 is not set +# CONFIG_MACH_STM32F405 is not set +# CONFIG_MACH_STM32F407 is not set +# CONFIG_MACH_STM32F427 is not set +# CONFIG_MACH_STM32F429 is not set +# CONFIG_MACH_STM32F446 is not set +# CONFIG_MACH_STM32F765 is not set +# CONFIG_MACH_STM32F031 is not set +# CONFIG_MACH_STM32F042 is not set +# CONFIG_MACH_STM32F070 is not set +# CONFIG_MACH_STM32F072 is not set +# CONFIG_MACH_STM32G070 is not set +# CONFIG_MACH_STM32G071 is not set +# CONFIG_MACH_STM32G0B0 is not set +# CONFIG_MACH_STM32G0B1 is not set +# CONFIG_MACH_STM32G431 is not set +# CONFIG_MACH_STM32G474 is not set +# CONFIG_MACH_STM32H723 is not set +# CONFIG_MACH_STM32H743 is not set +# CONFIG_MACH_STM32H750 is not set +# CONFIG_MACH_STM32L412 is not set +# CONFIG_MACH_N32G452 is not set +# CONFIG_MACH_N32G455 is not set +CONFIG_MACH_STM32F4=y +CONFIG_HAVE_STM32_USBOTG=y +CONFIG_STM32_DFU_ROM_ADDRESS=0x1fff0000 +# CONFIG_STM32_FLASH_START_8000 is not set +CONFIG_STM32_FLASH_START_C000=y +# CONFIG_STM32_FLASH_START_10000 is not set +# CONFIG_STM32_FLASH_START_4000 is not set +# CONFIG_STM32_FLASH_START_0000 is not set +# CONFIG_STM32_CLOCK_REF_8M is not set +# CONFIG_STM32_CLOCK_REF_12M is not set +# CONFIG_STM32_CLOCK_REF_16M is not set +# CONFIG_STM32_CLOCK_REF_20M is not set +CONFIG_STM32_CLOCK_REF_24M=y +# CONFIG_STM32_CLOCK_REF_25M is not set +# CONFIG_STM32_CLOCK_REF_INTERNAL is not set +CONFIG_CLOCK_REF_FREQ=24000000 +CONFIG_STM32F0_TRIM=16 +CONFIG_STM32_USB_PA11_PA12=y +# CONFIG_STM32_SERIAL_USART1 is not set +# CONFIG_STM32_SERIAL_USART1_ALT_PB7_PB6 is not set +# CONFIG_STM32_SERIAL_USART2 is not set +# CONFIG_STM32_SERIAL_USART2_ALT_PD6_PD5 is not set +# CONFIG_STM32_SERIAL_USART6 is not set +# CONFIG_STM32_SERIAL_USART6_ALT_PC7_PC6 is not set +CONFIG_USB=y +CONFIG_USB_VENDOR_ID=0x1d50 +CONFIG_USB_DEVICE_ID=0x614e +CONFIG_USB_SERIAL_NUMBER_CHIPID=y +CONFIG_USB_SERIAL_NUMBER="12345" +CONFIG_USB_MANUFACTURER="Klipper" + +# +# USB ids +# +# end of USB ids + +CONFIG_CAN_UUID_CUSTOM="abc1234" +CONFIG_WANT_ADC=y +CONFIG_WANT_SPI=y +CONFIG_WANT_SOFTWARE_SPI=y +CONFIG_WANT_I2C=y +CONFIG_WANT_SOFTWARE_I2C=y +CONFIG_WANT_HARD_PWM=y +CONFIG_WANT_BUTTONS=y +CONFIG_WANT_TMCUART=y +CONFIG_WANT_NEOPIXEL=y +CONFIG_WANT_PULSE_COUNTER=y +CONFIG_WANT_ST7920=y +CONFIG_WANT_HD44780=y +CONFIG_WANT_ADXL345=y +CONFIG_WANT_LIS2DW=y +CONFIG_WANT_MPU9250=y +CONFIG_WANT_ICM20948=y +CONFIG_WANT_THERMOCOUPLE=y +CONFIG_WANT_HX71X=y +CONFIG_WANT_ADS1220=y +CONFIG_WANT_LDC1612=y +# CONFIG_WANT_SENSOR_ANGLE is not set +CONFIG_WANT_GPIO_SPI=y +CONFIG_WANT_GPIO_ADC=y +CONFIG_WANT_GPIO_I2C=y +CONFIG_NEED_SENSOR_BULK=y +# CONFIG_WANT_OPTIMIZE_SIZE is not set + +# +# Feature Configuration +# + +# +# Microcontroller interfaces +# +# CONFIG_WANT_GPIO_BUTTONS is not set + +# +# ---- +# + +# +# LCD chips +# + +# +# Accelerometer chips +# + +# +# External ADC type chips +# + +# +# Compiler options +# +# end of Feature Configuration + +CONFIG_CANBUS_FREQUENCY=1000000 +CONFIG_INLINE_STEPPER_HACK=y +CONFIG_HAVE_STEPPER_OPTIMIZED_BOTH_EDGE=y +CONFIG_WANT_STEPPER_OPTIMIZED_BOTH_EDGE=y +CONFIG_INITIAL_PINS="PC8" +CONFIG_HAVE_GPIO=y +CONFIG_HAVE_GPIO_ADC=y +CONFIG_HAVE_GPIO_SPI=y +CONFIG_HAVE_GPIO_SDIO=y +CONFIG_HAVE_GPIO_I2C=y +CONFIG_HAVE_GPIO_HARD_PWM=y +CONFIG_HAVE_STRICT_TIMING=y +CONFIG_HAVE_CHIPID=y +CONFIG_HAVE_BOOTLOADER_REQUEST=y diff --git a/meta-opencentauri/recipes-apps/klipper/kalico-firmware-canvas_2026.02.00.bb b/meta-opencentauri/recipes-apps/klipper/kalico-firmware-canvas_2026.02.00.bb new file mode 100644 index 00000000..619a2281 --- /dev/null +++ b/meta-opencentauri/recipes-apps/klipper/kalico-firmware-canvas_2026.02.00.bb @@ -0,0 +1,35 @@ +require kalico_${PV}.inc +inherit update-rc.d + +SUMMARY = "Kalico 3D Printer Firmware" +DESCRIPTION = "Klipper, but Limitless" + +SRC_URI += " \ + file://config.canvas \ +" + +PR = "r3" + +DEPENDS += "gcc-arm-none-eabi-native" +RDEPENDS:${PN} = " \ + flashtool \ + canvas-bootloader-upgrade \ + mcu-flasher \ +" + +RPROVIDES:${PN} += "klipper-firmware-canvas" + +EXTRA_OEMAKE += "KCONFIG_CONFIG=../config.canvas" + +INITSCRIPT_NAME = "klipper-firmware-canvas" +INITSCRIPT_PARAMS = "defaults 94 4" + +do_install() { + install -d ${D}/lib/firmware + cp -r ${S}/out/klipper.bin ${D}/lib/firmware/klipper-canvas.bin + echo "${SRCREV}-${PR}" > ${D}/lib/firmware/klipper-canvas.bin.ver +} + +FILES:${PN} = " \ + /lib/firmware/klipper-canvas.bin \ +" diff --git a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.bb b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.bb index 895dc27c..6b44e107 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.bb +++ b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.bb @@ -31,6 +31,7 @@ RDEPENDS:${PN} = " \ kalico-firmware-dsp \ kalico-firmware-toolhead \ kalico-firmware-bed \ + kalico-firmware-canvas \ " RPROVIDES:${PN} += "klipper" diff --git a/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb b/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb index a16db6da..8d2316fc 100644 --- a/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb +++ b/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb @@ -38,7 +38,36 @@ do_install() { } FILES:${PN} = " \ - ${datadir}/klipper/AFC* \ + ${datadir}/klipper/AFC_buffer.py \ + ${datadir}/klipper/AFC_stats.py \ + ${datadir}/klipper/AFC_Toolchanger.py \ + ${datadir}/klipper/AFC_respond.py \ + ${datadir}/klipper/AFC_extruder.py \ + ${datadir}/klipper/AFC_unit.py \ + ${datadir}/klipper/AFC_prep.py \ + ${datadir}/klipper/AFC_led.py \ + ${datadir}/klipper/AFC_button.py \ + ${datadir}/klipper/AFC_vivid.py \ + ${datadir}/klipper/AFC_logger.py \ + ${datadir}/klipper/AFC_spool.py \ + ${datadir}/klipper/AFC_BoxTurtle.py \ + ${datadir}/klipper/AFC_lane.py \ + ${datadir}/klipper/AFC_canvas.py \ + ${datadir}/klipper/AFC_hub.py \ + ${datadir}/klipper/AFC_canvas_lane.py \ + ${datadir}/klipper/openams_integration.py \ + ${datadir}/klipper/AFC_poop.py \ + ${datadir}/klipper/AFC_stepper.py \ + ${datadir}/klipper/AFC_QuattroBox.py \ + ${datadir}/klipper/AFC_utils.py \ + ${datadir}/klipper/AFC_HTLF.py \ + ${datadir}/klipper/AFC_form_tip.py \ + ${datadir}/klipper/AFC_assist.py \ + ${datadir}/klipper/AFC.py \ + ${datadir}/klipper/AFC_OpenAMS.py \ + ${datadir}/klipper/AFC_functions.py \ + ${datadir}/klipper/AFC_NightOwl.py \ + ${datadir}/klipper/AFC_error.py \ ${sysconfdir}/klipper/config/extras-readonly/afc.cfg \ ${sysconfdir}/klipper/config/extras-readonly/canvas.cfg \ " \ No newline at end of file From f0ace463726867d3d918f052dc1e8fea4c875255 Mon Sep 17 00:00:00 2001 From: suchmememanyskill <38142618+suchmememanyskill@users.noreply.github.com> Date: Wed, 6 May 2026 00:25:31 +0200 Subject: [PATCH 04/19] Fix katapult patch --- ...-support-for-48kib-and-64kib-offset.patch} | 9 +++++--- .../0003-add-support-for-64kib-offset.patch | 21 ------------------- .../recipes-apps/katapult/katapult_1.0.0.inc | 3 +-- 3 files changed, 7 insertions(+), 26 deletions(-) rename meta-opencentauri/recipes-apps/katapult/files/{0002-add-support-for-48kib-offset.patch => 0002-add-support-for-48kib-and-64kib-offset.patch} (84%) delete mode 100644 meta-opencentauri/recipes-apps/katapult/files/0003-add-support-for-64kib-offset.patch diff --git a/meta-opencentauri/recipes-apps/katapult/files/0002-add-support-for-48kib-offset.patch b/meta-opencentauri/recipes-apps/katapult/files/0002-add-support-for-48kib-and-64kib-offset.patch similarity index 84% rename from meta-opencentauri/recipes-apps/katapult/files/0002-add-support-for-48kib-offset.patch rename to meta-opencentauri/recipes-apps/katapult/files/0002-add-support-for-48kib-and-64kib-offset.patch index 9c92047d..0980862b 100644 --- a/meta-opencentauri/recipes-apps/katapult/files/0002-add-support-for-48kib-offset.patch +++ b/meta-opencentauri/recipes-apps/katapult/files/0002-add-support-for-48kib-and-64kib-offset.patch @@ -1,5 +1,5 @@ diff --git a/src/stm32/Kconfig b/src/stm32/Kconfig -index d45234c..df937ef 100644 +index d45234c..4727d76 100644 --- a/src/stm32/Kconfig +++ b/src/stm32/Kconfig @@ -248,7 +248,7 @@ choice @@ -11,19 +11,22 @@ index d45234c..df937ef 100644 config STM32_FLASH_START_10000 bool "64KiB bootloader" if MACH_STM32F103 || MACH_STM32F446 || MACH_STM32F401 config STM32_FLASH_START_800 -@@ -496,6 +496,8 @@ choice +@@ -496,6 +496,10 @@ choice prompt "Application start offset" config STM32_APP_START_20000 bool "128KiB offset" if MACH_STM32H7 && !MACH_STM32H750 ++ config STM32_APP_START_10000 ++ bool "64KiB offset" if MACH_STM32F4 + config STM32_APP_START_C000 + bool "48KiB offset" if MACH_STM32F4 config STM32_APP_START_8000 bool "32KiB offset" if MACH_STM32F2 || MACH_STM32F4 || MACH_STM32H750 config STM32_APP_START_4000 -@@ -509,6 +511,7 @@ endchoice +@@ -509,6 +513,8 @@ endchoice config LAUNCH_APP_ADDRESS hex default 0x8020000 if STM32_APP_START_20000 ++ default 0x8010000 if STM32_APP_START_10000 + default 0x800C000 if STM32_APP_START_C000 default 0x8008000 if STM32_APP_START_8000 default 0x8004000 if STM32_APP_START_4000 diff --git a/meta-opencentauri/recipes-apps/katapult/files/0003-add-support-for-64kib-offset.patch b/meta-opencentauri/recipes-apps/katapult/files/0003-add-support-for-64kib-offset.patch deleted file mode 100644 index b3304ee3..00000000 --- a/meta-opencentauri/recipes-apps/katapult/files/0003-add-support-for-64kib-offset.patch +++ /dev/null @@ -1,21 +0,0 @@ -diff --git a/src/stm32/Kconfig b/src/stm32/Kconfig -index d45234c..e4a5f1a 100644 ---- a/src/stm32/Kconfig -+++ b/src/stm32/Kconfig -@@ -496,6 +496,8 @@ choice - prompt "Application start offset" - config STM32_APP_START_20000 - bool "128KiB offset" if MACH_STM32H7 && !MACH_STM32H750 -+ config STM32_APP_START_10000 -+ bool "64KiB offset" if MACH_STM32F4 - config STM32_APP_START_8000 - bool "32KiB offset" if MACH_STM32F2 || MACH_STM32F4 || MACH_STM32H750 - config STM32_APP_START_4000 -@@ -509,6 +511,7 @@ endchoice - config LAUNCH_APP_ADDRESS - hex - default 0x8020000 if STM32_APP_START_20000 -+ default 0x8010000 if STM32_APP_START_10000 - default 0x8008000 if STM32_APP_START_8000 - default 0x8004000 if STM32_APP_START_4000 - default 0x8002000 if STM32_APP_START_2000 diff --git a/meta-opencentauri/recipes-apps/katapult/katapult_1.0.0.inc b/meta-opencentauri/recipes-apps/katapult/katapult_1.0.0.inc index 74e9a776..0220d3b8 100644 --- a/meta-opencentauri/recipes-apps/katapult/katapult_1.0.0.inc +++ b/meta-opencentauri/recipes-apps/katapult/katapult_1.0.0.inc @@ -7,8 +7,7 @@ FILESEXTRAPATHS:prepend := "${THISDIR}/files:" SRC_URI = " \ git://github.com/Arksine/katapult.git;protocol=https;branch=master \ file://0001-Add-DEPLOYER_PAYLOAD-parameter.patch \ - file://0002-add-support-for-48kib-offset.patch \ - file://0003-add-support-for-64kib-offset.patch \ + file://0002-add-support-for-48kib-and-64kib-offset.patch \ " SRCREV = "ec59b9bb9ad6c2ec8d4dc6831fbc77f0b308e29e" From c8632780244bfc0a41fc72ad91c9ace7e29f095f Mon Sep 17 00:00:00 2001 From: suchmememanyskill <38142618+suchmememanyskill@users.noreply.github.com> Date: Wed, 6 May 2026 00:31:39 +0200 Subject: [PATCH 05/19] Don't forget klipper ver --- .../recipes-apps/klipper/kalico-firmware-canvas_2026.02.00.bb | 1 + 1 file changed, 1 insertion(+) diff --git a/meta-opencentauri/recipes-apps/klipper/kalico-firmware-canvas_2026.02.00.bb b/meta-opencentauri/recipes-apps/klipper/kalico-firmware-canvas_2026.02.00.bb index 619a2281..70f64563 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico-firmware-canvas_2026.02.00.bb +++ b/meta-opencentauri/recipes-apps/klipper/kalico-firmware-canvas_2026.02.00.bb @@ -32,4 +32,5 @@ do_install() { FILES:${PN} = " \ /lib/firmware/klipper-canvas.bin \ + /lib/firmware/klipper-canvas.bin.ver \ " From 828c390fc5d618265ab68c33a9f1b258f4f0afd6 Mon Sep 17 00:00:00 2001 From: suchmememanyskill <38142618+suchmememanyskill@users.noreply.github.com> Date: Wed, 6 May 2026 00:37:43 +0200 Subject: [PATCH 06/19] oops --- .../recipes-data/klipper-afc-addon/afc_0.1.bb | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb b/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb index 8d2316fc..61df055a 100644 --- a/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb +++ b/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb @@ -38,36 +38,36 @@ do_install() { } FILES:${PN} = " \ - ${datadir}/klipper/AFC_buffer.py \ - ${datadir}/klipper/AFC_stats.py \ - ${datadir}/klipper/AFC_Toolchanger.py \ - ${datadir}/klipper/AFC_respond.py \ - ${datadir}/klipper/AFC_extruder.py \ - ${datadir}/klipper/AFC_unit.py \ - ${datadir}/klipper/AFC_prep.py \ - ${datadir}/klipper/AFC_led.py \ - ${datadir}/klipper/AFC_button.py \ - ${datadir}/klipper/AFC_vivid.py \ - ${datadir}/klipper/AFC_logger.py \ - ${datadir}/klipper/AFC_spool.py \ - ${datadir}/klipper/AFC_BoxTurtle.py \ - ${datadir}/klipper/AFC_lane.py \ - ${datadir}/klipper/AFC_canvas.py \ - ${datadir}/klipper/AFC_hub.py \ - ${datadir}/klipper/AFC_canvas_lane.py \ - ${datadir}/klipper/openams_integration.py \ - ${datadir}/klipper/AFC_poop.py \ - ${datadir}/klipper/AFC_stepper.py \ - ${datadir}/klipper/AFC_QuattroBox.py \ - ${datadir}/klipper/AFC_utils.py \ - ${datadir}/klipper/AFC_HTLF.py \ - ${datadir}/klipper/AFC_form_tip.py \ - ${datadir}/klipper/AFC_assist.py \ - ${datadir}/klipper/AFC.py \ - ${datadir}/klipper/AFC_OpenAMS.py \ - ${datadir}/klipper/AFC_functions.py \ - ${datadir}/klipper/AFC_NightOwl.py \ - ${datadir}/klipper/AFC_error.py \ + ${datadir}/klipper/extras/AFC_buffer.py \ + ${datadir}/klipper/extras/AFC_stats.py \ + ${datadir}/klipper/extras/AFC_Toolchanger.py \ + ${datadir}/klipper/extras/AFC_respond.py \ + ${datadir}/klipper/extras/AFC_extruder.py \ + ${datadir}/klipper/extras/AFC_unit.py \ + ${datadir}/klipper/extras/AFC_prep.py \ + ${datadir}/klipper/extras/AFC_led.py \ + ${datadir}/klipper/extras/AFC_button.py \ + ${datadir}/klipper/extras/AFC_vivid.py \ + ${datadir}/klipper/extras/AFC_logger.py \ + ${datadir}/klipper/extras/AFC_spool.py \ + ${datadir}/klipper/extras/AFC_BoxTurtle.py \ + ${datadir}/klipper/extras/AFC_lane.py \ + ${datadir}/klipper/extras/AFC_canvas.py \ + ${datadir}/klipper/extras/AFC_hub.py \ + ${datadir}/klipper/extras/AFC_canvas_lane.py \ + ${datadir}/klipper/extras/openams_integration.py \ + ${datadir}/klipper/extras/AFC_poop.py \ + ${datadir}/klipper/extras/AFC_stepper.py \ + ${datadir}/klipper/extras/AFC_QuattroBox.py \ + ${datadir}/klipper/extras/AFC_utils.py \ + ${datadir}/klipper/extras/AFC_HTLF.py \ + ${datadir}/klipper/extras/AFC_form_tip.py \ + ${datadir}/klipper/extras/AFC_assist.py \ + ${datadir}/klipper/extras/AFC.py \ + ${datadir}/klipper/extras/AFC_OpenAMS.py \ + ${datadir}/klipper/extras/AFC_functions.py \ + ${datadir}/klipper/extras/AFC_NightOwl.py \ + ${datadir}/klipper/extras/AFC_error.py \ ${sysconfdir}/klipper/config/extras-readonly/afc.cfg \ ${sysconfdir}/klipper/config/extras-readonly/canvas.cfg \ " \ No newline at end of file From 987289b557f6e746b6cb45aeb5e250a0946dd846 Mon Sep 17 00:00:00 2001 From: suchmememanyskill <38142618+suchmememanyskill@users.noreply.github.com> Date: Wed, 6 May 2026 00:46:07 +0200 Subject: [PATCH 07/19] Don't forget to delete unnesesary file --- meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb | 1 + 1 file changed, 1 insertion(+) diff --git a/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb b/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb index 61df055a..1b6d2053 100644 --- a/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb +++ b/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb @@ -31,6 +31,7 @@ do_install() { # Install klippy extras install -d ${D}${datadir}/klipper/extras cp -r ${S}/extras/* ${D}${datadir}/klipper/extras/ + rm ${D}${datadir}/klipper/extras/__init__.py # Install config files install -d ${D}${sysconfdir}/klipper/config/extras-readonly From 26a9d0b087c4455730f8047698ecfbaf9a5cca87 Mon Sep 17 00:00:00 2001 From: suchmememanyskill <38142618+suchmememanyskill@users.noreply.github.com> Date: Thu, 7 May 2026 00:43:20 +0200 Subject: [PATCH 08/19] We don't have a canvas initscript yet --- .../klipper/kalico-firmware-canvas_2026.02.00.bb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/meta-opencentauri/recipes-apps/klipper/kalico-firmware-canvas_2026.02.00.bb b/meta-opencentauri/recipes-apps/klipper/kalico-firmware-canvas_2026.02.00.bb index 70f64563..da2436d3 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico-firmware-canvas_2026.02.00.bb +++ b/meta-opencentauri/recipes-apps/klipper/kalico-firmware-canvas_2026.02.00.bb @@ -1,5 +1,5 @@ require kalico_${PV}.inc -inherit update-rc.d +#inherit update-rc.d SUMMARY = "Kalico 3D Printer Firmware" DESCRIPTION = "Klipper, but Limitless" @@ -21,8 +21,8 @@ RPROVIDES:${PN} += "klipper-firmware-canvas" EXTRA_OEMAKE += "KCONFIG_CONFIG=../config.canvas" -INITSCRIPT_NAME = "klipper-firmware-canvas" -INITSCRIPT_PARAMS = "defaults 94 4" +#INITSCRIPT_NAME = "klipper-firmware-canvas" +#INITSCRIPT_PARAMS = "defaults 94 4" do_install() { install -d ${D}/lib/firmware From bd3520f34c36920d6ab5d396c08ed9ee1b70cf52 Mon Sep 17 00:00:00 2001 From: Sims <38142618+suchmememanyskill@users.noreply.github.com> Date: Thu, 7 May 2026 20:36:11 +0200 Subject: [PATCH 09/19] oops wrong path --- .../recipes-data/klipper-afc-addon/afc_0.1.bb | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb b/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb index 1b6d2053..73699cc2 100644 --- a/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb +++ b/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb @@ -29,9 +29,9 @@ do_compile[noexec] = "1" do_install() { # Install klippy extras - install -d ${D}${datadir}/klipper/extras - cp -r ${S}/extras/* ${D}${datadir}/klipper/extras/ - rm ${D}${datadir}/klipper/extras/__init__.py + install -d ${D}${datadir}/klipper/klippy/extras + cp -r ${S}/extras/* ${D}${datadir}/klipper/klippy/extras/ + rm ${D}${datadir}/klipper/klippy/extras/__init__.py # Install config files install -d ${D}${sysconfdir}/klipper/config/extras-readonly @@ -39,36 +39,36 @@ do_install() { } FILES:${PN} = " \ - ${datadir}/klipper/extras/AFC_buffer.py \ - ${datadir}/klipper/extras/AFC_stats.py \ - ${datadir}/klipper/extras/AFC_Toolchanger.py \ - ${datadir}/klipper/extras/AFC_respond.py \ - ${datadir}/klipper/extras/AFC_extruder.py \ - ${datadir}/klipper/extras/AFC_unit.py \ - ${datadir}/klipper/extras/AFC_prep.py \ - ${datadir}/klipper/extras/AFC_led.py \ - ${datadir}/klipper/extras/AFC_button.py \ - ${datadir}/klipper/extras/AFC_vivid.py \ - ${datadir}/klipper/extras/AFC_logger.py \ - ${datadir}/klipper/extras/AFC_spool.py \ - ${datadir}/klipper/extras/AFC_BoxTurtle.py \ - ${datadir}/klipper/extras/AFC_lane.py \ - ${datadir}/klipper/extras/AFC_canvas.py \ - ${datadir}/klipper/extras/AFC_hub.py \ - ${datadir}/klipper/extras/AFC_canvas_lane.py \ - ${datadir}/klipper/extras/openams_integration.py \ - ${datadir}/klipper/extras/AFC_poop.py \ - ${datadir}/klipper/extras/AFC_stepper.py \ - ${datadir}/klipper/extras/AFC_QuattroBox.py \ - ${datadir}/klipper/extras/AFC_utils.py \ - ${datadir}/klipper/extras/AFC_HTLF.py \ - ${datadir}/klipper/extras/AFC_form_tip.py \ - ${datadir}/klipper/extras/AFC_assist.py \ - ${datadir}/klipper/extras/AFC.py \ - ${datadir}/klipper/extras/AFC_OpenAMS.py \ - ${datadir}/klipper/extras/AFC_functions.py \ - ${datadir}/klipper/extras/AFC_NightOwl.py \ - ${datadir}/klipper/extras/AFC_error.py \ + ${datadir}/klipper/klippy/extras/AFC_buffer.py \ + ${datadir}/klipper/klippy/extras/AFC_stats.py \ + ${datadir}/klipper/klippy/extras/AFC_Toolchanger.py \ + ${datadir}/klipper/klippy/extras/AFC_respond.py \ + ${datadir}/klipper/klippy/extras/AFC_extruder.py \ + ${datadir}/klipper/klippy/extras/AFC_unit.py \ + ${datadir}/klipper/klippy/extras/AFC_prep.py \ + ${datadir}/klipper/klippy/extras/AFC_led.py \ + ${datadir}/klipper/klippy/extras/AFC_button.py \ + ${datadir}/klipper/klippy/extras/AFC_vivid.py \ + ${datadir}/klipper/klippy/extras/AFC_logger.py \ + ${datadir}/klipper/klippy/extras/AFC_spool.py \ + ${datadir}/klipper/klippy/extras/AFC_BoxTurtle.py \ + ${datadir}/klipper/klippy/extras/AFC_lane.py \ + ${datadir}/klipper/klippy/extras/AFC_canvas.py \ + ${datadir}/klipper/klippy/extras/AFC_hub.py \ + ${datadir}/klipper/klippy/extras/AFC_canvas_lane.py \ + ${datadir}/klipper/klippy/extras/openams_integration.py \ + ${datadir}/klipper/klippy/extras/AFC_poop.py \ + ${datadir}/klipper/klippy/extras/AFC_stepper.py \ + ${datadir}/klipper/klippy/extras/AFC_QuattroBox.py \ + ${datadir}/klipper/klippy/extras/AFC_utils.py \ + ${datadir}/klipper/klippy/extras/AFC_HTLF.py \ + ${datadir}/klipper/klippy/extras/AFC_form_tip.py \ + ${datadir}/klipper/klippy/extras/AFC_assist.py \ + ${datadir}/klipper/klippy/extras/AFC.py \ + ${datadir}/klipper/klippy/extras/AFC_OpenAMS.py \ + ${datadir}/klipper/klippy/extras/AFC_functions.py \ + ${datadir}/klipper/klippy/extras/AFC_NightOwl.py \ + ${datadir}/klipper/klippy/extras/AFC_error.py \ ${sysconfdir}/klipper/config/extras-readonly/afc.cfg \ ${sysconfdir}/klipper/config/extras-readonly/canvas.cfg \ " \ No newline at end of file From 33c6af747309fd0f9966d916b4e489d7e31a8576 Mon Sep 17 00:00:00 2001 From: suchmememanyskill <38142618+suchmememanyskill@users.noreply.github.com> Date: Fri, 8 May 2026 20:01:07 +0200 Subject: [PATCH 10/19] New AFC version --- meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb | 2 +- .../recipes-data/klipper-afc-addon/files/canvas.cfg | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb b/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb index 73699cc2..816c8057 100644 --- a/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb +++ b/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb @@ -12,7 +12,7 @@ SRC_URI = " \ file://canvas.cfg \ " -SRCREV = "9b5291954d27fdcfb5913a79c72e15884a9935b7" +SRCREV = "85d80506470595a3657c15a520712b43aad1a533" S = "${WORKDIR}/git" diff --git a/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg b/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg index 4e8a8bbf..0f65ea7f 100644 --- a/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg +++ b/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg @@ -182,6 +182,7 @@ gcode: [AFC] VarFile: /etc/klipper/config/AFC.var +default_material_type: PLA enable_sensors_in_gui: True ignore_spoolman_material_temps: False load_to_hub: False From ef3bbd205565a5ca68f7fc744b947d0e0ebd6808 Mon Sep 17 00:00:00 2001 From: suchmememanyskill <38142618+suchmememanyskill@users.noreply.github.com> Date: Thu, 14 May 2026 13:01:44 +0200 Subject: [PATCH 11/19] Add patch to allow excluding sections --- .../files/0004-allow-config-exclude.patch | 20 +++++++++++++++++++ .../klipper/kalico_2026.02.00.inc | 1 + 2 files changed, 21 insertions(+) create mode 100644 meta-opencentauri/recipes-apps/klipper/files/0004-allow-config-exclude.patch diff --git a/meta-opencentauri/recipes-apps/klipper/files/0004-allow-config-exclude.patch b/meta-opencentauri/recipes-apps/klipper/files/0004-allow-config-exclude.patch new file mode 100644 index 00000000..572b9bf2 --- /dev/null +++ b/meta-opencentauri/recipes-apps/klipper/files/0004-allow-config-exclude.patch @@ -0,0 +1,20 @@ +diff --git a/klippy/configfile.py b/klippy/configfile.py +index 691f1eb8..bb3ef969 100644 +--- a/klippy/configfile.py ++++ b/klippy/configfile.py +@@ -485,6 +485,15 @@ class PrinterConfig: + self._resolve_include( + filename, include_spec, fileconfig, visited + ) ++ elif header and header.startswith("!"): ++ self._parse_config_buffer(buffer, filename, fileconfig) ++ section_name = header[1:].strip() ++ if not fileconfig.has_section(section_name): ++ raise error( ++ "Section '%s' does not exist for exclusion" ++ % (section_name,) ++ ) ++ fileconfig.remove_section(section_name) + else: + line = _INCLUDERE.sub( + lambda match: _fix_include_path(filename, match), diff --git a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc index 808267da..7193262a 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc +++ b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc @@ -9,6 +9,7 @@ SRC_URI = "git://github.com/OpenCentauri/kalico.git;protocol=https;branch=rpmsg- file://0002-reduce-calibration-difference-tolerance.patch \ file://0001-Reduce-log-rotate-threshold.patch \ file://0003-drv8833-motor-control.patch \ + file://0004-allow-config-exclude.patch \ " SRCREV = "afe7178d0859f3cbc80e591473f86ee64183122b" From 9c6f6d077e3cfd3d97a87ff2e5b36e150bc559b6 Mon Sep 17 00:00:00 2001 From: suchmememanyskill <38142618+suchmememanyskill@users.noreply.github.com> Date: Thu, 14 May 2026 14:29:48 +0200 Subject: [PATCH 12/19] Bump AFC version --- meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb b/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb index 816c8057..4a25dde6 100644 --- a/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb +++ b/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb @@ -12,7 +12,7 @@ SRC_URI = " \ file://canvas.cfg \ " -SRCREV = "85d80506470595a3657c15a520712b43aad1a533" +SRCREV = "431e42b71e90ce2b3a3c49b973d6e64b0ae1e5fd" S = "${WORKDIR}/git" From 86bd610308bcb00db002b2134f354d934ed48436 Mon Sep 17 00:00:00 2001 From: suchmememanyskill <38142618+suchmememanyskill@users.noreply.github.com> Date: Thu, 14 May 2026 16:57:26 +0200 Subject: [PATCH 13/19] Canvas CFG changes --- .../klipper-afc-addon/files/canvas.cfg | 47 ++++++++++++------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg b/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg index 0f65ea7f..2d3afdde 100644 --- a/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg +++ b/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg @@ -59,11 +59,13 @@ aliases: [AFC_hub toolhead_4way_hub] switch_pin: !hotend:PB2 enable_hub_runout: False +afc_bowden_length: 6.5 +afc_unload_bowden_length: 40 [AFC_extruder extruder] pin_tool_start: !hotend:PB2 -tool_stn: 94 -tool_stn_unload: 31 +tool_stn: 43.5 +tool_stn_unload: 42 tool_sensor_after_extruder: 0 tool_unload_speed: 15 tool_load_speed: 8 @@ -73,16 +75,14 @@ extruder: extruder enable_9v_pin: canvas:ENABLE_9V enable_24v_pin: canvas:ENABLE_24V tangle_pin: !hotend:PB0 +toolhead_cover_pin: hotend:PC4 +cutter_pin: !hotend:PC5 short_moves_speed: 30 long_moves_speed: 70 -tool_unload_lane_extra_speed: 40 # Unused prep_distance: 120 prep_speed: 60 -tool_load_sync_speed_offset: 2 -tool_load_lane_extra_distance: 6.5 -tool_unload_lane_extra_distance: 40 # Unused -[AFC_canvas_lane canvas_lane0] +[AFC_canvas_lane CANVAS_1] unit: CANVAS_1:1 hub: toolhead_4way_hub map: T0 @@ -94,7 +94,7 @@ odometer_resolution: 3.15786 led_white_pin: canvas:LED0_WHITE led_red_pin: canvas:LED0_RED -[AFC_canvas_lane canvas_lane1] +[AFC_canvas_lane CANVAS_2] unit: CANVAS_1:2 hub: toolhead_4way_hub map: T1 @@ -106,7 +106,7 @@ odometer_resolution: 3.15786 led_white_pin: canvas:LED1_WHITE led_red_pin: canvas:LED1_RED -[AFC_canvas_lane canvas_lane2] +[AFC_canvas_lane CANVAS_3] unit: CANVAS_1:3 hub: toolhead_4way_hub map: T2 @@ -118,7 +118,7 @@ odometer_resolution: 3.15786 led_white_pin: canvas:LED2_WHITE led_red_pin: canvas:LED2_RED -[AFC_canvas_lane canvas_lane3] +[AFC_canvas_lane CANVAS_4] unit: CANVAS_1:4 hub: toolhead_4way_hub map: T3 @@ -131,36 +131,48 @@ led_white_pin: canvas:LED3_WHITE led_red_pin: canvas:LED3_RED [gcode_macro CUT] +variable_retract_length: 21 +variable_pushback_length: 16 gcode: + {% set self = printer["gcode_macro CUT"] %} + {% set retract_length = self.retract_length|float %} + {% set pushback_length = self.pushback_length|float %} + {% if "xyz" not in printer.toolhead.homed_axes %} G28 ; Home if necessary {% endif %} G90 ; Absolute positioning + M83 ; E relative G0 Z14.5 F600 ; Ensure we have some Z clearance + + G1 E-{retract_length} F600 ; Retract filament + G0 Y30 F15000 ; Get some clearance for Y G0 X255 F8000 ; Move to cutting position G0 Y3 F1200 ; Cut G0 Y30 F8000 ; Retract from cutting + + G1 E{pushback_length} F600 ; Push back filament + M400 ; Wait for movements to complete [gcode_macro POOP] gcode: - {% if params.PURGE_LENGTH %} - M83 - G1 E{params.PURGE_LENGTH} F600 - {% endif %} + {% set purge_length = params.PURGE_LENGTH|default(100, true)|int %} + M83 + G1 E{purge_length} F600 # Wipe Nozzle, from new firmware printer.cfg [gcode_macro M729] gcode: - SAVE_GCODE_STATE NAME=m729_saved_state - G90 ; Absolute positioning + M83 ; E relative ; === Approach === G1 X202 F6000 ; Move to wiper X start position G1 Y264.5 F6000 ; Move to wiper Y position (top of wiper) + G1 E-2 F600 ; === Pass 1: X sweeps at top of wiper === G1 X165 F8000 ; Wipe left (165 vs 170 in old — 5mm wider sweep) @@ -175,11 +187,10 @@ gcode: ; === Extract === G1 Y264.5 F1200 ; Slow lift up through wiper (clean exit) + G1 E2 F600 M400 ; Wait for all moves to complete - RESTORE_GCODE_STATE NAME=m729_saved_state - [AFC] VarFile: /etc/klipper/config/AFC.var default_material_type: PLA From 093640dbbd952e51b367618bbab0a277ee475bce Mon Sep 17 00:00:00 2001 From: suchmememanyskill <38142618+suchmememanyskill@users.noreply.github.com> Date: Thu, 14 May 2026 23:38:30 +0200 Subject: [PATCH 14/19] Speed --- .../recipes-data/klipper-afc-addon/files/canvas.cfg | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg b/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg index 2d3afdde..5c5e7d95 100644 --- a/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg +++ b/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg @@ -78,7 +78,7 @@ tangle_pin: !hotend:PB0 toolhead_cover_pin: hotend:PC4 cutter_pin: !hotend:PC5 short_moves_speed: 30 -long_moves_speed: 70 +long_moves_speed: 100 prep_distance: 120 prep_speed: 60 @@ -154,8 +154,6 @@ gcode: G0 Y30 F8000 ; Retract from cutting G1 E{pushback_length} F600 ; Push back filament - - M400 ; Wait for movements to complete [gcode_macro POOP] gcode: @@ -188,8 +186,6 @@ gcode: ; === Extract === G1 Y264.5 F1200 ; Slow lift up through wiper (clean exit) G1 E2 F600 - - M400 ; Wait for all moves to complete [AFC] VarFile: /etc/klipper/config/AFC.var From e38eeff2e51356312d39f2db3f0075b09702715f Mon Sep 17 00:00:00 2001 From: suchmememanyskill <38142618+suchmememanyskill@users.noreply.github.com> Date: Fri, 15 May 2026 00:45:26 +0200 Subject: [PATCH 15/19] Bump AFC version --- meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb b/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb index 4a25dde6..0fdadaf0 100644 --- a/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb +++ b/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb @@ -12,7 +12,7 @@ SRC_URI = " \ file://canvas.cfg \ " -SRCREV = "431e42b71e90ce2b3a3c49b973d6e64b0ae1e5fd" +SRCREV = "ae53a76cf945bf6cf1b35b0a0c624f44cc3bfa0d" S = "${WORKDIR}/git" From 46a793e516641d210c8f7774eb19f67fe1776002 Mon Sep 17 00:00:00 2001 From: suchmememanyskill <38142618+suchmememanyskill@users.noreply.github.com> Date: Mon, 18 May 2026 22:22:43 +0200 Subject: [PATCH 16/19] Add stock bootloader flasher, add way to auto-upgrade canvas firmware --- .../katapult/canvas-bootloader-stock_1.0.0.bb | 25 ++++++ .../files/canvas-cc1-bootloader-stock.bin | Bin 0 -> 49152 bytes .../files/klipper-firmware-canvas-init-d | 72 ++++++++++++++++++ .../kalico-firmware-canvas_2026.02.00.bb | 13 +++- .../config-manager/files/config_manager.py | 3 + .../config-manager/files/default.conf | 3 + .../update-scripts/files/restore-mcu-firmware | 12 ++- .../update-scripts/update-scripts_0.1.0.bb | 1 + 8 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 meta-opencentauri/recipes-apps/katapult/canvas-bootloader-stock_1.0.0.bb create mode 100644 meta-opencentauri/recipes-apps/katapult/files/canvas-cc1-bootloader-stock.bin create mode 100644 meta-opencentauri/recipes-apps/klipper/files/klipper-firmware-canvas-init-d diff --git a/meta-opencentauri/recipes-apps/katapult/canvas-bootloader-stock_1.0.0.bb b/meta-opencentauri/recipes-apps/katapult/canvas-bootloader-stock_1.0.0.bb new file mode 100644 index 00000000..86c35295 --- /dev/null +++ b/meta-opencentauri/recipes-apps/katapult/canvas-bootloader-stock_1.0.0.bb @@ -0,0 +1,25 @@ +require katapult_${PV}.inc + +SUMMARY = "Stock Canvas Bootloader Deployer" +DESCRIPTION = "Builds the Katapult deployer binary for reverting the canvas bootloader to stock." + +SRC_URI += " \ + file://config.canvas \ + file://canvas-cc1-bootloader-stock.bin \ +" + +DEPENDS += "gcc-arm-none-eabi-native" + +EXTRA_OEMAKE += " \ + KCONFIG_CONFIG=../config.canvas \ + DEPLOYER_PAYLOAD=../canvas-cc1-bootloader-stock.bin \ +" + +do_install() { + install -d ${D}/lib/firmware + install -m 0644 ${S}/out/deployer.bin ${D}/lib/firmware/katapult-deployer-stock-canvas.bin +} + +FILES:${PN} = " \ + /lib/firmware/katapult-deployer-stock-canvas.bin \ +" \ No newline at end of file diff --git a/meta-opencentauri/recipes-apps/katapult/files/canvas-cc1-bootloader-stock.bin b/meta-opencentauri/recipes-apps/katapult/files/canvas-cc1-bootloader-stock.bin new file mode 100644 index 0000000000000000000000000000000000000000..fddcc2494be979a628ebfaed149f7d00f650e096 GIT binary patch literal 49152 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�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 z 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 literal 0 HcmV?d00001 diff --git a/meta-opencentauri/recipes-apps/klipper/files/klipper-firmware-canvas-init-d b/meta-opencentauri/recipes-apps/klipper/files/klipper-firmware-canvas-init-d new file mode 100644 index 00000000..5d4a4a52 --- /dev/null +++ b/meta-opencentauri/recipes-apps/klipper/files/klipper-firmware-canvas-init-d @@ -0,0 +1,72 @@ +#!/bin/sh +### BEGIN INIT INFO +# Provides: klipper-firmware-canvas +# Required-Start: $local_fs $remote_fs $network +# Required-Stop: $local_fs $remote_fs $network +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Start OpenCentauri canvas firmware +### END INIT INFO + +FIRMWARE="/lib/firmware/klipper-canvas.bin" +FIRMWARE_VERSION="/lib/firmware/klipper-canvas.bin.ver" +KATAPULT_DEPLOYER="/lib/firmware/katapult-deployer-canvas.bin" +INSTALLED_VERSION="/etc/canvas.ver" +SERIALPORT="/dev/serial/by-path/platform-4200000.usb-usb-0:1.4:1.0" +FLASH_ENABLED="$(config-manager flash_mcu elegoo_canvas 2>/dev/null)" + +update(){ + if flashtool -d $SERIALPORT -f $FIRMWARE; then + cp $FIRMWARE_VERSION $INSTALLED_VERSION + sleep 3 + echo "Firmware flashed and version recorded." + else + echo "Firmware flash failed, version not updated." + return 1 + fi +} + +case "$1" in + start) + echo "Starting klipper-firmware-canvas..." + + if [ "$FLASH_ENABLED" != "True" ]; then + echo "Skipping canvas firmware flash because flash_mcu.elegoo_canvas is not enabled." + exit 0 + fi + + if [ -f "$INSTALLED_VERSION" ] && [ "$(cat $INSTALLED_VERSION)" = "$(cat $FIRMWARE_VERSION)" ]; then + echo "Firmware is already up to date." + exit 0 + else + echo "Firmware version mismatch or not installed, flashing firmware..." + if update; then + exit 0 + else + echo "Attempting bootloader upgrade..." + if mcu-flasher --canvas --firmware $KATAPULT_DEPLOYER --no-wait $SERIALPORT --firmware-version 6.4.9; then + sleep 3 + echo "Bootloader deployer flashed successfully, clearing installed version." + rm -f $INSTALLED_VERSION + update + else + echo "Cannot update bootloader." + exit 1 + fi + fi + fi + ;; + status) + if [ "$FLASH_ENABLED" = "True" ]; then + echo "klipper-firmware-canvas flashing is enabled" + else + echo "klipper-firmware-canvas flashing is disabled" + fi + ;; + *) + echo "Usage: $0 {start|status}" + exit 1 + ;; +esac + +exit 0 \ No newline at end of file diff --git a/meta-opencentauri/recipes-apps/klipper/kalico-firmware-canvas_2026.02.00.bb b/meta-opencentauri/recipes-apps/klipper/kalico-firmware-canvas_2026.02.00.bb index da2436d3..552daeee 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico-firmware-canvas_2026.02.00.bb +++ b/meta-opencentauri/recipes-apps/klipper/kalico-firmware-canvas_2026.02.00.bb @@ -1,11 +1,12 @@ require kalico_${PV}.inc -#inherit update-rc.d +inherit update-rc.d SUMMARY = "Kalico 3D Printer Firmware" DESCRIPTION = "Klipper, but Limitless" SRC_URI += " \ file://config.canvas \ + file://klipper-firmware-canvas-init-d \ " PR = "r3" @@ -14,6 +15,7 @@ DEPENDS += "gcc-arm-none-eabi-native" RDEPENDS:${PN} = " \ flashtool \ canvas-bootloader-upgrade \ + config-manager \ mcu-flasher \ " @@ -21,16 +23,21 @@ RPROVIDES:${PN} += "klipper-firmware-canvas" EXTRA_OEMAKE += "KCONFIG_CONFIG=../config.canvas" -#INITSCRIPT_NAME = "klipper-firmware-canvas" -#INITSCRIPT_PARAMS = "defaults 94 4" +INITSCRIPT_NAME = "klipper-firmware-canvas" +INITSCRIPT_PARAMS = "defaults 94 4" do_install() { install -d ${D}/lib/firmware cp -r ${S}/out/klipper.bin ${D}/lib/firmware/klipper-canvas.bin echo "${SRCREV}-${PR}" > ${D}/lib/firmware/klipper-canvas.bin.ver + + install -d ${D}${sysconfdir}/init.d + cp ${WORKDIR}/klipper-firmware-canvas-init-d ${D}${sysconfdir}/init.d/klipper-firmware-canvas + chmod 0755 ${D}${sysconfdir}/init.d/klipper-firmware-canvas } FILES:${PN} = " \ /lib/firmware/klipper-canvas.bin \ /lib/firmware/klipper-canvas.bin.ver \ + ${sysconfdir}/init.d/klipper-firmware-canvas \ " diff --git a/meta-opencentauri/recipes-data/config-manager/files/config_manager.py b/meta-opencentauri/recipes-data/config-manager/files/config_manager.py index 5fe82f8a..e69926e8 100644 --- a/meta-opencentauri/recipes-data/config-manager/files/config_manager.py +++ b/meta-opencentauri/recipes-data/config-manager/files/config_manager.py @@ -10,6 +10,9 @@ 'update': { 'release': ['stable', 'nightly'], }, + 'flash_mcu': { + 'elegoo_canvas': ['True', 'False'], + }, 'klipper': { 'sync_camera_led_to_chamber_led': ['True', 'False'], 'camera_led_default_on': ['True', 'False'], diff --git a/meta-opencentauri/recipes-data/config-manager/files/default.conf b/meta-opencentauri/recipes-data/config-manager/files/default.conf index 2059cd74..06453b4f 100644 --- a/meta-opencentauri/recipes-data/config-manager/files/default.conf +++ b/meta-opencentauri/recipes-data/config-manager/files/default.conf @@ -6,6 +6,9 @@ screen_brightness = 100 [update] release = stable +[flash_mcu] +elegoo_canvas = False + [klipper] sync_camera_led_to_chamber_led = False camera_led_default_on = True diff --git a/meta-opencentauri/recipes-data/update-scripts/files/restore-mcu-firmware b/meta-opencentauri/recipes-data/update-scripts/files/restore-mcu-firmware index e3497635..99a7e73b 100644 --- a/meta-opencentauri/recipes-data/update-scripts/files/restore-mcu-firmware +++ b/meta-opencentauri/recipes-data/update-scripts/files/restore-mcu-firmware @@ -2,6 +2,8 @@ set -e +FLASH_CANVAS="$(config-manager flash_mcu elegoo_canvas 2>/dev/null)" + SERIALPORT_BED="/dev/ttyS4" VERSION_BED="/etc/bed.ver" STOCK_FW_BED="/lib/firmware/katapult-deployer-stock-bed.bin" @@ -10,10 +12,18 @@ SERIALPORT_TOOLHEAD="/dev/serial/by-path/platform-4101400.usb-usb-0:1:1.0" VERSION_TOOLHEAD="/etc/toolhead.ver" STOCK_FW_TOOLHEAD="/lib/firmware/katapult-deployer-stock-toolhead.bin" +SERIALPORT_CANVAS="/dev/serial/by-path/platform-4200000.usb-usb-0:1.4:1.0" +VERSION_CANVAS="/etc/canvas.ver" +STOCK_FW_CANVAS="/lib/firmware/katapult-deployer-stock-canvas.bin" + # Flash stock bootloader service klipper stop flashtool -d $SERIALPORT_BED -b 250000 -r flashtool -d $SERIALPORT_BED -f $STOCK_FW_BED -b 115200 rm -f $VERSION_BED flashtool -d $SERIALPORT_TOOLHEAD -f $STOCK_FW_TOOLHEAD -rm -f $VERSION_TOOLHEAD \ No newline at end of file +rm -f $VERSION_TOOLHEAD +if [ "$FLASH_CANVAS" = "True" ]; then + flashtool -d $SERIALPORT_CANVAS -f $STOCK_FW_CANVAS + rm -f $VERSION_CANVAS +fi \ No newline at end of file diff --git a/meta-opencentauri/recipes-data/update-scripts/update-scripts_0.1.0.bb b/meta-opencentauri/recipes-data/update-scripts/update-scripts_0.1.0.bb index 84882769..56e4b53b 100644 --- a/meta-opencentauri/recipes-data/update-scripts/update-scripts_0.1.0.bb +++ b/meta-opencentauri/recipes-data/update-scripts/update-scripts_0.1.0.bb @@ -18,6 +18,7 @@ RDEPENDS:${PN} = " \ flashtool \ toolhead-bootloader-stock \ bed-bootloader-stock \ + canvas-bootloader-stock \ config-manager \ " From 5b01788f020ff963f1c57804f00b3481b66ede63 Mon Sep 17 00:00:00 2001 From: suchmememanyskill <38142618+suchmememanyskill@users.noreply.github.com> Date: Tue, 19 May 2026 00:17:28 +0200 Subject: [PATCH 17/19] oops upload the correct configs --- .../recipes-apps/katapult/files/config.canvas | 14 ++++++++++---- .../recipes-apps/klipper/files/config.canvas | 10 +++++++--- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/meta-opencentauri/recipes-apps/katapult/files/config.canvas b/meta-opencentauri/recipes-apps/katapult/files/config.canvas index b89231ca..f73c2945 100644 --- a/meta-opencentauri/recipes-apps/katapult/files/config.canvas +++ b/meta-opencentauri/recipes-apps/katapult/files/config.canvas @@ -31,13 +31,13 @@ CONFIG_MACH_STM32F401=y # CONFIG_MACH_STM32G431 is not set # CONFIG_MACH_STM32H723 is not set # CONFIG_MACH_STM32H743 is not set +# CONFIG_MACH_STM32H750 is not set CONFIG_MACH_STM32F4=y CONFIG_HAVE_STM32_USBOTG=y CONFIG_STM32_DFU_ROM_ADDRESS=0x1fff0000 # CONFIG_STM32_FLASH_START_0000 is not set # CONFIG_STM32_FLASH_START_8000 is not set -CONFIG_STM32_FLASH_START_C000=y -# CONFIG_STM32_FLASH_START_10000 is not set +CONFIG_STM32_FLASH_START_10000=y # CONFIG_STM32_FLASH_START_4000 is not set # CONFIG_STM32_CLOCK_REF_8M is not set # CONFIG_STM32_CLOCK_REF_12M is not set @@ -54,7 +54,7 @@ CONFIG_STM32_USB_PA11_PA12=y # CONFIG_STM32_SERIAL_USART1_ALT_PB7_PB6 is not set # CONFIG_STM32_SERIAL_USART2 is not set # CONFIG_STM32_SERIAL_USART2_ALT_PD6_PD5 is not set -CONFIG_STM32_APP_START_C000=y +CONFIG_STM32_APP_START_10000=y # CONFIG_STM32_APP_START_8000 is not set # CONFIG_STM32_APP_START_4000 is not set CONFIG_USBSERIAL=y @@ -70,10 +70,16 @@ CONFIG_USB_SERIAL_NUMBER="12345" # end of USB ids CONFIG_CANBUS_FREQUENCY=1000000 +CONFIG_HAVE_OPTIMIZE_OS=y +# CONFIG_HAVE_OPTIMIZE_O2 is not set +# CONFIG_HAVE_OPTIMIZE_OZ is not set +# CONFIG_HAVE_OPTIMIZE_OG is not set +# CONFIG_HAVE_OPTIMIZE_O0 is not set +CONFIG_OPTIMIZE_FLAG="-Os" CONFIG_INITIAL_PINS="" CONFIG_ENABLE_DOUBLE_RESET=y # CONFIG_ENABLE_BUTTON is not set # CONFIG_ENABLE_LED is not set CONFIG_BUILD_DEPLOYER=y CONFIG_HAVE_CHIPID=y -CONFIG_KATAPULT_VERSION="b0bf421-dirty" +CONFIG_KATAPULT_VERSION="v0.0.1-113-gec59b9b-dirty" diff --git a/meta-opencentauri/recipes-apps/klipper/files/config.canvas b/meta-opencentauri/recipes-apps/klipper/files/config.canvas index 88a28170..fa5477af 100644 --- a/meta-opencentauri/recipes-apps/klipper/files/config.canvas +++ b/meta-opencentauri/recipes-apps/klipper/files/config.canvas @@ -53,8 +53,8 @@ CONFIG_MACH_STM32F4=y CONFIG_HAVE_STM32_USBOTG=y CONFIG_STM32_DFU_ROM_ADDRESS=0x1fff0000 # CONFIG_STM32_FLASH_START_8000 is not set -CONFIG_STM32_FLASH_START_C000=y -# CONFIG_STM32_FLASH_START_10000 is not set +# CONFIG_STM32_FLASH_START_C000 is not set +CONFIG_STM32_FLASH_START_10000=y # CONFIG_STM32_FLASH_START_4000 is not set # CONFIG_STM32_FLASH_START_0000 is not set # CONFIG_STM32_CLOCK_REF_8M is not set @@ -104,6 +104,7 @@ CONFIG_WANT_MPU9250=y CONFIG_WANT_ICM20948=y CONFIG_WANT_THERMOCOUPLE=y CONFIG_WANT_HX71X=y +CONFIG_WANT_HX711S=y CONFIG_WANT_ADS1220=y CONFIG_WANT_LDC1612=y # CONFIG_WANT_SENSOR_ANGLE is not set @@ -111,6 +112,9 @@ CONFIG_WANT_GPIO_SPI=y CONFIG_WANT_GPIO_ADC=y CONFIG_WANT_GPIO_I2C=y CONFIG_NEED_SENSOR_BULK=y +CONFIG_NEED_LOAD_CELL_PROBE=y +CONFIG_NEED_LOAD_CELL_FUSION=y +CONFIG_NEED_SOS_FILTER=y # CONFIG_WANT_OPTIMIZE_SIZE is not set # @@ -147,7 +151,7 @@ CONFIG_CANBUS_FREQUENCY=1000000 CONFIG_INLINE_STEPPER_HACK=y CONFIG_HAVE_STEPPER_OPTIMIZED_BOTH_EDGE=y CONFIG_WANT_STEPPER_OPTIMIZED_BOTH_EDGE=y -CONFIG_INITIAL_PINS="PC8" +CONFIG_INITIAL_PINS="" CONFIG_HAVE_GPIO=y CONFIG_HAVE_GPIO_ADC=y CONFIG_HAVE_GPIO_SPI=y From e3c18f28cc947ea9216d7175bd90463a718d66b8 Mon Sep 17 00:00:00 2001 From: suchmememanyskill <38142618+suchmememanyskill@users.noreply.github.com> Date: Sun, 24 May 2026 02:38:15 +0200 Subject: [PATCH 18/19] Improve macros for AFC --- .../recipes-data/klipper-afc-addon/afc_0.1.bb | 2 +- .../klipper-afc-addon/files/canvas.cfg | 30 +++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb b/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb index 0fdadaf0..6479af3b 100644 --- a/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb +++ b/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb @@ -12,7 +12,7 @@ SRC_URI = " \ file://canvas.cfg \ " -SRCREV = "ae53a76cf945bf6cf1b35b0a0c624f44cc3bfa0d" +SRCREV = "6aed17d9fb28d90c567efc3a296df24b76e9b363" S = "${WORKDIR}/git" diff --git a/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg b/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg index 5c5e7d95..dff5c850 100644 --- a/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg +++ b/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg @@ -135,6 +135,7 @@ variable_retract_length: 21 variable_pushback_length: 16 gcode: {% set self = printer["gcode_macro CUT"] %} + {% set z_max = svar.z_maximum_lifting_distance|int %} {% set retract_length = self.retract_length|float %} {% set pushback_length = self.pushback_length|float %} @@ -142,9 +143,16 @@ gcode: G28 ; Home if necessary {% endif %} + {% if (printer.gcode_move.position.z + 4) < z_max %} + G91 + G1 Z+4 F3000 + {% else %} + G90 + G1 Z{z_max} F3000 + {% endif %} + G90 ; Absolute positioning M83 ; E relative - G0 Z14.5 F600 ; Ensure we have some Z clearance G1 E-{retract_length} F600 ; Retract filament @@ -187,6 +195,22 @@ gcode: G1 Y264.5 F1200 ; Slow lift up through wiper (clean exit) G1 E2 F600 +[gcode_macro KICK] +gcode: + G90 ; Absolute positioning + M83 ; E relative + + G1 X202 Y264.5 F6000 ; Move to tray position + G1 E-2 F600 ; Retract slightly to reduce pressure during kick + M106 S255 ; Full blast fan to solidify plastic + G4 P3000 ; Wait 3 seconds for fan to do its work + G1 X182 F8000 ; Kick left + M106 S0 ; Turn off fan + + ; Exit + G1 Y245 F6000 + G1 E2 F600 ; Push back filament after kick + [AFC] VarFile: /etc/klipper/config/AFC.var default_material_type: PLA @@ -230,10 +254,10 @@ poop_cmd: POOP # Kick Settings kick: False +kick_cmd: KICK # Wipe Settings -wipe: True -wipe_cmd: M729 +wipe: False # Form Tip Settings form_tip: False \ No newline at end of file From dcc29bc2c47543f647c73df45b8150c11f840470 Mon Sep 17 00:00:00 2001 From: suchmememanyskill <38142618+suchmememanyskill@users.noreply.github.com> Date: Mon, 25 May 2026 11:36:19 +0200 Subject: [PATCH 19/19] Remove toolhead cover pin --- .../recipes-data/klipper-afc-addon/files/canvas.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg b/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg index dff5c850..2785c5d1 100644 --- a/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg +++ b/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg @@ -75,7 +75,6 @@ extruder: extruder enable_9v_pin: canvas:ENABLE_9V enable_24v_pin: canvas:ENABLE_24V tangle_pin: !hotend:PB0 -toolhead_cover_pin: hotend:PC4 cutter_pin: !hotend:PC5 short_moves_speed: 30 long_moves_speed: 100