diff --git a/meta-opencentauri/images/opencentauri-image-base.bb b/meta-opencentauri/images/opencentauri-image-base.bb index a2281ced..38fab979 100644 --- a/meta-opencentauri/images/opencentauri-image-base.bb +++ b/meta-opencentauri/images/opencentauri-image-base.bb @@ -45,6 +45,7 @@ CORE_IMAGE_EXTRA_INSTALL += "\ v4l-utils \ iproute2 \ chrony \ + afc \ " INITRAMFS_IMAGE = "core-image-tiny-initramfs" 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/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-and-64kib-offset.patch similarity index 84% 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-and-64kib-offset.patch index 9c92047d..0980862b 100644 --- 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-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/canvas-cc1-bootloader-stock.bin b/meta-opencentauri/recipes-apps/katapult/files/canvas-cc1-bootloader-stock.bin new file mode 100644 index 00000000..fddcc249 Binary files /dev/null and b/meta-opencentauri/recipes-apps/katapult/files/canvas-cc1-bootloader-stock.bin differ 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..f73c2945 --- /dev/null +++ b/meta-opencentauri/recipes-apps/katapult/files/config.canvas @@ -0,0 +1,85 @@ +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_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_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 +# 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_10000=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_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="v0.0.1-113-gec59b9b-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..0220d3b8 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,10 @@ 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-and-64kib-offset.patch \ " -SRCREV = "32584cbbb66c4dc85fc87c0fa87ed508f7c2df52" +SRCREV = "ec59b9bb9ad6c2ec8d4dc6831fbc77f0b308e29e" S = "${WORKDIR}/git" 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..d3920196 --- /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..14cd066f +--- /dev/null ++++ b/klippy/extras/drv8833.py +@@ -0,0 +1,787 @@ ++# 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, wait_for_completion=True): ++ 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) ++ if wait_for_completion: ++ 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) +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..cee90955 +--- /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/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/files/config.canvas b/meta-opencentauri/recipes-apps/klipper/files/config.canvas new file mode 100644 index 00000000..fa5477af --- /dev/null +++ b/meta-opencentauri/recipes-apps/klipper/files/config.canvas @@ -0,0 +1,163 @@ +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 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 +# 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_HX711S=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_NEED_LOAD_CELL_PROBE=y +CONFIG_NEED_LOAD_CELL_FUSION=y +CONFIG_NEED_SOS_FILTER=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="" +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/files/klipper-firmware-canvas-init-d b/meta-opencentauri/recipes-apps/klipper/files/klipper-firmware-canvas-init-d new file mode 100644 index 00000000..f07fc390 --- /dev/null +++ b/meta-opencentauri/recipes-apps/klipper/files/klipper-firmware-canvas-init-d @@ -0,0 +1,64 @@ +#!/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 + if mcu-flasher --canvas --timeout 10 --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 + fi + + update + 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/files/klipper-firmware-toolhead-init-d b/meta-opencentauri/recipes-apps/klipper/files/klipper-firmware-toolhead-init-d index 0e859c8a..9a650c57 100644 --- a/meta-opencentauri/recipes-apps/klipper/files/klipper-firmware-toolhead-init-d +++ b/meta-opencentauri/recipes-apps/klipper/files/klipper-firmware-toolhead-init-d @@ -35,6 +35,18 @@ case "$1" in echo $GPIO > /sys/class/gpio/unexport sleep 3 + if [ -e "$SERIALPORT" ]; then + VENDOR=$( + udevadm info -q property -n "$SERIALPORT" | + sed -n 's/^ID_VENDOR=//p' + ) + + if [ "$VENDOR" = "ShenZhenCBD" ]; then + echo "Detected Elegoo bootloader, resetting saved version..." + rm -f $INSTALLED_VERSION + fi + fi + if [ -f "$INSTALLED_VERSION" ] && [ "$(cat $INSTALLED_VERSION)" == "$(cat $FIRMWARE_VERSION)" ]; then echo "Firmware is already up to date." exit 0 diff --git a/meta-opencentauri/recipes-apps/klipper/files/machine.cfg b/meta-opencentauri/recipes-apps/klipper/files/machine.cfg index d0852cad..91fbbaa5 100644 --- a/meta-opencentauri/recipes-apps/klipper/files/machine.cfg +++ b/meta-opencentauri/recipes-apps/klipper/files/machine.cfg @@ -148,7 +148,7 @@ max_temp: 335 min_extrude_temp: 180 pressure_advance: 0.04 pressure_advance_smooth_time: 0.04 -max_extrude_only_distance: 150 +max_extrude_only_distance: 500 #was 150, for purging max_extrude_cross_section: 5 control: pid pid_Kp: 24.860427 diff --git a/meta-opencentauri/recipes-apps/klipper/files/macros.cfg b/meta-opencentauri/recipes-apps/klipper/files/macros.cfg index 8be0281e..f00219e2 100644 --- a/meta-opencentauri/recipes-apps/klipper/files/macros.cfg +++ b/meta-opencentauri/recipes-apps/klipper/files/macros.cfg @@ -6,7 +6,7 @@ [gcode_macro _global_var] variable_target_extruder: 0 variable_retract_amount: 2 -variable_extruder_temp:{'probing': 140, 'cleaning': 180, 'loading': 230, 'purging': 250} +variable_extruder_temp:{'probing': 140, 'cleaning': 190, 'loading': 230, 'purging': 250} variable_fan_state:{'p1': 0.0, 'p2': 0.0, 'p3': 0.0} variable_z_maximum_lifting_distance: 256 variable_pause_resume_travel_speed: 150 @@ -17,7 +17,8 @@ gcode: {% set target_bed = params.BED|int %} {% set target_extruder = params.EXTRUDER|int %} {% set target_chamber = params.CHAMBER|int %} - {% set sensor = printer['filament_switch_sensor filament_sensor'] %} + {% set tool = params.TOOL|default(0)|int %} + {% set sensor = printer['filament_switch_sensor filament_sensor'] if 'filament_switch_sensor filament_sensor' in printer else none %} {% set global = printer['gcode_macro _global_var'] %} {% set settings = printer['gcode_macro _COSMOS_SETTINGS']|default({}) %} {% set heatsoak = settings.heatsoak|default(1)|float %} @@ -35,7 +36,7 @@ gcode: SET_GCODE_VARIABLE MACRO=_global_var VARIABLE=target_extruder VALUE={target_extruder} - {% if sensor.enabled and not sensor.filament_detected %} + {% if sensor and sensor.enabled and not sensor.filament_detected %} SET_DISPLAY_TEXT MSG="NO FILAMENT LOADED!" CANCEL_PRINT RETURN @@ -96,11 +97,17 @@ gcode: {% endif %} {% endif %} + M104 S{target_extruder} + {% if adaptive_purge %} SMART_PARK {% endif %} M109 S{target_extruder} + {% if "AFC" in printer %} + T{tool} + {% endif %} + {% if adaptive_purge %} LINE_PURGE {% endif %} @@ -113,10 +120,11 @@ gcode: {% set global = printer['gcode_macro _global_var'] %} {% set settings = printer['gcode_macro _COSMOS_SETTINGS']|default({}) %} {% set z_max = global.z_maximum_lifting_distance|int %} + {% set unload_after_print = settings.unload_after_print|default('false')|lower == 'true' %} {% set z_ideal = settings.z_ideal_lifting_distance|default(170)|int %} {% set toolhead_led = settings.toolhead_led|default('false')|lower == 'true' %} {% set e_mintemp = printer.configfile.settings['extruder'].min_extrude_temp %} - {% set sensor = printer['filament_switch_sensor filament_sensor'] %} + {% set sensor = printer['filament_switch_sensor filament_sensor'] if 'filament_switch_sensor filament_sensor' in printer else printer["filament_switch_sensor extruder_tool_start"] %} {% if printer.toolhead.homed_axes|lower == "xyz" %} G91 @@ -140,6 +148,14 @@ gcode: G1 Z+{(z_max - printer.gcode_move.position.z)} F3000 {% endif %} + {% if sensor.enabled and sensor.filament_detected %} # TODO: This is wrong. We should assume there's filament if the filament sensor is disabled + {% if (printer.extruder.target != 0 and (printer.extruder.temperature + 5) >= printer.extruder.target and printer.extruder.temperature >= e_mintemp) %} + {% if unload_after_print %} + UNLOAD_FILAMENT + {% endif %} + {% endif %} + {% endif %} + MOVE_TO_TRAY {% endif %} TURN_OFF_FANS @@ -168,9 +184,10 @@ rename_existing: PAUSE_BASE gcode: {% set global = printer['gcode_macro _global_var'] %} {% set state = params.STATE|default('normal') %} + {% set emergency = params.EMERGENCY|default('false')|lower == 'true' %} {% set retract_amount = global.retract_amount|float %} {% set z_lift_max = global.z_maximum_lifting_distance|int %} - {% set sensor = printer['filament_switch_sensor filament_sensor'] %} + {% set sensor = printer['filament_switch_sensor filament_sensor'] if 'filament_switch_sensor filament_sensor' in printer else printer["filament_switch_sensor extruder_tool_start"] %} {% set pos = printer.gcode_move.position %} {% if printer.pause_resume.is_paused %} @@ -199,6 +216,12 @@ gcode: G90 {% endif %} + {% if emergency %} + SET_DISPLAY_TEXT MSG="Emergency pause. Please check the printer and resume when ready." + M104 S{global.extruder_temp.probing} + RETURN + {% endif %} + {% if state == 'filament_change' %} SET_DISPLAY_TEXT MSG="Paused for filament change" UNLOAD_FILAMENT @@ -218,7 +241,7 @@ gcode: rename_existing: RESUME_BASE gcode: {% set global = printer['gcode_macro _global_var'] %} - {% set sensor = printer['filament_switch_sensor filament_sensor'] %} + {% set sensor = printer['filament_switch_sensor filament_sensor'] if 'filament_switch_sensor filament_sensor' in printer else printer["filament_switch_sensor extruder_tool_start"] %} {% set extruder_target_temp = printer['gcode_macro _global_var'].target_extruder %} # If filament sensor is enabled require filament present before resuming @@ -287,7 +310,7 @@ gcode: {% for wipes in range(4) %} {% for i in range(8) %} {% set x = 187 - (i * 2) %} - {% set y = 265 if i % 2 == 0 else 263 %} + {% set y = 264 if i % 2 == 0 else 262 %} G1 X{x} Y{y} {% endfor %} G1 X187 @@ -300,7 +323,7 @@ gcode: {% for wipes in range(4) %} {% for i in range(8) %} {% set x = 187 - (i * 2) %} - {% set y = 263 if i % 2 == 0 else 265 %} + {% set y = 262 if i % 2 == 0 else 264 %} G1 X{x} Y{y} {% endfor %} G1 X187 @@ -319,20 +342,23 @@ gcode: G28 ; Home if necessary {% endif %} - {% if printer.gcode_move.position.x > 201.5 and printer.gcode_move.position.x < 202.5 and printer.gcode_move.position.y > 264.5 and printer.gcode_move.position.y < 265.5 %} + {% if printer.gcode_move.position.x > 201.5 and printer.gcode_move.position.x < 202.5 and printer.gcode_move.position.y > 263.5 and printer.gcode_move.position.y < 264.5 %} RETURN ; Already at the tray {% endif %} G90 ; Absolute positioning G0 X202 F12000 G0 Y250 F12000 ; Move near to the try - G0 Y265 F1200 ; Move slowly back to the tray + G0 Y264 F1200 ; Move slowly back to the tray M400 ; Wait for movements to complete [gcode_macro KICK] +variable_end_kick_x: 186 gcode: {% set global = printer['gcode_macro _global_var'] %} + {% set self = printer["gcode_macro KICK"] %} + {% set end_kick_x = self.end_kick_x|float %} {% set retract_amount = global.retract_amount|float %} {% if "xyz" not in printer.toolhead.homed_axes %} @@ -350,7 +376,7 @@ gcode: G1 E-{retract_amount} F300 ; 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 X186 F8000 ; Kick left + G1 X{self.end_kick_x} F8000 ; Kick left M106 S0 ; Turn off fan G1 Y250 F6000 ; Exit @@ -464,16 +490,38 @@ gcode: M400 ; Wait for movements to complete [gcode_macro CUT_FILAMENT] +variable_retract_length: 10 +variable_pushback_length: 5 gcode: + {% set self = printer["gcode_macro CUT_FILAMENT"] %} + {% set global = printer["gcode_macro _global_var"] %} + {% set z_max = global.z_maximum_lifting_distance|int %} + {% 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 + M83 ; E relative + + {% if printer.extruder.target >= 180 and printer.extruder.can_extrude %} + G1 E-{retract_length} F300 ; Retract filament + {% endif %} + + # TODO: This can probably be faster G0 Y20 F8000 G0 X255 F8000 G0 Y4 F1200 G0 Y20 F1200 + + {% if printer.extruder.target >= 180 and printer.extruder.can_extrude %} + MOVE_TO_TRAY + G1 E{pushback_length} F300 ; Push back filament + G1 E-{pushback_length} F600 ; Retract from pushback + {% endif %} + M400 [homing_override] 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..552daeee --- /dev/null +++ b/meta-opencentauri/recipes-apps/klipper/kalico-firmware-canvas_2026.02.00.bb @@ -0,0 +1,43 @@ +require kalico_${PV}.inc +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" + +DEPENDS += "gcc-arm-none-eabi-native" +RDEPENDS:${PN} = " \ + flashtool \ + canvas-bootloader-upgrade \ + config-manager \ + 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 + + 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-apps/klipper/kalico_2026.02.00.bb b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.bb index dc8e319a..7d11153b 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.bb +++ b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.bb @@ -34,6 +34,7 @@ RDEPENDS:${PN} = " \ kalico-firmware-toolhead \ kalico-firmware-bed \ check-update \ + kalico-firmware-canvas \ " RPROVIDES:${PN} += "klipper" 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 eb5965a6..050d592a 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc +++ b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc @@ -12,6 +12,8 @@ SRC_URI = "git://github.com/OpenCentauri/kalico.git;protocol=https;branch=hx711s file://0001-implement-pulldown-resistor-in-thermistor-calc.patch \ file://0001-probe-home-z-includes-z-offset.patch \ file://0001-force-specific-input-shaper.patch \ + file://0003-drv8833-motor-control.patch \ + file://0004-allow-config-exclude.patch \ " SRCREV = "3f166182f82f95584112571b4f7b2258922be60d" 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..3263d5b8 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 = "8690e3f9f84555cd0037a18f214959ae7274de7d" PR = "r1" S = "${WORKDIR}/git/mcu-flasher" diff --git a/meta-opencentauri/recipes-data/config-manager/files/build-klipper-var-config.sh b/meta-opencentauri/recipes-data/config-manager/files/build-klipper-var-config.sh index a980a764..2273da33 100644 --- a/meta-opencentauri/recipes-data/config-manager/files/build-klipper-var-config.sh +++ b/meta-opencentauri/recipes-data/config-manager/files/build-klipper-var-config.sh @@ -15,6 +15,7 @@ camera_led_default_on=$(get_config_value camera_led_default_on) heatsoak=$(get_config_value heatsoak) adaptive_mesh=$(get_config_value adaptive_mesh) adaptive_purge=$(get_config_value adaptive_purge) +unload_after_print=$(get_config_value unload_after_print) nozzle_z_homing=$(get_config_value nozzle_z_homing) z_ideal_lifting_distance=$(get_config_value z_ideal_lifting_distance) full_calibrate_hotend_temperature=$(get_config_value full_calibrate_hotend_temperature) @@ -34,6 +35,7 @@ variable_camera_led_default_on: ${camera_led_default_on} variable_heatsoak: ${heatsoak} variable_adaptive_mesh: ${adaptive_mesh} variable_adaptive_purge: ${adaptive_purge} +variable_unload_after_print: ${unload_after_print} variable_nozzle_z_homing: ${nozzle_z_homing} variable_z_ideal_lifting_distance: ${z_ideal_lifting_distance} variable_full_calibrate_hotend_temperature: ${full_calibrate_hotend_temperature} 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 58e5c524..b64a6840 100644 --- a/meta-opencentauri/recipes-data/config-manager/files/config_manager.py +++ b/meta-opencentauri/recipes-data/config-manager/files/config_manager.py @@ -11,6 +11,9 @@ 'release': ['stable', 'nightly'], 'check_for_updates': ['True', 'False'], }, + 'flash_mcu': { + 'elegoo_canvas': ['True', 'False'], + }, 'klipper': { 'sync_camera_led_to_chamber_led': ['True', 'False'], 'camera_led_default_on': ['True', 'False'], @@ -18,6 +21,7 @@ 'heatsoak': [str(x / 10) for x in range(100)] + [str(x) for x in range(31)], 'adaptive_mesh': ['True', 'False'], 'adaptive_purge': ['True', 'False'], + 'unload_after_print': ['True', 'False'], 'nozzle_z_homing': ['True', 'False'], 'z_ideal_lifting_distance': [str(i) for i in range(257)], 'full_calibrate_hotend_temperature': [str(i) for i in range(200, 301)], diff --git a/meta-opencentauri/recipes-data/config-manager/files/default.conf b/meta-opencentauri/recipes-data/config-manager/files/default.conf index 0835ae1f..842ff819 100644 --- a/meta-opencentauri/recipes-data/config-manager/files/default.conf +++ b/meta-opencentauri/recipes-data/config-manager/files/default.conf @@ -16,6 +16,9 @@ release = stable # Check for updates automatically on startup. Options: True, False. check_for_updates = True +[flash_mcu] +elegoo_canvas = False + [klipper] # Keep the camera LED state in sync with the chamber LED. sync_camera_led_to_chamber_led = False @@ -29,6 +32,8 @@ heatsoak = 1 adaptive_mesh = False # Prime the nozzle near the print area before each print. adaptive_purge = True +# Unload filament automatically when a print finishes. +unload_after_print = False # Use the nozzle and load cell probe for Z homing. nozzle_z_homing = False # Drop the bed to this height in millimeters when a print finishes. Range: 0-256. 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..f661ffd7 --- /dev/null +++ b/meta-opencentauri/recipes-data/klipper-afc-addon/afc_0.1.bb @@ -0,0 +1,74 @@ +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 = "72f597e210d4633922be4f45eacc5be1403fa747" + +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/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 + install -m 0644 ${WORKDIR}/canvas.cfg ${WORKDIR}/afc.cfg ${D}${sysconfdir}/klipper/config/extras-readonly +} + +FILES:${PN} = " \ + ${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 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..79982ac4 --- /dev/null +++ b/meta-opencentauri/recipes-data/klipper-afc-addon/files/afc.cfg @@ -0,0 +1,116 @@ +# ----- +# PLEASE DO NOT EDIT THIS FILE. EDIT printer.cfg INSTEAD. +# UPDATES WILL RESET THIS FILE. YOUR EDITS WILL BE LOST WHEN UPDATING. +# ----- + +# 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 +disable_pause_resume_remapping: True + +[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..c590aae3 --- /dev/null +++ b/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg @@ -0,0 +1,279 @@ +# ----- +# PLEASE DO NOT EDIT THIS FILE. EDIT printer.cfg INSTEAD. +# UPDATES WILL RESET THIS FILE. YOUR EDITS WILL BE LOST WHEN UPDATING. +# ----- + +[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_bowden_length: 8 +afc_unload_bowden_length: 40 + +[AFC_extruder extruder] +pin_tool_start: !hotend:PB2 +tool_stn: 43.5 +tool_stn_unload: 42 +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 +cutter_pin: !hotend:PC5 +short_moves_speed: 30 +long_moves_speed: 100 +prep_distance: 120 +prep_speed: 60 + +[AFC_canvas_lane CANVAS_1] +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 +disengage_distance: 0.5 +enable_hub_runout: True + +[AFC_canvas_lane CANVAS_2] +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 +disengage_distance: 0.5 +enable_hub_runout: True + +[AFC_canvas_lane CANVAS_3] +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 +disengage_distance: 0.5 +enable_hub_runout: True + +[AFC_canvas_lane CANVAS_4] +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 +disengage_distance: 0.5 +enable_hub_runout: True + +[gcode_macro POOP] +gcode: + {% set purge_length = params.PURGE_LENGTH|default(100, true)|float %} + + {% if printer.extruder.target < 180 or not printer.extruder.can_extrude %} + {action_raise_error("Extruder temperature too low for purging. Please heat the extruder to at least 180°C.")} + {% endif %} + + MOVE_TO_TRAY + M83 + RESPOND TYPE=command MSG="Purging {purge_length}mm of filament" + G1 Y265 F1200 + G1 E{purge_length} F300 + G1 Y264 F1200 + +[gcode_macro CLEAN_NOZZLE] +gcode: + {% set global = printer['gcode_macro _global_var'] %} + {% set toolhead_target = printer.extruder.target|int %} + BED_MESH_CLEAR + M104 S{global.extruder_temp.cleaning} + {% if printer.toolhead.homed_axes != "xyz" %} + G28 + {% endif %} + G90 + M83 + + MOVE_TO_TRAY + M109 S{global.extruder_temp.cleaning} + + M106 S255 + + ; === Approach === + G1 X202 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) + + # TODO: Testing needed. Is this enough cleaning? + + M104 S{toolhead_target} + M107 + M400 + +[gcode_macro KICK] +variable_end_kick_x: 181 + +[gcode_button toolhead_front_cover_detection] +pin: hotend:PC4 +press_gcode: + RESPOND TYPE=echo MSG="Front cover has fallen off!" + + {% if printer.print_stats.state == "printing" %} + PAUSE EMERGENCY=true + {% endif %} +release_gcode: + RESPOND TYPE=echo MSG="Front cover has been re-attached" + +[!gcode_macro LOAD_FILAMENT] + +[gcode_macro UNLOAD_FILAMENT] +gcode: + BT_TOOL_UNLOAD + +[gcode_macro M600] +gcode: + {% set tool = params.T|int %} + {% set purge = params.L|default(0)|float %} + + T{tool} PURGE_LENGTH={purge} + +[!filament_switch_sensor filament_sensor] + +[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 +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_FILAMENT +tool_cut_threshold: 10000 + +# Park Settings +park: True +park_cmd: MOVE_TO_TRAY + +# Poop Settings +poop: True +poop_cmd: POOP + +# Kick Settings +kick: True +kick_cmd: KICK + +# Wipe Settings +wipe: False + +# Form Tip Settings +form_tip: False + +[extruder] +pulldown_resistor: 100000 + +[temperature_sensor Mcu_canvas] +sensor_type: temperature_mcu +sensor_mcu: canvas 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 32126373..4c555ca5 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,6 +12,10 @@ 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 @@ -18,6 +24,11 @@ rm -f $VERSION_BED flashtool -d $SERIALPORT_TOOLHEAD -f $STOCK_FW_TOOLHEAD rm -f $VERSION_TOOLHEAD +if [ "$FLASH_CANVAS" = "True" ]; then + flashtool -d $SERIALPORT_CANVAS -f $STOCK_FW_CANVAS + rm -f $VERSION_CANVAS +fi + # Delete board-resource files cd /board-resource rm -f *.log* 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 \ "