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" 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..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/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..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 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 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-apps/klipper/kalico_2026.02.00.inc b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc index 674ceef0..7193262a 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,8 @@ 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 \ + file://0004-allow-config-exclude.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/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/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..6479af3b --- /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 = "6aed17d9fb28d90c567efc3a296df24b76e9b363" + +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..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..2785c5d1 --- /dev/null +++ b/meta-opencentauri/recipes-data/klipper-afc-addon/files/canvas.cfg @@ -0,0 +1,262 @@ +[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: 6.5 +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 + +[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 + +[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 + +[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 + +[gcode_macro CUT] +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 %} + + {% if "xyz" not in printer.toolhead.homed_axes %} + 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 + + 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 + +[gcode_macro POOP] +gcode: + {% 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: + 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) + 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) + 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 +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 +kick_cmd: KICK + +# Wipe Settings +wipe: False + +# Form Tip Settings +form_tip: False \ No newline at end of file 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 \ "