From b926d5d9ebcde9aed70ed27e096c9081ae59719a Mon Sep 17 00:00:00 2001 From: Paul Swenson Date: Sat, 18 Jul 2026 09:07:07 -0400 Subject: [PATCH 01/20] zram: make swap initialization reliable on low-memory printers Replace the one-shot zram setup with bounded, observable initialization. Wait for the device, select an available compressor, verify swap activation, and retry transient module or sysfs failures once through a module reload. Keep initialization idempotent, clean up failed attempts safely, and log state/errors so a boot without zram is diagnosable. --- .../recipes-core/zram/files/init | 423 ++++++++++++++++-- 1 file changed, 385 insertions(+), 38 deletions(-) mode change 100644 => 100755 meta-opencentauri/recipes-core/zram/files/init diff --git a/meta-opencentauri/recipes-core/zram/files/init b/meta-opencentauri/recipes-core/zram/files/init old mode 100644 new mode 100755 index e9bdbf8e..712b7a2a --- a/meta-opencentauri/recipes-core/zram/files/init +++ b/meta-opencentauri/recipes-core/zram/files/init @@ -6,67 +6,414 @@ # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Increased Performance In Linux With zRam (Virtual Swap Compressed in RAM) -# Description: Adapted from zram init script in meta-openembedded +# Description: Reliable zram swap initialization for embedded SysV systems ### END INIT INFO -set -e + +# Do not use set -e here. A transient sysfs or device-node failure must be +# logged and retried, not silently turn into a boot without swap. + +NAME=zram +PATH=${ZRAM_PATH:-/sbin:/usr/sbin:/bin:/usr/bin} + +ZRAM_SYSFS=${ZRAM_SYSFS:-/sys/block/zram0} +ZRAM_MODULE_SYSFS=${ZRAM_MODULE_SYSFS:-/sys/module/zram} +ZRAM_DEV=${ZRAM_DEV:-/dev/zram0} +PROC_SWAPS=${PROC_SWAPS:-/proc/swaps} +MEMINFO=${MEMINFO:-/proc/meminfo} +SYSCTL_DIR=${SYSCTL_DIR:-/proc/sys} +ZRAM_DEFAULTS=${ZRAM_DEFAULTS:-/etc/default/zram} + +# Defaults selected for the CC1. Every retry-related value is deliberately +# capped below so a malformed /etc/default/zram cannot hang the boot forever. +FACTOR=200 +ZRAM_ALGO="lz4 lzo-rle zstd lzo" +ZRAM_MAX_ATTEMPTS=5 +ZRAM_READY_TIMEOUT=5 +ZRAM_RETRY_DELAY=1 +ZRAM_PRIORITY=100 + +log() { + log_message="$*" + if command -v logger >/dev/null 2>&1; then + logger -t "$NAME" "$log_message" 2>/dev/null || true + fi + echo "$NAME: $log_message" +} + +is_decimal() { + case "$1" in + ''|*[!0-9]*) return 1 ;; + *) return 0 ;; + esac +} + +is_active() { + grep -q "^${ZRAM_DEV}[[:space:]]" "$PROC_SWAPS" 2>/dev/null +} + +load_defaults() { + if [ -f "$ZRAM_DEFAULTS" ]; then + # shellcheck disable=SC1090 + if ! . "$ZRAM_DEFAULTS"; then + log "ERROR: could not source $ZRAM_DEFAULTS" + return 1 + fi + fi + return 0 +} + +validate_config() { + if ! is_decimal "$FACTOR" || [ "$FACTOR" -lt 1 ] || [ "$FACTOR" -gt 400 ]; then + log "ERROR: FACTOR must be an integer from 1 through 400 (got '$FACTOR')" + return 1 + fi + if ! is_decimal "$ZRAM_MAX_ATTEMPTS" || [ "$ZRAM_MAX_ATTEMPTS" -lt 1 ] || [ "$ZRAM_MAX_ATTEMPTS" -gt 5 ]; then + log "ERROR: ZRAM_MAX_ATTEMPTS must be an integer from 1 through 5 (got '$ZRAM_MAX_ATTEMPTS')" + return 1 + fi + if ! is_decimal "$ZRAM_READY_TIMEOUT" || [ "$ZRAM_READY_TIMEOUT" -lt 1 ] || [ "$ZRAM_READY_TIMEOUT" -gt 5 ]; then + log "ERROR: ZRAM_READY_TIMEOUT must be an integer from 1 through 5 (got '$ZRAM_READY_TIMEOUT')" + return 1 + fi + if ! is_decimal "$ZRAM_RETRY_DELAY" || [ "$ZRAM_RETRY_DELAY" -gt 5 ]; then + log "ERROR: ZRAM_RETRY_DELAY must be an integer from 0 through 5 (got '$ZRAM_RETRY_DELAY')" + return 1 + fi + if ! is_decimal "$ZRAM_PRIORITY" || [ "$ZRAM_PRIORITY" -gt 32767 ]; then + log "ERROR: ZRAM_PRIORITY must be an integer from 0 through 32767 (got '$ZRAM_PRIORITY')" + return 1 + fi + return 0 +} + +read_value() { + if [ -r "$1" ]; then + cat "$1" 2>/dev/null + else + echo unavailable + fi +} + +log_state() { + state_disksize=$(read_value "$ZRAM_SYSFS/disksize") + state_initstate=$(read_value "$ZRAM_SYSFS/initstate") + state_algorithm=$(read_value "$ZRAM_SYSFS/comp_algorithm") + if [ -r "$PROC_SWAPS" ]; then + state_swap=$(grep "^${ZRAM_DEV}[[:space:]]" "$PROC_SWAPS" 2>/dev/null || true) + else + state_swap=unavailable + fi + [ -n "$state_swap" ] || state_swap=inactive + log "state: disksize=$state_disksize initstate=$state_initstate algorithm=$state_algorithm swap=$state_swap" +} + +wait_for_device() { + ready_count=0 + while [ "$ready_count" -lt "$ZRAM_READY_TIMEOUT" ]; do + if [ -e "$ZRAM_SYSFS/disksize" ] && \ + [ -e "$ZRAM_SYSFS/reset" ] && \ + [ -e "$ZRAM_SYSFS/comp_algorithm" ] && \ + [ -e "$ZRAM_DEV" ]; then + return 0 + fi + sleep 1 + ready_count=$((ready_count + 1)) + done + log "ERROR: zram device did not become ready within ${ZRAM_READY_TIMEOUT}s" + log_state + return 1 +} + +ensure_module() { + command_output=$(modprobe zram num_devices=1 2>&1) + command_status=$? + if [ "$command_status" -ne 0 ]; then + log "ERROR: modprobe zram failed (status $command_status): $command_output" + return 1 + fi + [ -z "$command_output" ] || log "modprobe: $command_output" + return 0 +} + +reset_device() { + if [ ! -e "$ZRAM_SYSFS/reset" ]; then + log "ERROR: zram reset attribute is unavailable" + return 1 + fi + + command_output=$(printf '1\n' > "$ZRAM_SYSFS/reset" 2>&1) + command_status=$? + if [ "$command_status" -ne 0 ]; then + log "ERROR: could not reset $ZRAM_DEV (status $command_status): $command_output" + return 1 + fi + + reset_count=0 + while [ "$reset_count" -lt "$ZRAM_READY_TIMEOUT" ]; do + reset_disksize=$(read_value "$ZRAM_SYSFS/disksize") + if [ "$reset_disksize" = "0" ]; then + return 0 + fi + sleep 1 + reset_count=$((reset_count + 1)) + done + log "ERROR: reset did not return $ZRAM_DEV to disksize=0" + return 1 +} + +cleanup_device() { + cleanup_status=0 + + if is_active; then + command_output=$(swapoff "$ZRAM_DEV" 2>&1) + command_status=$? + if [ "$command_status" -ne 0 ]; then + log "ERROR: swapoff $ZRAM_DEV failed (status $command_status): $command_output" + cleanup_status=1 + fi + fi + + if is_active; then + log "ERROR: $ZRAM_DEV is still active; refusing to reset it" + cleanup_status=1 + elif [ -e "$ZRAM_SYSFS/reset" ]; then + reset_device || cleanup_status=1 + fi + + return "$cleanup_status" +} + +reload_module_once() { + if is_active; then + log "note: swap became active during recovery; keeping the active device" + return 0 + fi + + if [ -d "$ZRAM_MODULE_SYSFS" ]; then + command_output=$(rmmod zram 2>&1) + command_status=$? + if [ "$command_status" -ne 0 ]; then + log "note: rmmod zram failed (status $command_status): $command_output" + else + log "module reload recovery: zram unloaded" + sleep 1 + fi + fi + + ensure_module || return 1 + wait_for_device || return 1 + return 0 +} + +select_algorithm() { + algorithm_selected=0 + for algorithm in $ZRAM_ALGO; do + command_output=$(printf '%s\n' "$algorithm" > "$ZRAM_SYSFS/comp_algorithm" 2>&1) + command_status=$? + if [ "$command_status" -eq 0 ]; then + log "compressor: $algorithm" + algorithm_selected=1 + break + fi + log "note: compressor '$algorithm' was rejected (status $command_status): $command_output" + done + + if [ "$algorithm_selected" -eq 0 ]; then + current_algorithm=$(read_value "$ZRAM_SYSFS/comp_algorithm") + if [ -n "$current_algorithm" ] && [ "$current_algorithm" != unavailable ]; then + log "note: keeping kernel-selected compressor: $current_algorithm" + return 0 + fi + log "ERROR: no usable zram compressor is available" + return 1 + fi + return 0 +} + +configure_device() { + if ! reset_device; then + return 1 + fi + if ! select_algorithm; then + return 1 + fi + + command_output=$(printf '%s\n' "$disksize" > "$ZRAM_SYSFS/disksize" 2>&1) + command_status=$? + if [ "$command_status" -ne 0 ]; then + log "ERROR: could not set disksize=$disksize (status $command_status): $command_output" + return 1 + fi + + configured_disksize=$(read_value "$ZRAM_SYSFS/disksize") + if [ "$configured_disksize" != "$disksize" ]; then + log "ERROR: disksize verification failed (expected $disksize, got $configured_disksize)" + return 1 + fi + + command_output=$(mkswap "$ZRAM_DEV" 2>&1) + command_status=$? + if [ "$command_status" -ne 0 ]; then + log "ERROR: mkswap $ZRAM_DEV failed (status $command_status): $command_output" + return 1 + fi + + command_output=$(swapon -p "$ZRAM_PRIORITY" "$ZRAM_DEV" 2>&1) + command_status=$? + if [ "$command_status" -ne 0 ]; then + log "ERROR: swapon $ZRAM_DEV failed (status $command_status): $command_output" + return 1 + fi + + if ! is_active; then + log "ERROR: swapon returned success but $ZRAM_DEV is absent from $PROC_SWAPS" + return 1 + fi + return 0 +} + +tune_vm() { + for tuning in \ + vm.swappiness=150 \ + vm.watermark_boost_factor=0 \ + vm.watermark_scale_factor=125 \ + vm.page-cluster=0 \ + vm.vfs_cache_pressure=500 + do + tuning_key=${tuning%%=*} + tuning_value=${tuning#*=} + tuning_path="$SYSCTL_DIR/$(printf '%s' "$tuning_key" | tr '.' '/')" + if [ ! -e "$tuning_path" ]; then + log "note: $tuning_key is not present on this kernel (skipped)" + continue + fi + command_output=$(printf '%s\n' "$tuning_value" > "$tuning_path" 2>&1) + command_status=$? + if [ "$command_status" -ne 0 ]; then + log "note: could not set $tuning_key=$tuning_value (status $command_status): $command_output" + fi + done +} + +get_memory_size() { + memtotal=$(awk '/^MemTotal:[[:space:]]+[0-9]+/ { print $2; exit }' "$MEMINFO" 2>/dev/null) + if ! is_decimal "$memtotal" || [ "$memtotal" -lt 1 ]; then + log "ERROR: could not read a valid MemTotal from $MEMINFO" + return 1 + fi + + # Multiply before converting kB to bytes, but keep the intermediate + # product bounded for the 32-bit shell used by the CC1. + disksize_kb=$((memtotal * FACTOR / 100)) + disksize=$((disksize_kb * 1024)) + if [ "$disksize" -lt 4096 ]; then + log "ERROR: calculated zram disksize is invalid: $disksize bytes" + return 1 + fi + log "memory: ${memtotal}kB; zram disksize=${disksize} bytes (factor ${FACTOR}%)" + return 0 +} start() { - #default Factor % = 200 change this value here or create /etc/default/zram - FACTOR=200 - #& put the above single line in /etc/default/zram with the value you want - [ -f /etc/default/zram ] && . /etc/default/zram || true - factor=$FACTOR # percentage + load_defaults || return 1 + validate_config || return 1 - # get the amount of memory in the machine - memtotal=$(grep MemTotal /proc/meminfo | awk ' { print $2 } ') - disksize=$(($memtotal*$factor/100*1024)) + if is_active; then + log "zram swap already active; nothing to do" + log_state + return 0 + fi + + get_memory_size || return 1 + log "starting zram swap (up to $ZRAM_MAX_ATTEMPTS attempts; readiness timeout ${ZRAM_READY_TIMEOUT}s)" + + attempt=1 + module_reload_used=0 + while [ "$attempt" -le "$ZRAM_MAX_ATTEMPTS" ]; do + log "initialization attempt $attempt/$ZRAM_MAX_ATTEMPTS" - # load dependency modules - modprobe zram num_devices=1 - echo "zram devices probed successfully" + if ensure_module && wait_for_device && configure_device; then + log "zram swap active: ${disksize} bytes at priority ${ZRAM_PRIORITY}" + log_state + tune_vm + return 0 + fi - echo 1 > /sys/block/zram0/reset - echo lz4 > /sys/block/zram0/comp_algorithm - echo $disksize > /sys/block/zram0/disksize + log "ERROR: initialization attempt $attempt failed" + cleanup_device || true - # Creating swap filesystems - mkswap /dev/zram0 + if is_active; then + log "zram swap is active after recovery; treating initialization as successful" + log_state + tune_vm + return 0 + fi - # zram tuning - echo 150 > /proc/sys/vm/swappiness - echo 0 > /proc/sys/vm/watermark_boost_factor - echo 125 > /proc/sys/vm/watermark_scale_factor - echo 0 > /proc/sys/vm/page-cluster - echo 500 > /proc/sys/vm/vfs_cache_pressure + if [ "$module_reload_used" -eq 0 ] && [ "$attempt" -lt "$ZRAM_MAX_ATTEMPTS" ]; then + log "recovery: attempting one zram module unload/reload" + reload_module_once || log "note: module reload recovery did not complete" + module_reload_used=1 + fi - # Switch the swaps on - swapon -p 100 /dev/zram0 + if [ "$attempt" -lt "$ZRAM_MAX_ATTEMPTS" ] && [ "$ZRAM_RETRY_DELAY" -gt 0 ]; then + sleep "$ZRAM_RETRY_DELAY" + fi + attempt=$((attempt + 1)) + done + + log "ERROR: zram swap unavailable after $ZRAM_MAX_ATTEMPTS attempts" + log_state + return 1 } stop() { - # Switching off swap - if [ "$(grep /dev/zram0 /proc/swaps)" != "" ]; then - swapoff /dev/zram0 - sleep 1 + stop_status=0 + + if is_active; then + command_output=$(swapoff "$ZRAM_DEV" 2>&1) + command_status=$? + if [ "$command_status" -ne 0 ] || is_active; then + log "ERROR: could not disable swap on $ZRAM_DEV (status $command_status): $command_output" + stop_status=1 + fi fi - sleep 1 - rmmod zram + + if ! is_active && [ -e "$ZRAM_SYSFS/reset" ]; then + reset_device || stop_status=1 + fi + + if [ -d "$ZRAM_MODULE_SYSFS" ]; then + command_output=$(rmmod zram 2>&1) + command_status=$? + if [ "$command_status" -ne 0 ]; then + log "ERROR: rmmod zram failed (status $command_status): $command_output" + stop_status=1 + else + log "zram module unloaded" + fi + fi + return "$stop_status" } +RETVAL=0 case "$1" in start) - start + start || RETVAL=$? ;; stop) - stop + stop || RETVAL=$? ;; restart) - stop - sleep 3 - start + if stop; then + sleep 1 + start || RETVAL=$? + else + RETVAL=1 + fi ;; *) echo "Usage: $0 {start|stop|restart}" RETVAL=1 + ;; esac -exit $RETVAL +exit "$RETVAL" From 764e17418088de33338b2ba0680511242f59fed8 Mon Sep 17 00:00:00 2001 From: Paul Swenson Date: Sat, 18 Jul 2026 09:54:15 -0400 Subject: [PATCH 02/20] overlayfs: refresh image-owned init scripts on update Before mounting the persistent /etc overlay, remove upper-layer init-script overrides only when the new rootfs also supplies that script. This lets image updates refresh launch defaults for every Cosmos-owned service while preserving user-created scripts such as ustreamer.new. Also clear overlayfs whiteouts that hide image-owned scripts. Extend the pre-flash cleanup to cover zram alongside the Moonraker and uStreamer scripts changed by this update. --- .../images/files/overlayfs-etc-preinit.sh.in | 31 +++++++++++++++++++ .../swu-flasher/files/readonly-config-reset | 11 ++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/meta-opencentauri/images/files/overlayfs-etc-preinit.sh.in b/meta-opencentauri/images/files/overlayfs-etc-preinit.sh.in index d3a48249..cbcc48b0 100644 --- a/meta-opencentauri/images/files/overlayfs-etc-preinit.sh.in +++ b/meta-opencentauri/images/files/overlayfs-etc-preinit.sh.in @@ -58,6 +58,37 @@ then mkdir -p $UPPER_DIR mkdir -p $WORK_DIR + # Init scripts supplied by the image are release-owned rather than printer + # configuration. Before /etc becomes an overlay, compare upper entries + # with the new rootfs's /etc/init.d and discard only stale overrides of + # scripts that the image also supplies. This keeps user-created scripts + # (for example ustreamer.new) intact while allowing every shipped service + # to receive revised launch defaults on the first boot after an A/B update. + if [ -d "$UPPER_DIR/init.d" ]; then + for upper_script in "$UPPER_DIR"/init.d/*; do + [ -f "$upper_script" ] || continue + script_name=${upper_script##*/} + [ -f "/etc/init.d/$script_name" ] || continue + rm -f "$upper_script" + done + + # A deleted image script may be represented by an overlayfs whiteout. + # Remove whiteouts only when they hide an image-owned init script. + for whiteout in "$UPPER_DIR"/init.d/.wh.*; do + [ -e "$whiteout" ] || continue + whiteout_name=${whiteout##*/} + case "$whiteout_name" in + .wh..wh..opq) + rm -f "$whiteout" + ;; + .wh.*) + script_name=${whiteout_name#.wh.} + [ -f "/etc/init.d/$script_name" ] && rm -f "$whiteout" + ;; + esac + done + fi + if {OVERLAYFS_ETC_EXPOSE_LOWER}; then mkdir -p $LOWER_DIR diff --git a/meta-opencentauri/recipes-core/swu-flasher/files/readonly-config-reset b/meta-opencentauri/recipes-core/swu-flasher/files/readonly-config-reset index ace52a1f..66472d38 100644 --- a/meta-opencentauri/recipes-core/swu-flasher/files/readonly-config-reset +++ b/meta-opencentauri/recipes-core/swu-flasher/files/readonly-config-reset @@ -9,6 +9,15 @@ backup_file="$KLIPPER_CONFIG_BACKUP_DIR/readonly-$backup_timestamp.tar.gz" mkdir -p "$KLIPPER_CONFIG_BACKUP_DIR" tar -czf "$backup_file" /etc/klipper/config/*-readonly rm -rf /data/overlay-etc/upper/klipper/config/*-readonly + +# These package-owned scripts have changes in this release. The new rootfs +# performs the general image-owned init-script cleanup before mounting /etc; +# keep this focused pre-flash cleanup for launch scripts changed by this update. +rm -f \ + /data/overlay-etc/upper/init.d/moonraker \ + /data/overlay-etc/upper/init.d/ustreamer \ + /data/overlay-etc/upper/init.d/zram + sync echo 3 > /proc/sys/vm/drop_caches -echo "Readonly configs reset to default state. A reboot may be required for files to reappear." \ No newline at end of file +echo "Readonly configs and package-owned launch scripts reset to image defaults. A reboot may be required for files to reappear." From 4831b0a1c6452586011e9c73ef339d8def6a9333 Mon Sep 17 00:00:00 2001 From: Paul Swenson Date: Sat, 18 Jul 2026 10:51:52 -0400 Subject: [PATCH 03/20] ustreamer: reduce camera streamer memory use Apply the Centauri-focused uStreamer memory reductions and make the production camera service run at 864x480, camera MJPEG, and 8 FPS. The patch bounds thread stacks and frame backlog, avoids retaining the blank RGB canvas, starts generic frame buffers smaller, borrows JPEG buffers safely for HTTP delivery, and retries with MMAP when a camera rejects USERPTR capture. On Centauri with an active 864x480/8 FPS client, PSS fell from 4.67 MiB to 3.08 MiB and RSS from 5.76 MiB to 4.14 MiB. Snapshot and live video remain functional. Tested: CC1 eMMC build Tested: flashed CC1; snapshot and 8 FPS live stream --- .../files/0002-reduce-memory-usage.patch | 371 ++++++++++++++++++ .../ustreamer/files/ustreamer-init-d | 4 +- .../recipes-apps/ustreamer/ustreamer_6.55.bb | 1 + 3 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 meta-opencentauri/recipes-apps/ustreamer/files/0002-reduce-memory-usage.patch diff --git a/meta-opencentauri/recipes-apps/ustreamer/files/0002-reduce-memory-usage.patch b/meta-opencentauri/recipes-apps/ustreamer/files/0002-reduce-memory-usage.patch new file mode 100644 index 00000000..d50b8a4b --- /dev/null +++ b/meta-opencentauri/recipes-apps/ustreamer/files/0002-reduce-memory-usage.patch @@ -0,0 +1,371 @@ +From 144318c470cb5512227f24d217a7539de47be201 Mon Sep 17 00:00:00 2001 +From: Paul Swenson +Date: Fri, 10 Jul 2026 10:15:31 -0400 +Subject: [PATCH] ustreamer: reduce camera streamer memory use + +The Centauri Carbon camera service shares a 128 MiB system with the +printer-control stack, so its steady-state frame buffering matters. At +864x480 camera-MJPEG and 8 FPS on carbon2u, the stock ustreamer measured +4.67 MiB PSS and 5.76 MiB RSS. This change measured 3.17 MiB PSS and +4.30 MiB RSS while snapshots and a live stream remained functional. + +Reduce long-lived allocations without changing the service architecture: + +* cap internal thread stacks; +* retain only the newest JPEG in the streamer backlog and let HTTP clients + borrow a frame while it remains valid; +* avoid allocating a permanent RGB canvas for the blank frame; +* start generic frame buffers smaller and grow them on demand; and +* use bounded USERPTR buffers when supported, with an MMAP retry when a UVC + camera rejects USERPTR. + +The CC2 UVC camera rejects USERPTR queueing, so MMAP remains the validated +capture method on this device. The fallback keeps the safer low-allocation +default for cameras that do support USERPTR without sacrificing operation on +the deployed camera. + +Signed-off-by: Paul Swenson +Upstream-Status: Pending [Not submitted to upstream yet] +--- + src/libs/capture.c | 37 ++++++++++++++++++++++++++++--- + src/libs/frame.c | 44 +++++++++++++++++++++++++++++++++++-- + src/libs/frame.h | 3 +++ + src/libs/frametext.c | 1 + + src/libs/queue.c | 2 +- + src/libs/threading.h | 36 +++++++++++++++++++++++++++++- + src/ustreamer/blank.c | 1 + + src/ustreamer/encoders/hw/encoder.c | 4 +++- + src/ustreamer/options.c | 2 +- + src/ustreamer/stream.c | 13 ++++++++--- + 10 files changed, 131 insertions(+), 12 deletions(-) + +diff --git a/src/libs/capture.c b/src/libs/capture.c +index ee2021e17dd23083d45dd7a37678efff1adcc817..2614e117120d1a9fdb1c109c62758d526873cd71 100644 +--- a/src/libs/capture.c ++++ b/src/libs/capture.c +@@ -94,6 +94,8 @@ static int _capture_open_dv_timings(us_capture_s *cap, bool apply); + static int _capture_open_format(us_capture_s *cap, bool first); + static void _capture_open_hw_fps(us_capture_s *cap); + static void _capture_open_jpeg_quality(us_capture_s *cap); ++static uz _capture_get_userptr_buffer_size(const us_capture_runtime_s *run); ++static int _capture_open_impl(us_capture_s *cap); + static int _capture_open_io_method(us_capture_s *cap); + static int _capture_open_io_method_mmap(us_capture_s *cap); + static int _capture_open_io_method_userptr(us_capture_s *cap); +@@ -144,7 +146,7 @@ us_capture_s *us_capture_init(void) { + cap->format = V4L2_PIX_FMT_YUYV; + cap->jpeg_quality = 80; + cap->standard = V4L2_STD_UNKNOWN; +- cap->io_method = V4L2_MEMORY_MMAP; ++ cap->io_method = V4L2_MEMORY_USERPTR; + cap->n_bufs = us_get_cores_available() + 1; + cap->min_frame_size = 128; + cap->timeout = 1; +@@ -185,6 +187,27 @@ int us_capture_parse_io_method(const char *str) { + } + + int us_capture_open(us_capture_s *cap) { ++ const enum v4l2_memory requested_io_method = cap->io_method; ++ int result = _capture_open_impl(cap); ++ ++ if (result >= 0) { ++ return result; ++ } ++ ++ if (requested_io_method != V4L2_MEMORY_USERPTR) { ++ return result; ++ } ++ ++ _LOG_INFO("USERPTR capture failed, retrying with MMAP"); ++ cap->io_method = V4L2_MEMORY_MMAP; ++ result = _capture_open_impl(cap); ++ if (result < 0) { ++ cap->io_method = requested_io_method; ++ } ++ return result; ++} ++ ++static int _capture_open_impl(us_capture_s *cap) { + us_capture_runtime_s *const run = cap->run; + + if (access(cap->path, R_OK | W_OK) < 0) { +@@ -1009,12 +1032,11 @@ static int _capture_open_io_method_userptr(us_capture_s *cap) { + US_CALLOC(run->bufs, req.count); + + const uint page_size = getpagesize(); +- const uint buf_size = us_align_size(run->raw_size, page_size); ++ const uint buf_size = _capture_get_userptr_buffer_size(run); + + for (run->n_bufs = 0; run->n_bufs < req.count; ++run->n_bufs) { + us_capture_hwbuf_s *hw = &run->bufs[run->n_bufs]; + US_A((hw->raw.data = aligned_alloc(page_size, buf_size)) != NULL); +- memset(hw->raw.data, 0, buf_size); + hw->raw.allocated = buf_size; + if (run->capture_mplane) { + US_CALLOC(hw->buf.m.planes, VIDEO_MAX_PLANES); +@@ -1023,6 +1045,15 @@ static int _capture_open_io_method_userptr(us_capture_s *cap) { + return 0; + } + ++static uz _capture_get_userptr_buffer_size(const us_capture_runtime_s *run) { ++ // The driver-reported sizeimage is much larger than the actual MJPEG payload ++ // we observe on this target. Cap USERPTR to keep the resident capture buffer ++ // small while still leaving a conservative safety margin. ++ const uz raw_size = run->raw_size; ++ const uz cap_size = 256 * 1024UL; ++ return us_align_size(US_MIN(raw_size, cap_size), getpagesize()); ++} ++ + static int _capture_open_queue_buffers(us_capture_s *cap) { + us_capture_runtime_s *const run = cap->run; + +diff --git a/src/libs/frame.c b/src/libs/frame.c +index a770969e9ad7134e2c98328c9b906782090aafcf..222eb06cd7e4e803d429c6c54c4152d2f1a75acf 100644 +--- a/src/libs/frame.c ++++ b/src/libs/frame.c +@@ -35,27 +35,67 @@ + us_frame_s *us_frame_init(void) { + us_frame_s *frame; + US_CALLOC(frame, 1); +- us_frame_realloc_data(frame, 32 * 1024); ++ // Most live JPEG buffers settle well below 32 KiB in this deployment, ++ // so keep the startup floor small and grow only when needed. ++ us_frame_realloc_data(frame, 8 * 1024); + frame->dma_fd = -1; + return frame; + } + + void us_frame_destroy(us_frame_s *frame) { +- US_DELETE(frame->data, free); ++ if (!frame->external_data) { ++ US_DELETE(frame->data, free); ++ } + free(frame); + } + + void us_frame_realloc_data(us_frame_s *frame, uz size) { ++ if (frame->external_data) { ++ u8 *owned; ++ US_CALLOC(owned, size); ++ if (frame->data != NULL && frame->used > 0) { ++ memcpy(owned, frame->data, frame->used); ++ } ++ frame->data = owned; ++ frame->external_data = false; ++ frame->allocated = size; ++ return; ++ } + if (frame->allocated < size) { + US_REALLOC(frame->data, size); + frame->allocated = size; + } + } + ++void us_frame_release_data(us_frame_s *frame) { ++ if (frame->external_data) { ++ frame->data = NULL; ++ frame->used = 0; ++ frame->allocated = 0; ++ frame->external_data = false; ++ return; ++ } ++ ++ US_DELETE(frame->data, free); ++ frame->used = 0; ++ frame->allocated = 0; ++} ++ ++void us_frame_set_external_data(us_frame_s *frame, const u8 *data, uz size) { ++ if (!frame->external_data) { ++ US_DELETE(frame->data, free); ++ } ++ frame->data = (u8*)data; ++ frame->used = size; ++ frame->allocated = 0; ++ frame->external_data = true; ++} ++ + void us_frame_set_data(us_frame_s *frame, const u8 *data, uz size) { + us_frame_realloc_data(frame, size); + memcpy(frame->data, data, size); + frame->used = size; ++ frame->external_data = false; + } + + void us_frame_append_data(us_frame_s *frame, const u8 *data, uz size) { +diff --git a/src/libs/frame.h b/src/libs/frame.h +index 74c45512c6fa1fd0755b621483294ffb61e0292d..a24ce4bf994edf24748ed1e1cbc0e736f0641df7 100644 +--- a/src/libs/frame.h ++++ b/src/libs/frame.h +@@ -49,6 +49,7 @@ typedef struct { + uz used; + uz allocated; + int dma_fd; ++ bool external_data; + + US_FRAME_META_DECLARE; + } us_frame_s; +@@ -102,6 +103,8 @@ us_frame_s *us_frame_init(void); + void us_frame_destroy(us_frame_s *frame); + + void us_frame_realloc_data(us_frame_s *frame, uz size); ++void us_frame_release_data(us_frame_s *frame); ++void us_frame_set_external_data(us_frame_s *frame, const u8 *data, uz size); + void us_frame_set_data(us_frame_s *frame, const u8 *data, uz size); + void us_frame_append_data(us_frame_s *frame, const u8 *data, uz size); + +diff --git a/src/libs/frametext.c b/src/libs/frametext.c +index d5adfc73765558dd3f7e0b5052dec9c4678b1041..9daa61f02666a588c3d63378aeede9d1faee6393 100644 +--- a/src/libs/frametext.c ++++ b/src/libs/frametext.c +@@ -92,6 +92,7 @@ void us_frametext_draw(us_frametext_s *ft, const char *text, uint width, uint he + if ( + frame->width == width && frame->height == height + && ft->text != NULL && !strcmp(ft->text, text) ++ && frame->data != NULL && frame->used > 0 + ) { + return; + } +diff --git a/src/libs/queue.c b/src/libs/queue.c +index d72f656a7fdfde16c6e7f83202f8914a8cd808c7..854ecb11556a441b632a8365886e16ca9c22f5e1 100644 +--- a/src/libs/queue.c ++++ b/src/libs/queue.c +@@ -107,5 +107,5 @@ bool us_queue_is_empty(us_queue_s *q) { + US_MUTEX_LOCK(q->mutex); + const uint size = q->size; + US_MUTEX_UNLOCK(q->mutex); +- return (bool)(q->capacity - size); ++ return (size == 0); + } +diff --git a/src/libs/threading.h b/src/libs/threading.h +index 2f161af24c9c78702a6eaca2ad6438f3df7783f2..be145e1407259bf9a9088ecb8295034248cd0988 100644 +--- a/src/libs/threading.h ++++ b/src/libs/threading.h +@@ -46,7 +46,41 @@ + # define US_THREAD_NAME_SIZE ((uz)16) + #endif + +-#define US_THREAD_CREATE(x_tid, x_func, x_arg) US_A(!pthread_create(&(x_tid), NULL, (x_func), (x_arg))) ++#ifndef US_THREAD_STACK_SIZE ++# define US_THREAD_STACK_SIZE (32 * 1024UL) ++#endif ++ ++INLINE int us_thread_create(pthread_t *tid, void *(*func)(void*), void *arg) { ++#if (US_THREAD_STACK_SIZE > 0) ++ pthread_attr_t attr; ++ US_A(!pthread_attr_init(&attr)); ++ ++ // These threads are long-lived but have shallow call stacks, so cap the ++ // default pthread stack reservation to keep memory overhead predictable. ++ uz stack_size = US_THREAD_STACK_SIZE; ++ const uz min_stack_size = (uz)PTHREAD_STACK_MIN; ++ if (stack_size < min_stack_size) { ++ stack_size = min_stack_size; ++ } ++# if defined(_SC_PAGESIZE) ++ { ++ const long page_size = sysconf(_SC_PAGESIZE); ++ if (page_size > 0 && (stack_size % (uz)page_size) != 0) { ++ stack_size += (uz)page_size - (stack_size % (uz)page_size); ++ } ++ } ++# endif ++ US_A(!pthread_attr_setstacksize(&attr, stack_size)); ++ ++ const int retval = pthread_create(tid, &attr, func, arg); ++ US_A(!pthread_attr_destroy(&attr)); ++ return retval; ++#else ++ return pthread_create(tid, NULL, func, arg); ++#endif ++} ++ ++#define US_THREAD_CREATE(x_tid, x_func, x_arg) US_A(!us_thread_create(&(x_tid), (x_func), (x_arg))) + #define US_THREAD_JOIN(x_tid) US_A(!pthread_join((x_tid), NULL)) + + #ifdef WITH_PTHREAD_NP +diff --git a/src/ustreamer/blank.c b/src/ustreamer/blank.c +index 09b1b0008c406d43ffba1d78b7f1664b749d63ea..50fb5af947acabeee2b78e8be9746d3ab8a13b43 100644 +--- a/src/ustreamer/blank.c ++++ b/src/ustreamer/blank.c +@@ -43,6 +43,7 @@ us_blank_s *us_blank_init(void) { + void us_blank_draw(us_blank_s *blank, const char *text, uint width, uint height) { + us_frametext_draw(blank->ft, text, width, height); + us_cpu_encoder_compress(blank->raw, blank->jpeg, 95); ++ us_frame_release_data(blank->raw); + } + + void us_blank_destroy(us_blank_s *blank) { +diff --git a/src/ustreamer/encoders/hw/encoder.c b/src/ustreamer/encoders/hw/encoder.c +index 7e35495954e16d6ca5473c6d09b00220784c7c6c..32791083f107a7196885325bf3b5cd25bf7d0e2c 100644 +--- a/src/ustreamer/encoders/hw/encoder.c ++++ b/src/ustreamer/encoders/hw/encoder.c +@@ -69,7 +69,9 @@ void _copy_plus_huffman(const us_frame_s *src, us_frame_s *dest) { + us_frame_append_data(dest, src_ptr, src->used - paste); + + } else { +- us_frame_set_data(dest, src->data, src->used); ++ // Borrow the camera JPEG buffer directly when it is already complete. ++ // The caller keeps the capture buffer alive until after exposure. ++ us_frame_set_external_data(dest, src->data, src->used); + } + + us_frame_encoding_end(dest); +diff --git a/src/ustreamer/options.c b/src/ustreamer/options.c +index 048e6c50854edbf369c39e9ca4128205364d4993..2d36e1277c0b6ba0768562f41f2da7c52f0e7ae4 100644 +--- a/src/ustreamer/options.c ++++ b/src/ustreamer/options.c +@@ -703,7 +703,7 @@ static void _help( + SAY(" Available: %s; default: disabled.\n", US_STANDARDS_STR); + SAY(" -I|--io-method ───────────── Set V4L2 IO method (see kernel documentation)."); + SAY(" Changing of this parameter may increase the performance. Or not."); +- SAY(" Available: %s; default: MMAP.\n", US_IO_METHODS_STR); ++ SAY(" Available: %s; default: USERPTR, with MMAP fallback if needed.\n", US_IO_METHODS_STR); + SAY(" -f|--desired-fps ──────────────── Desired FPS. Default: maximum possible.\n"); + SAY(" -z|--min-frame-size ───────────── Drop frames smaller than this limit. Useful if the device"); + SAY(" produces small-sized garbage frames. Default: %zu bytes.\n", cap->min_frame_size); +diff --git a/src/ustreamer/stream.c b/src/ustreamer/stream.c +index 9ae94e42b35d355df43d449a595810eefb06d501..dd28e3e64589d916e96f37d67fbeb0feaf8f3be8 100644 +--- a/src/ustreamer/stream.c ++++ b/src/ustreamer/stream.c +@@ -58,6 +58,11 @@ + #endif + + ++#ifndef US_HTTP_JPEG_RING_SIZE ++# define US_HTTP_JPEG_RING_SIZE 1 ++#endif ++ ++ + typedef struct { + pthread_t tid; + us_capture_s *cap; +@@ -104,7 +109,9 @@ us_stream_s *us_stream_init(us_capture_s *cap, us_encoder_s *enc) { + http->drm_fpsi = us_fpsi_init("DRM", true); + # endif + http->h264_fpsi = us_fpsi_init("H264", true); +- US_RING_INIT_WITH_ITEMS(http->jpeg_ring, 4, us_frame_init); ++ // Keep a minimal HTTP JPEG backlog: the refresher copies the newest frame ++ // into the stable exposed buffer, so extra slots mostly retain stale copies. ++ US_RING_INIT_WITH_ITEMS(http->jpeg_ring, US_HTTP_JPEG_RING_SIZE, us_frame_init); + atomic_init(&http->has_clients, false); + atomic_init(&http->snapshot_requested, 0); + atomic_init(&http->last_req_ts, 0); +@@ -331,8 +338,6 @@ static void *_jpeg_thread(void *v_ctx) { + us_encoder_job_s *const job = wr->job; + + if (job->hw != NULL) { +- us_capture_hwbuf_decref(job->hw); +- job->hw = NULL; + if (wr->job_failed) { + // pass + } else if (wr->job_timely) { +@@ -345,6 +350,8 @@ static void *_jpeg_thread(void *v_ctx) { + } else { + US_LOG_PERF("JPEG: ----- Encoded JPEG dropped; worker=%s", wr->name); + } ++ us_capture_hwbuf_decref(job->hw); ++ job->hw = NULL; + } + + us_capture_hwbuf_s *hw = _get_latest_hw(ctx->q); diff --git a/meta-opencentauri/recipes-apps/ustreamer/files/ustreamer-init-d b/meta-opencentauri/recipes-apps/ustreamer/files/ustreamer-init-d index b513fcfe..6c3cbcfc 100644 --- a/meta-opencentauri/recipes-apps/ustreamer/files/ustreamer-init-d +++ b/meta-opencentauri/recipes-apps/ustreamer/files/ustreamer-init-d @@ -9,7 +9,9 @@ ### END INIT INFO USTREAMER_EXEC="/usr/bin/ustreamer" -USTREAMER_ARGS="-r 864x480 -c HW -m MJPEG -b 1 -w 1 -f 10 -l --host=0.0.0.0 --allow-origin=*" +# The CC2 deployment uses camera-provided MJPEG at 864x480. Eight FPS is the +# measured stable delivery rate with the bounded frame backlog. +USTREAMER_ARGS="-r 864x480 -c HW -m MJPEG -b 1 -w 1 -f 8 -l --host=0.0.0.0 --allow-origin=*" PIDFILE="/var/run/ustreamer.pid" case "$1" in diff --git a/meta-opencentauri/recipes-apps/ustreamer/ustreamer_6.55.bb b/meta-opencentauri/recipes-apps/ustreamer/ustreamer_6.55.bb index 5c5ea395..73abeb9d 100644 --- a/meta-opencentauri/recipes-apps/ustreamer/ustreamer_6.55.bb +++ b/meta-opencentauri/recipes-apps/ustreamer/ustreamer_6.55.bb @@ -18,6 +18,7 @@ INITSCRIPT_PARAMS = "defaults 97 5" SRC_URI = " \ git://github.com/pikvm/ustreamer.git;protocol=https;branch=master \ file://0001-retry-http-bind.patch \ + file://0002-reduce-memory-usage.patch \ file://ustreamer-init-d \ " SRCREV = "88460b72e191035d04355e25106af817cbfe069e" From 78ad740c5245a646c799fb2aff42218fcef64e04 Mon Sep 17 00:00:00 2001 From: Paul Swenson Date: Sat, 18 Jul 2026 14:57:05 -0400 Subject: [PATCH 04/20] overlayfs: escape shell expansions in preinit template The preinit script is rendered with Python str.format(). Escape the init-script cleanup parameter expansions so the generated shell script retains them instead of treating them as format fields. --- meta-opencentauri/images/files/overlayfs-etc-preinit.sh.in | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/meta-opencentauri/images/files/overlayfs-etc-preinit.sh.in b/meta-opencentauri/images/files/overlayfs-etc-preinit.sh.in index cbcc48b0..34fc34d9 100644 --- a/meta-opencentauri/images/files/overlayfs-etc-preinit.sh.in +++ b/meta-opencentauri/images/files/overlayfs-etc-preinit.sh.in @@ -67,7 +67,7 @@ then if [ -d "$UPPER_DIR/init.d" ]; then for upper_script in "$UPPER_DIR"/init.d/*; do [ -f "$upper_script" ] || continue - script_name=${upper_script##*/} + script_name=${{upper_script##*/}} [ -f "/etc/init.d/$script_name" ] || continue rm -f "$upper_script" done @@ -76,13 +76,13 @@ then # Remove whiteouts only when they hide an image-owned init script. for whiteout in "$UPPER_DIR"/init.d/.wh.*; do [ -e "$whiteout" ] || continue - whiteout_name=${whiteout##*/} + whiteout_name=${{whiteout##*/}} case "$whiteout_name" in .wh..wh..opq) rm -f "$whiteout" ;; .wh.*) - script_name=${whiteout_name#.wh.} + script_name=${{whiteout_name#.wh.}} [ -f "/etc/init.d/$script_name" ] && rm -f "$whiteout" ;; esac From b5d80fc7c2ac06e64b95b7bdb46880afa432b465 Mon Sep 17 00:00:00 2001 From: Paul Swenson Date: Sat, 18 Jul 2026 14:57:05 -0400 Subject: [PATCH 05/20] kernel: reduce runtime memory footprint on Centauri Disable unused debug, tracing, cgroup, container, asynchronous I/O, and networking facilities in the CC1 kernel profile while retaining required printer hardware interfaces, display, camera, RPMsg, swap, and zram support. --- .../runtime-memory.cfg | 97 +++++++++++++++++++ .../linux/linux-mainline_%.bbappend | 1 + 2 files changed, 98 insertions(+) create mode 100644 meta-opencentauri/recipes-kernel/linux/linux-mainline/elegoo-centauri-carbon1/runtime-memory.cfg diff --git a/meta-opencentauri/recipes-kernel/linux/linux-mainline/elegoo-centauri-carbon1/runtime-memory.cfg b/meta-opencentauri/recipes-kernel/linux/linux-mainline/elegoo-centauri-carbon1/runtime-memory.cfg new file mode 100644 index 00000000..5b18ea94 --- /dev/null +++ b/meta-opencentauri/recipes-kernel/linux/linux-mainline/elegoo-centauri-carbon1/runtime-memory.cfg @@ -0,0 +1,97 @@ +# Runtime-memory profile for the 128 MiB Centauri Carbon 1. +# +# Keep this as the final machine fragment so generic arm/defconfig settings do +# not re-enable facilities that are not part of the appliance contract. + +# Release diagnostics and runtime instrumentation. Developers should use a +# separate debug kernel rather than carrying these tables in every printer. +# Linux 6.6 only exposes KALLSYMS when EXPERT is enabled. EXPERT selects the +# DEBUG_KERNEL menu, but not its instrumentation: retain the menu symbol so +# that the material KALLSYMS tables can be removed and explicitly keep its +# debug features off below. +CONFIG_EXPERT=y +CONFIG_DEBUG_KERNEL=y +# CONFIG_DEBUG_MISC is not set +# CONFIG_DEBUG_BUGVERBOSE is not set +# CONFIG_SLUB_DEBUG is not set +# CONFIG_IKCONFIG is not set +# CONFIG_IKCONFIG_PROC is not set +# CONFIG_KALLSYMS is not set +# CONFIG_FTRACE is not set +# CONFIG_EVENT_TRACING is not set +# CONFIG_TRACING is not set +# CONFIG_UPROBES is not set +# CONFIG_UPROBE_EVENTS is not set +# CONFIG_PERF_EVENTS is not set +# CONFIG_TASKSTATS is not set +# CONFIG_TASK_DELAY_ACCT is not set +# CONFIG_TASK_XACCT is not set +# CONFIG_TASK_IO_ACCOUNTING is not set +# CONFIG_SCHED_INFO is not set + +# No containers, sandboxing, or general-purpose asynchronous I/O are used on +# the appliance image. +# SCHED_AUTOGROUP selects the otherwise unused cgroup scheduler hierarchy. +# CONFIG_SCHED_AUTOGROUP is not set +# CONFIG_CGROUPS is not set +# CONFIG_CGROUP_SCHED is not set +# CONFIG_CGROUP_NET_CLASSID is not set +# CONFIG_FAIR_GROUP_SCHED is not set +# Basic networking selects the small core BPF interpreter in Linux 6.6. The +# syscall and JIT remain disabled by kernel-size-reduction.cfg. +# CONFIG_IO_URING is not set +# CONFIG_FANOTIFY is not set +# CONFIG_HIGHMEM is not set +# CONFIG_POSIX_MQUEUE is not set +# CONFIG_SYSVIPC is not set +# CONFIG_SECCOMP is not set +# CONFIG_SECCOMP_FILTER is not set + +# The appliance networking contract is IPv4 TCP/UDP over Ethernet or Wi-Fi; +# it has no firewall, bridge, VLAN, or IPv6 requirement. +# CONFIG_IPV6 is not set +# CONFIG_NETFILTER is not set +# CONFIG_NETFILTER_ADVANCED is not set +# CONFIG_NF_CONNTRACK is not set +# CONFIG_BRIDGE is not set +# CONFIG_VLAN_8021Q is not set + +# The CC1 is a D1/T113 board. Generic arm/defconfig enables several older +# Allwinner pinctrl/PWM/I2C drivers that can never bind to this device tree. +# CONFIG_PINCTRL_SUN4I_A10 is not set +# CONFIG_PINCTRL_SUN5I is not set +# CONFIG_PINCTRL_SUN6I_A31 is not set +# CONFIG_PINCTRL_SUN6I_A31_R is not set +# CONFIG_PINCTRL_SUN8I_A23 is not set +# CONFIG_PINCTRL_SUN8I_A23_R is not set +# CONFIG_PINCTRL_SUN8I_A33 is not set +# CONFIG_PINCTRL_SUN8I_A83T is not set +# CONFIG_PINCTRL_SUN8I_A83T_R is not set +# CONFIG_PINCTRL_SUN8I_H3 is not set +# CONFIG_PINCTRL_SUN8I_H3_R is not set +# CONFIG_PINCTRL_SUN9I_A80 is not set +# CONFIG_PINCTRL_SUN9I_A80_R is not set +# CONFIG_PWM_SUN4I is not set + +# Explicitly retain the printer's diagnostics and hardware interfaces. +CONFIG_DEVMEM=y +CONFIG_GPIOLIB=y +CONFIG_GPIO_SYSFS=y +CONFIG_GPIO_CDEV=y +CONFIG_GPIO_CDEV_V1=y +CONFIG_I2C_CHARDEV=y +# The D1/T113 device tree uses an A31-compatible I2C node. Linux binds that +# node through this legacy-named driver and exposes the printer's /dev/i2c-2. +CONFIG_I2C_MV64XXX=y +CONFIG_SPI_SPIDEV=y +CONFIG_USB_STORAGE=y +CONFIG_MEDIA_SUPPORT=m +CONFIG_USB_VIDEO_CLASS=m +CONFIG_DRM=y +CONFIG_DRM_FBDEV_EMULATION=y +CONFIG_RPMSG=y +CONFIG_RPMSG_TTY=m +CONFIG_REMOTEPROC=y +CONFIG_SUNXI_R528_HIFI4_REMOTEPROC=m +CONFIG_SWAP=y +CONFIG_ZRAM=m diff --git a/meta-opencentauri/recipes-kernel/linux/linux-mainline_%.bbappend b/meta-opencentauri/recipes-kernel/linux/linux-mainline_%.bbappend index 92041866..1efe52cc 100644 --- a/meta-opencentauri/recipes-kernel/linux/linux-mainline_%.bbappend +++ b/meta-opencentauri/recipes-kernel/linux/linux-mainline_%.bbappend @@ -16,6 +16,7 @@ SRC_URI:append:elegoo-centauri-carbon1 = " \ file://squashfs-overlayfs.cfg \ file://kernel-size-reduction.cfg \ file://usb-net-adapters.cfg \ + file://runtime-memory.cfg \ file://0001-dt-bindings-pwm-Add-binding-for-Allwinner-D1-T113-S3.patch \ file://0002-pwm-Add-Allwinner-s-D1-T113-S3-R329-SoCs-PWM-support.patch \ file://0003-riscv-dts-allwinner-d1-Add-pwm-node.patch \ From 4edb6bed9fbe0a0510530dc14db64f41ba694573 Mon Sep 17 00:00:00 2001 From: Paul Swenson Date: Sat, 18 Jul 2026 14:57:05 -0400 Subject: [PATCH 06/20] python: reduce Klipper and Moonraker memory overhead Run both services with optimized Python bytecode and without debug location ranges, ship optimized target bytecode, and cap glibc allocator arenas for the 128 MiB target. --- .../recipes-apps/klipper/files/klipper-init-d | 5 ++++- .../recipes-apps/klipper/kalico_2026.02.00.bb | 7 ++++--- .../recipes-apps/moonraker/files/moonraker-init-d | 8 +++++++- .../recipes-apps/moonraker/moonraker_0.10.0.bb | 6 +++++- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/meta-opencentauri/recipes-apps/klipper/files/klipper-init-d b/meta-opencentauri/recipes-apps/klipper/files/klipper-init-d index 5d0bc2e9..7267a7ee 100644 --- a/meta-opencentauri/recipes-apps/klipper/files/klipper-init-d +++ b/meta-opencentauri/recipes-apps/klipper/files/klipper-init-d @@ -10,7 +10,7 @@ KLIPPY_EXEC="/usr/bin/python3" KLIPPER_PRINTER_CONFIG="/etc/klipper/config/printer.cfg" -KLIPPY_ARGS="-O /usr/share/klipper/klippy/klippy.py $KLIPPER_PRINTER_CONFIG -l /board-resource/klippy.log -a /run/klippy.sock" +KLIPPY_ARGS="-O -X no_debug_ranges /usr/share/klipper/klippy/klippy.py $KLIPPER_PRINTER_CONFIG -l /board-resource/klippy.log -a /run/klippy.sock" PIDFILE="/var/run/klipper.pid" KLIPPER_VAR_CONFIG_BUILDER="/usr/bin/build-klipper-var-config" KLIPPER_CONFIG_VERSION="0.0.8-2" @@ -18,6 +18,9 @@ KLIPPER_CONFIG_VERSION_FILE="/etc/klipper-config.ver" KLIPPER_CONFIG_BACKUP_DIR="/etc/klipper/config/config-backups" export TMPDIR="/user-resource/.tmp" +# Limit glibc's per-thread arena retention. This is particularly effective +# for Klipper's threaded Python and native-extension workload on Centauri. +export MALLOC_ARENA_MAX=1 ensure_klipper_config_version() { current_version="" 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..143240c2 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.bb +++ b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.bb @@ -17,7 +17,7 @@ SRC_URI += " \ file://client.cfg \ " -inherit python3-dir update-rc.d +inherit python3-dir python3native update-rc.d RDEPENDS:${PN} = " \ python3 \ @@ -82,9 +82,10 @@ do_install() { sed -i 's/APP_NAME = "Kalico"/APP_NAME = "${DISTRO_NAME}"/' ${D}${datadir}/klipper/klippy/__init__.py echo "Release - ${DISTRO_VERSION}" > ${D}${datadir}/klipper/klippy/.version - # Remove any .pyc files to avoid TMPDIR references + # Remove any source-tree caches, then ship target-version optimized + # bytecode without debug ranges. Stripping ${D} avoids TMPDIR references. find ${D} -name '*.pyc' -delete - find ${D} -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null || true + PYTHONNODEBUGRANGES=1 ${PYTHON} -O -m compileall -q -s ${D} ${D}${datadir}/klipper # Make config_examples to suppress moonraker warnings install -d ${D}${datadir}/klipper/config diff --git a/meta-opencentauri/recipes-apps/moonraker/files/moonraker-init-d b/meta-opencentauri/recipes-apps/moonraker/files/moonraker-init-d index 73ef744e..4fe984e2 100644 --- a/meta-opencentauri/recipes-apps/moonraker/files/moonraker-init-d +++ b/meta-opencentauri/recipes-apps/moonraker/files/moonraker-init-d @@ -9,11 +9,17 @@ ### END INIT INFO MOONRAKER_EXEC="/usr/bin/python3" -MOONRAKER_ARGS="-m moonraker.moonraker -d /etc/klipper -l /board-resource/moonraker.log -u /run/moonraker.sock" +# Match Klipper's optimization level and omit optional debug location ranges. +# This reduces Python code-object overhead without changing Moonraker's +# enabled component set or public API. +MOONRAKER_ARGS="-O -X no_debug_ranges -m moonraker.moonraker -d /etc/klipper -l /board-resource/moonraker.log -u /run/moonraker.sock" PIDFILE="/var/run/moonraker.pid" export PYTHONPATH="/usr/share/moonraker" export TMPDIR="/user-resource/.tmp" +# Limit glibc's per-thread arena retention. On the live Centauri this reduced +# Klipper PSS by about 3 MiB while the printer API remained healthy. +export MALLOC_ARENA_MAX=1 case "$1" in start) diff --git a/meta-opencentauri/recipes-apps/moonraker/moonraker_0.10.0.bb b/meta-opencentauri/recipes-apps/moonraker/moonraker_0.10.0.bb index 5770b60c..5a982ff8 100644 --- a/meta-opencentauri/recipes-apps/moonraker/moonraker_0.10.0.bb +++ b/meta-opencentauri/recipes-apps/moonraker/moonraker_0.10.0.bb @@ -23,7 +23,7 @@ S = "${WORKDIR}/git" PR = "r1" -inherit python3-dir update-rc.d +inherit python3-dir python3native update-rc.d DEPENDS = " \ python3-native \ @@ -70,6 +70,10 @@ do_install() { # Install moonraker python package install -d ${D}${datadir}/moonraker cp -r ${S}/moonraker ${D}${datadir}/moonraker/ + # Ship target-version optimized bytecode. Removing debug ranges makes + # code objects and their cache files smaller; -s keeps build paths out of + # tracebacks and avoids a TMPDIR-dependent cache. + PYTHONNODEBUGRANGES=1 ${PYTHON} -O -m compileall -q -s ${D} ${D}${datadir}/moonraker # Install default moonraker config install -d ${D}${sysconfdir}/klipper From c25ea7b6ecc6dffc9302226209cdec6c4c4a7308 Mon Sep 17 00:00:00 2001 From: Paul Swenson Date: Sat, 18 Jul 2026 14:57:05 -0400 Subject: [PATCH 07/20] kalico: bound input-shaper calibration memory Batch Welch PSD windows and retain acceleration samples as float32 with relative timestamps, preserving calibration results while avoiding capture-sized temporary arrays. --- ...ory-optimisation-in-shaper_calibrate.patch | 110 ++++++++++++++++++ .../klipper/kalico_2026.02.00.inc | 3 +- 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 meta-opencentauri/recipes-apps/klipper/files/0001-Memory-optimisation-in-shaper_calibrate.patch diff --git a/meta-opencentauri/recipes-apps/klipper/files/0001-Memory-optimisation-in-shaper_calibrate.patch b/meta-opencentauri/recipes-apps/klipper/files/0001-Memory-optimisation-in-shaper_calibrate.patch new file mode 100644 index 00000000..e157609b --- /dev/null +++ b/meta-opencentauri/recipes-apps/klipper/files/0001-Memory-optimisation-in-shaper_calibrate.patch @@ -0,0 +1,110 @@ +From 0e90d3d558a119af3239519e105778c8a5bd3d16 Mon Sep 17 00:00:00 2001 +From: James Turton +Date: Sat, 27 Jun 2026 10:50:56 +0200 +Subject: [PATCH] shaper_calibrate: bound Welch PSD memory use + +The previous Welch implementation materialised and transformed every +overlapping window at once. On the printer that makes its largest temporary +arrays proportional to the complete accelerometer capture. + +Accumulate the FFT energy in fixed 64-window batches instead. This preserves +the original PSD calculation while bounding the window and FFT intermediates. +Also pack the retained samples as float32 and keep timestamps relative to the +first sample, avoiding precision loss from large absolute timestamps. + +This intentionally does not change accelerometer capture or raw CSV writing: +it reduces the memory used while Kalico processes calibration data. + +Upstream-Status: Pending [OpenCentauri memory-constrained targets] + +--- + klippy/extras/shaper_calibrate.py | 49 ++++++++++++++++++++++-------- + 1 file changed, 35 insertions(+), 14 deletions(-) + +diff --git a/klippy/extras/shaper_calibrate.py b/klippy/extras/shaper_calibrate.py +index 230e2f58..457d3632 100644 +--- a/klippy/extras/shaper_calibrate.py ++++ b/klippy/extras/shaper_calibrate.py +@@ -147,25 +147,31 @@ class ShaperCalibrate: + window = np.kaiser(nfft, 6.0) + # Compensation for windowing loss + scale = 1.0 / (window**2).sum() ++ window = window[:, None] + + # Split into overlapping windows of size nfft + overlap = nfft // 2 + x = self._split_into_windows(x, nfft, overlap) ++ n_windows = x.shape[-1] ++ ++ # Accumulate the response over windows in small batches to keep ++ # peak memory usage low ++ psd = np.zeros(nfft // 2 + 1) ++ batch = 64 ++ for start in range(0, n_windows, batch): ++ win = x[:, start : start + batch] ++ # First detrend, then apply windowing function ++ win = window * (win - win.mean(axis=0)) ++ # Calculate frequency response for each window using FFT ++ result = np.fft.rfft(win, n=nfft, axis=0) ++ psd += (result.real**2 + result.imag**2).sum(axis=-1) + +- # First detrend, then apply windowing function +- x = window[:, None] * (x - np.mean(x, axis=0)) +- +- # Calculate frequency response for each window using FFT +- result = np.fft.rfft(x, n=nfft, axis=0) +- result = np.conjugate(result) * result +- result *= scale / fs ++ # Welch's algorithm: average response over windows ++ psd *= scale / (fs * n_windows) + # For one-sided FFT output the response must be doubled, except + # the last point for unpaired Nyquist frequency (assuming even nfft) + # and the 'DC' term (0 Hz) +- result[1:-1, :] *= 2.0 +- +- # Welch's algorithm: average response over windows +- psd = result.real.mean(axis=-1) ++ psd[1:-1] *= 2.0 + + # Calculate the frequency bins + freqs = np.fft.rfftfreq(nfft, 1.0 / fs) +@@ -177,14 +183,19 @@ class ShaperCalibrate: + return None + if isinstance(raw_values, np.ndarray): + data = raw_values ++ N = data.shape[0] ++ T = float(data[-1, 0]) - float(data[0, 0]) + else: + samples = raw_values.get_samples() + if not samples: + return None +- data = np.array(samples) ++ # Determine the sampling interval before converting timestamps ++ # to a compact single-precision representation. ++ N = len(samples) ++ T = samples[-1][0] - samples[0][0] ++ data = np.asarray(samples, dtype=np.float32) ++ del samples + +- N = data.shape[0] +- T = data[-1, 0] - data[0, 0] + SAMPLING_FREQ = N / T + # Round up to the nearest power of 2 for faster FFT + M = 1 << int(SAMPLING_FREQ * WINDOW_T_SEC - 1).bit_length() +@@ -199,6 +210,16 @@ class ShaperCalibrate: + return CalibrationData(fx, px + py + pz, px, py, pz) + + def process_accelerometer_data(self, data): ++ np = self.numpy ++ if not isinstance(data, np.ndarray): ++ samples = data.get_samples() ++ if samples: ++ # Keep timestamps relative to the first sample before packing ++ # into float32, preserving the sampling interval precision. ++ data = np.asarray(samples, dtype=np.float32) ++ data[0, 0] = 0.0 ++ data[-1, 0] = samples[-1][0] - samples[0][0] ++ del samples + calibration_data = self.background_process_exec( + self.calc_freq_response, (data,) + ) 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 5767cedc..c94c4234 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc +++ b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc @@ -12,10 +12,11 @@ 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://0001-Memory-optimisation-in-shaper_calibrate.patch \ " SRCREV = "fa59d011f425c5aef6f06abc08efe43f7f6c569d" -PR = "r7" +PR = "r8" S = "${WORKDIR}/git" From c851dcbb9ecb0e96dad2f76d5c8dc55d1866cdc1 Mon Sep 17 00:00:00 2001 From: Paul Swenson Date: Sat, 18 Jul 2026 14:57:05 -0400 Subject: [PATCH 08/20] rusty-shaper: add low-RAM input-shaper calibration Package the stripped GPL-3 Rusty calibrator and make it the selectable low-RAM Cosmos calibration path. Add asynchronous Moonraker reporting, Rusty/Classic selection, candidate configuration, and a non-disruptive Rusty workflow that leaves the camera and UI running. Keep Classic calibration as the fallback and skip load-cell calibration when a saved calibration already exists. --- .../images/opencentauri-image-base.bb | 1 + .../klipper/files/calibration.cfg | 116 ++++++++++- .../recipes-apps/klipper/files/shell.cfg | 5 + .../rusty-shaper/files/klippy-rusty-shaper | 63 ++++++ .../rusty-shaper/rusty-shaper_0.1.0.bb | 190 ++++++++++++++++++ .../files/build-klipper-var-config.sh | 8 +- .../config-manager/files/config_manager.py | 17 +- .../config-manager/files/default.conf | 7 +- 8 files changed, 394 insertions(+), 13 deletions(-) create mode 100644 meta-opencentauri/recipes-apps/rusty-shaper/files/klippy-rusty-shaper create mode 100644 meta-opencentauri/recipes-apps/rusty-shaper/rusty-shaper_0.1.0.bb diff --git a/meta-opencentauri/images/opencentauri-image-base.bb b/meta-opencentauri/images/opencentauri-image-base.bb index a2281ced..d556c88c 100644 --- a/meta-opencentauri/images/opencentauri-image-base.bb +++ b/meta-opencentauri/images/opencentauri-image-base.bb @@ -21,6 +21,7 @@ CORE_IMAGE_EXTRA_INSTALL += "\ rtw88 \ aic8800 \ kalico \ + rusty-shaper \ moonraker \ mainsail \ fluidd \ diff --git a/meta-opencentauri/recipes-apps/klipper/files/calibration.cfg b/meta-opencentauri/recipes-apps/klipper/files/calibration.cfg index 05cf425e..593f30fa 100644 --- a/meta-opencentauri/recipes-apps/klipper/files/calibration.cfg +++ b/meta-opencentauri/recipes-apps/klipper/files/calibration.cfg @@ -3,6 +3,11 @@ # UPDATES WILL RESET THIS FILE. YOUR EDITS WILL BE LOST WHEN UPDATING. # ----- +[gcode_macro M125] +description: Write a shaper-calibration status message to the UI and klippy.log. +gcode: + { action_respond_info(rawparams) } + [gcode_macro _CHECK_CALIBRATION_VARS] variable_shaper_calibrated: False variable_bed_mesh_calibrated: False @@ -88,9 +93,12 @@ gcode: {% set settings = printer["gcode_macro _COSMOS_SETTINGS"]|default({}) %} {% set hotend_temperature = settings.full_calibrate_hotend_temperature|int %} {% set bed_temperature = settings.full_calibrate_bed_temperature|int %} + {% set input_shaper = settings.input_shaper|default('rusty')|lower %} RESPOND TYPE=command MSG="action:prompt_begin Calibrate all" - RESPOND TYPE=command MSG="action:prompt_text The screen and camera will be disabled during resonance compensation calibration. When the calibration is complete, you will be prompted to save the updated configuration." + {% if input_shaper == 'classic' %} + RESPOND TYPE=command MSG="action:prompt_text The screen and camera will be disabled during resonance compensation calibration. When the calibration is complete, you will be prompted to save the updated configuration." + {% endif %} RESPOND TYPE=command MSG="action:prompt_text The bed mesh will be calibrated with a bed temperature of {bed_temperature}C. The extruder PID will be calibrated with a hotend temperature of {hotend_temperature}C." RESPOND TYPE=command MSG="action:prompt_text Please clear out the chamber and bed of any foreign objects and click Ready." RESPOND TYPE=command MSG="action:prompt_footer_button Back|CHECK_CALIBRATION" @@ -98,25 +106,60 @@ gcode: RESPOND TYPE=command MSG="action:prompt_show" [gcode_macro _CALIBRATE_ALL_STEP_2] +gcode: + _CLOSE_PROMPT + _CALIBRATE_ALL_STEP_4 + +[gcode_macro _CALIBRATE_ALL_STEP_4] gcode: {% set settings = printer["gcode_macro _COSMOS_SETTINGS"]|default({}) %} {% set hotend_temperature = settings.full_calibrate_hotend_temperature|int %} {% set bed_temperature = settings.full_calibrate_bed_temperature|int %} + {% set input_shaper = settings.input_shaper|default('rusty')|lower %} + {% set input_shapers = settings.input_shapers|default('mzv')|trim %} _CLOSE_PROMPT SET_DISPLAY_TEXT MSG="Performing shaper calibration" - RUN_SHELL_COMMAND CMD=CAMERA_STOP - RUN_SHELL_COMMAND CMD=GUI_STOP G28 M400 - SHAPER_CALIBRATE AXIS=X FORCE_SHAPER=mzv - G4 P1000 - SHAPER_CALIBRATE AXIS=Y FORCE_SHAPER=mzv - G4 P1000 - RUN_SHELL_COMMAND CMD=GUI_START - RUN_SHELL_COMMAND CMD=CAMERA_START - + {% if input_shaper == 'rusty' %} + TEST_RESONANCES AXIS=X OUTPUT=raw_data + G4 P1000 + TEST_RESONANCES AXIS=Y OUTPUT=raw_data + G4 P1000 + SET_GCODE_VARIABLE MACRO=_RUSTY_SHAPER_LAUNCH VARIABLE=full_calibration VALUE=True + SET_GCODE_VARIABLE MACRO=_RUSTY_SHAPER_LAUNCH VARIABLE=shapers VALUE="'{input_shapers}'" + UPDATE_DELAYED_GCODE ID=rusty_shaper_start DURATION=0.1 + {% elif input_shaper == 'classic' %} + {% if ',' in input_shapers or input_shapers == 'all' %} + { action_raise_error("Classic input-shaper calibration supports one shaper or a blank input_shapers setting.") } + {% endif %} + RUN_SHELL_COMMAND CMD=CAMERA_STOP + RUN_SHELL_COMMAND CMD=GUI_STOP + {% if input_shapers %} + SHAPER_CALIBRATE AXIS=X FORCE_SHAPER={input_shapers} + G4 P1000 + SHAPER_CALIBRATE AXIS=Y FORCE_SHAPER={input_shapers} + {% else %} + SHAPER_CALIBRATE AXIS=X + G4 P1000 + SHAPER_CALIBRATE AXIS=Y + {% endif %} + G4 P1000 + RUN_SHELL_COMMAND CMD=GUI_START + RUN_SHELL_COMMAND CMD=CAMERA_START + _CALIBRATE_ALL_STEP_5 + {% else %} + { action_raise_error("Unknown input_shaper setting: " ~ input_shaper) } + {% endif %} + +[gcode_macro _CALIBRATE_ALL_STEP_5] +gcode: + {% set settings = printer["gcode_macro _COSMOS_SETTINGS"]|default({}) %} + {% set hotend_temperature = settings.full_calibrate_hotend_temperature|int %} + {% set bed_temperature = settings.full_calibrate_bed_temperature|int %} + SET_DISPLAY_TEXT MSG="Performing extruder PID calibration" MOVE_TO_TRAY PID_CALIBRATE HEATER=extruder TARGET={hotend_temperature} @@ -134,6 +177,59 @@ gcode: _SAVE_CONFIG_PROMPT +[gcode_macro _RUSTY_SHAPER_LAUNCH] +variable_full_calibration: False +variable_shapers: '' +gcode: + +[gcode_macro _RUSTY_SHAPER_START_STANDALONE] +description: Queue Rusty Shaper after the caller's G-code has returned +gcode: + SET_GCODE_VARIABLE MACRO=_RUSTY_SHAPER_LAUNCH VARIABLE=full_calibration VALUE=False + SET_GCODE_VARIABLE MACRO=_RUSTY_SHAPER_LAUNCH VARIABLE=shapers VALUE="''" + UPDATE_DELAYED_GCODE ID=rusty_shaper_start DURATION=0.1 + +[delayed_gcode rusty_shaper_start] +initial_duration: 0 +gcode: + {% set launch = printer["gcode_macro _RUSTY_SHAPER_LAUNCH"] %} + {% set full_calibration = launch.full_calibration %} + {% set shapers = launch.shapers|default('') %} + SET_GCODE_VARIABLE MACRO=_RUSTY_SHAPER_LAUNCH VARIABLE=full_calibration VALUE=False + {% if full_calibration %} + RUN_SHELL_COMMAND CMD=RUSTY_SHAPER_START PARAMS="full {shapers}" + {% else %} + RUN_SHELL_COMMAND CMD=RUSTY_SHAPER_START + {% endif %} + +[gcode_macro _RUSTY_SHAPER_STATE] +variable_result: 'idle' +variable_continue_full_calibration: False +gcode: + +[gcode_macro RUSTY_SHAPER_FINISHED] +description: Receive the asynchronous Rusty Shaper worker result +gcode: + {% set result = params.RESULT|default('error')|lower %} + {% set continue_full_calibration = params.FULL|default(0)|int %} + SET_GCODE_VARIABLE MACRO=_RUSTY_SHAPER_STATE VARIABLE=result VALUE="'{result}'" + SET_GCODE_VARIABLE MACRO=_RUSTY_SHAPER_STATE VARIABLE=continue_full_calibration VALUE={continue_full_calibration} + UPDATE_DELAYED_GCODE ID=rusty_shaper_complete DURATION=0.1 + +[delayed_gcode rusty_shaper_complete] +initial_duration: 0 +gcode: + {% set result = printer["gcode_macro _RUSTY_SHAPER_STATE"].result %} + {% set continue_full_calibration = printer["gcode_macro _RUSTY_SHAPER_STATE"].continue_full_calibration %} + SET_GCODE_VARIABLE MACRO=_RUSTY_SHAPER_STATE VARIABLE=continue_full_calibration VALUE=False + {% if result == 'success' %} + {% if continue_full_calibration %} + _CALIBRATE_ALL_STEP_5 + {% endif %} + {% else %} + { action_raise_error("Rusty Shaper failed; the remaining calibration steps were not started.") } + {% endif %} + [delayed_gcode open_oobe] initial_duration: 2 gcode: diff --git a/meta-opencentauri/recipes-apps/klipper/files/shell.cfg b/meta-opencentauri/recipes-apps/klipper/files/shell.cfg index 0c50e204..0788916b 100644 --- a/meta-opencentauri/recipes-apps/klipper/files/shell.cfg +++ b/meta-opencentauri/recipes-apps/klipper/files/shell.cfg @@ -78,6 +78,11 @@ command: /etc/init.d/gui-switcher stop timeout: 5 verbose: False +[gcode_shell_command RUSTY_SHAPER_START] +command: /usr/bin/klippy-rusty-shaper +timeout: 5 +verbose: False + [gcode_shell_command REBOOT_SYSTEM] command: reboot timeout: 5 diff --git a/meta-opencentauri/recipes-apps/rusty-shaper/files/klippy-rusty-shaper b/meta-opencentauri/recipes-apps/rusty-shaper/files/klippy-rusty-shaper new file mode 100644 index 00000000..d4bc40aa --- /dev/null +++ b/meta-opencentauri/recipes-apps/rusty-shaper/files/klippy-rusty-shaper @@ -0,0 +1,63 @@ +#!/bin/sh + +log_file=/etc/klipper/logs/klippy.log +full_calibration=0 +if [ "${1:-}" = full ]; then + full_calibration=1 + shift +fi +shapers=${1:-} +run_dir=/user-resource/.tmp +moonraker_host=127.0.0.1 +moonraker_port=80 +worker_log="$run_dir/rusty-shaper.log" +status_file="$run_dir/rusty-shaper.status" + +find_raw_data() { + axis=$1 + awk -v axis="$axis" ' + /Writing raw accelerometer data to/ && $0 ~ ("raw_data_" axis "_") { + if (match($0, /\/[^ ]+\.csv/)) + file = substr($0, RSTART, RLENGTH) + } + END { print file } + ' "$log_file" +} + +mkdir -p "$run_dir" +rm -f "$status_file" + +# RUN_SHELL_COMMAND waits for its child. Rusty Shaper reports progress and +# applies settings through Moonraker, which requires Klipper's reactor to be +# free, so detach the worker before it issues those callbacks. +( + x_file=$(find_raw_data x) + y_file=$(find_raw_data y) + + if [ -z "$x_file" ] || [ -z "$y_file" ]; then + echo "Rusty Shaper could not find the X and Y raw resonance captures in $log_file" >&2 + result=error + else + work_dir=$(dirname "$x_file") + set -- --workdir "$work_dir" --output csv --output klippy \ + --log-macro M125 --moonraker-host "$moonraker_host" --moonraker-port "$moonraker_port" + + if [ -n "$shapers" ]; then + set -- "$@" --shapers "$shapers" + fi + + if /usr/bin/rusty-shaper "$@" "$x_file" "$y_file"; then + result=success + else + result=error + fi + fi + + printf '%s\n' "$result" > "$status_file" + curl --fail --silent --show-error --connect-timeout 2 --max-time 5 \ + --data-urlencode "script=RUSTY_SHAPER_FINISHED RESULT=$result FULL=$full_calibration" \ + "http://$moonraker_host:$moonraker_port/printer/gcode/script" >/dev/null || \ + echo "Rusty Shaper could not report completion to Klipper" >&2 +) > "$worker_log" 2>&1 < /dev/null & + +exit 0 diff --git a/meta-opencentauri/recipes-apps/rusty-shaper/rusty-shaper_0.1.0.bb b/meta-opencentauri/recipes-apps/rusty-shaper/rusty-shaper_0.1.0.bb new file mode 100644 index 00000000..5e1af6cb --- /dev/null +++ b/meta-opencentauri/recipes-apps/rusty-shaper/rusty-shaper_0.1.0.bb @@ -0,0 +1,190 @@ +inherit cargo + +SUMMARY = "Low-RAM Rust input-shaper calibration" +DESCRIPTION = "Rust implementation of Kalico input-shaper calibration for constrained printers" +HOMEPAGE = "https://github.com/OpenCentauri/OpenCentauri" +LICENSE = "GPL-3.0-only" +LIC_FILES_CHKSUM = "file://COPYING;md5=1ebbd3e34237af26da5dc08a4e440464" + +SRC_URI = "git://github.com/OpenCentauri/OpenCentauri.git;protocol=https;branch=paul/rusty-shaper-review \ + file://klippy-rusty-shaper \ + crate://crates.io/adler2/2.0.1 \ + crate://crates.io/android_system_properties/0.1.5 \ + crate://crates.io/anstream/1.0.0 \ + crate://crates.io/anstyle/1.0.14 \ + crate://crates.io/anstyle-parse/1.0.0 \ + crate://crates.io/anstyle-query/1.1.5 \ + crate://crates.io/anstyle-wincon/3.0.11 \ + crate://crates.io/approx/0.5.1 \ + crate://crates.io/autocfg/1.5.1 \ + crate://crates.io/bitflags/2.13.0 \ + crate://crates.io/bumpalo/3.20.3 \ + crate://crates.io/cc/1.2.65 \ + crate://crates.io/cfg-if/1.0.4 \ + crate://crates.io/chrono/0.4.45 \ + crate://crates.io/clap/4.6.1 \ + crate://crates.io/clap_builder/4.6.0 \ + crate://crates.io/clap_derive/4.6.1 \ + crate://crates.io/clap_lex/1.1.0 \ + crate://crates.io/colorchoice/1.0.5 \ + crate://crates.io/core-foundation-sys/0.8.7 \ + crate://crates.io/crc32fast/1.5.0 \ + crate://crates.io/csv/1.4.0 \ + crate://crates.io/csv-core/0.1.13 \ + crate://crates.io/errno/0.3.14 \ + crate://crates.io/fastrand/2.4.1 \ + crate://crates.io/flate2/1.1.9 \ + crate://crates.io/find-msvc-tools/0.1.9 \ + crate://crates.io/futures-core/0.3.32 \ + crate://crates.io/futures-task/0.3.32 \ + crate://crates.io/futures-util/0.3.32 \ + crate://crates.io/getrandom/0.4.3 \ + crate://crates.io/heck/0.5.0 \ + crate://crates.io/iana-time-zone/0.1.65 \ + crate://crates.io/iana-time-zone-haiku/0.1.2 \ + crate://crates.io/is_terminal_polyfill/1.70.2 \ + crate://crates.io/itoa/1.0.18 \ + crate://crates.io/js-sys/0.3.103 \ + crate://crates.io/libc/0.2.186 \ + crate://crates.io/linux-raw-sys/0.12.1 \ + crate://crates.io/log/0.4.33 \ + crate://crates.io/memchr/2.8.2 \ + crate://crates.io/miniz_oxide/0.8.9 \ + crate://crates.io/num-complex/0.4.6 \ + crate://crates.io/num-integer/0.1.46 \ + crate://crates.io/num-traits/0.2.19 \ + crate://crates.io/once_cell/1.21.4 \ + crate://crates.io/once_cell_polyfill/1.70.2 \ + crate://crates.io/pin-project-lite/0.2.17 \ + crate://crates.io/primal-check/0.3.4 \ + crate://crates.io/proc-macro2/1.0.106 \ + crate://crates.io/quote/1.0.46 \ + crate://crates.io/r-efi/6.0.0 \ + crate://crates.io/rustfft/6.4.1 \ + crate://crates.io/rustix/1.1.4 \ + crate://crates.io/rustversion/1.0.22 \ + crate://crates.io/ryu/1.0.23 \ + crate://crates.io/serde/1.0.228 \ + crate://crates.io/serde_core/1.0.228 \ + crate://crates.io/serde_derive/1.0.228 \ + crate://crates.io/serde_json/1.0.150 \ + crate://crates.io/simd-adler32/0.3.10 \ + crate://crates.io/shlex/2.0.1 \ + crate://crates.io/slab/0.4.12 \ + crate://crates.io/strength_reduce/0.2.4 \ + crate://crates.io/strsim/0.11.1 \ + crate://crates.io/syn/2.0.118 \ + crate://crates.io/tempfile/3.27.0 \ + crate://crates.io/thiserror/2.0.18 \ + crate://crates.io/thiserror-impl/2.0.18 \ + crate://crates.io/transpose/0.2.3 \ + crate://crates.io/unicode-ident/1.0.24 \ + crate://crates.io/utf8parse/0.2.2 \ + crate://crates.io/wasm-bindgen/0.2.126 \ + crate://crates.io/wasm-bindgen-macro/0.2.126 \ + crate://crates.io/wasm-bindgen-macro-support/0.2.126 \ + crate://crates.io/wasm-bindgen-shared/0.2.126 \ + crate://crates.io/windows-core/0.62.2 \ + crate://crates.io/windows-implement/0.60.2 \ + crate://crates.io/windows-interface/0.59.3 \ + crate://crates.io/windows-link/0.2.1 \ + crate://crates.io/windows-result/0.4.1 \ + crate://crates.io/windows-strings/0.5.1 \ + crate://crates.io/windows-sys/0.61.2 \ + crate://crates.io/zmij/1.0.21 \ +" +SRCREV = "186532a007bbf6c3fff03e3a80666e85d7c7247d" +S = "${WORKDIR}/git/rusty-shaper" + +# The upstream release profile intentionally strips this low-RAM binary +# (strip = true, LTO, opt-level = z). Do not ask package QA to strip it again. +INSANE_SKIP:${PN} += "already-stripped" + +do_install:append() { + install -m 0755 ${WORKDIR}/klippy-rusty-shaper ${D}${bindir}/klippy-rusty-shaper +} + +SRC_URI[android_system_properties-0.1.5.sha256sum] = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +SRC_URI[adler2-2.0.1.sha256sum] = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +SRC_URI[anstream-1.0.0.sha256sum] = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +SRC_URI[anstyle-1.0.14.sha256sum] = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" +SRC_URI[anstyle-parse-1.0.0.sha256sum] = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +SRC_URI[anstyle-query-1.1.5.sha256sum] = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +SRC_URI[anstyle-wincon-3.0.11.sha256sum] = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +SRC_URI[approx-0.5.1.sha256sum] = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +SRC_URI[autocfg-1.5.1.sha256sum] = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +SRC_URI[bitflags-2.13.0.sha256sum] = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +SRC_URI[bumpalo-3.20.3.sha256sum] = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +SRC_URI[cc-1.2.65.sha256sum] = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +SRC_URI[cfg-if-1.0.4.sha256sum] = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +SRC_URI[chrono-0.4.45.sha256sum] = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +SRC_URI[clap-4.6.1.sha256sum] = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +SRC_URI[clap_builder-4.6.0.sha256sum] = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +SRC_URI[clap_derive-4.6.1.sha256sum] = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +SRC_URI[clap_lex-1.1.0.sha256sum] = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +SRC_URI[colorchoice-1.0.5.sha256sum] = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +SRC_URI[core-foundation-sys-0.8.7.sha256sum] = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +SRC_URI[crc32fast-1.5.0.sha256sum] = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +SRC_URI[csv-1.4.0.sha256sum] = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +SRC_URI[csv-core-0.1.13.sha256sum] = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +SRC_URI[errno-0.3.14.sha256sum] = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +SRC_URI[fastrand-2.4.1.sha256sum] = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +SRC_URI[flate2-1.1.9.sha256sum] = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +SRC_URI[find-msvc-tools-0.1.9.sha256sum] = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +SRC_URI[futures-core-0.3.32.sha256sum] = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +SRC_URI[futures-task-0.3.32.sha256sum] = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +SRC_URI[futures-util-0.3.32.sha256sum] = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +SRC_URI[getrandom-0.4.3.sha256sum] = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +SRC_URI[heck-0.5.0.sha256sum] = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +SRC_URI[iana-time-zone-0.1.65.sha256sum] = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +SRC_URI[iana-time-zone-haiku-0.1.2.sha256sum] = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +SRC_URI[is_terminal_polyfill-1.70.2.sha256sum] = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +SRC_URI[itoa-1.0.18.sha256sum] = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +SRC_URI[js-sys-0.3.103.sha256sum] = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +SRC_URI[libc-0.2.186.sha256sum] = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +SRC_URI[linux-raw-sys-0.12.1.sha256sum] = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +SRC_URI[log-0.4.33.sha256sum] = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +SRC_URI[memchr-2.8.2.sha256sum] = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +SRC_URI[miniz_oxide-0.8.9.sha256sum] = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +SRC_URI[num-complex-0.4.6.sha256sum] = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +SRC_URI[num-integer-0.1.46.sha256sum] = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +SRC_URI[num-traits-0.2.19.sha256sum] = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +SRC_URI[once_cell-1.21.4.sha256sum] = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +SRC_URI[once_cell_polyfill-1.70.2.sha256sum] = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +SRC_URI[pin-project-lite-0.2.17.sha256sum] = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +SRC_URI[primal-check-0.3.4.sha256sum] = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" +SRC_URI[proc-macro2-1.0.106.sha256sum] = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +SRC_URI[quote-1.0.46.sha256sum] = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +SRC_URI[r-efi-6.0.0.sha256sum] = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +SRC_URI[rustfft-6.4.1.sha256sum] = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" +SRC_URI[rustix-1.1.4.sha256sum] = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +SRC_URI[rustversion-1.0.22.sha256sum] = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +SRC_URI[ryu-1.0.23.sha256sum] = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +SRC_URI[serde-1.0.228.sha256sum] = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +SRC_URI[serde_core-1.0.228.sha256sum] = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +SRC_URI[serde_derive-1.0.228.sha256sum] = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +SRC_URI[serde_json-1.0.150.sha256sum] = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +SRC_URI[simd-adler32-0.3.10.sha256sum] = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +SRC_URI[shlex-2.0.1.sha256sum] = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +SRC_URI[slab-0.4.12.sha256sum] = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +SRC_URI[strength_reduce-0.2.4.sha256sum] = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" +SRC_URI[strsim-0.11.1.sha256sum] = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +SRC_URI[syn-2.0.118.sha256sum] = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +SRC_URI[tempfile-3.27.0.sha256sum] = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +SRC_URI[thiserror-2.0.18.sha256sum] = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +SRC_URI[thiserror-impl-2.0.18.sha256sum] = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +SRC_URI[transpose-0.2.3.sha256sum] = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +SRC_URI[unicode-ident-1.0.24.sha256sum] = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +SRC_URI[utf8parse-0.2.2.sha256sum] = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +SRC_URI[wasm-bindgen-0.2.126.sha256sum] = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +SRC_URI[wasm-bindgen-macro-0.2.126.sha256sum] = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +SRC_URI[wasm-bindgen-macro-support-0.2.126.sha256sum] = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +SRC_URI[wasm-bindgen-shared-0.2.126.sha256sum] = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +SRC_URI[windows-core-0.62.2.sha256sum] = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +SRC_URI[windows-implement-0.60.2.sha256sum] = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +SRC_URI[windows-interface-0.59.3.sha256sum] = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +SRC_URI[windows-link-0.2.1.sha256sum] = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +SRC_URI[windows-result-0.4.1.sha256sum] = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +SRC_URI[windows-strings-0.5.1.sha256sum] = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +SRC_URI[windows-sys-0.61.2.sha256sum] = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +SRC_URI[zmij-1.0.21.sha256sum] = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" 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..4d61053f 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 @@ -21,6 +21,8 @@ full_calibrate_hotend_temperature=$(get_config_value full_calibrate_hotend_tempe full_calibrate_bed_temperature=$(get_config_value full_calibrate_bed_temperature) bypass_calibration=$(get_config_value bypass_calibration) toolhead_led=$(get_config_value toolhead_led) +input_shaper=$(get_config_value input_shaper) +input_shapers=$(get_config_value input_shapers) mkdir -p "${OUTPUT_DIR}" @@ -40,7 +42,11 @@ variable_full_calibrate_hotend_temperature: ${full_calibrate_hotend_temperature} variable_full_calibrate_bed_temperature: ${full_calibrate_bed_temperature} variable_bypass_calibration: ${bypass_calibration} variable_toolhead_led: ${toolhead_led} +variable_input_shaper: '${input_shaper}' +variable_input_shapers: '${input_shapers}' +variable_toolhead_led: ${toolhead_led} +variable_input_shaper: '${input_shaper}' +variable_input_shapers: '${input_shapers}' gcode: EOF - 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..614e505a 100644 --- a/meta-opencentauri/recipes-data/config-manager/files/config_manager.py +++ b/meta-opencentauri/recipes-data/config-manager/files/config_manager.py @@ -1,6 +1,17 @@ #!/usr/bin/env python3 import os, configparser, sys +VALID_INPUT_SHAPERS = {'zv', 'mzv', 'zvd', 'ei', '2hump_ei', '3hump_ei'} + +def valid_input_shapers(value: str) -> bool: + """Accept an empty value or a comma-separated Rusty Shaper list.""" + if not value: + return True + shapers = value.split(',') + return ('all' not in shapers or len(shapers) == 1) and all( + shaper in VALID_INPUT_SHAPERS or shaper == 'all' + for shaper in shapers) + VALIDATORS = { 'ui': { 'screen_ui': ['grumpyscreen', 'guppyscreen', 'atomscreen', 'none'], @@ -23,6 +34,8 @@ 'full_calibrate_hotend_temperature': [str(i) for i in range(200, 301)], 'full_calibrate_bed_temperature': [str(i) for i in range(30, 111)], 'toolhead_led': ['True', 'False'], + 'input_shaper': ['rusty', 'classic'], + 'input_shapers': valid_input_shapers, }, } @@ -37,7 +50,9 @@ def validate_config(config : dict): if option not in config[section]: continue value = config[section][option] - if value not in valid_values: + is_valid = (valid_values(value) if callable(valid_values) + else value in valid_values) + if not is_valid: print(f"Warning: Invalid value '{value}' for '{option}' in section '{section}'. Valid options are: {valid_values}", file=sys.stderr) del config[section][option] diff --git a/meta-opencentauri/recipes-data/config-manager/files/default.conf b/meta-opencentauri/recipes-data/config-manager/files/default.conf index 0835ae1f..7663506a 100644 --- a/meta-opencentauri/recipes-data/config-manager/files/default.conf +++ b/meta-opencentauri/recipes-data/config-manager/files/default.conf @@ -38,4 +38,9 @@ full_calibrate_hotend_temperature = 250 # Bed temperature in Celsius used during the full calibration workflow. Range: 40-100. full_calibrate_bed_temperature = 60 # Turn on Toolhead LED for printing and loading filament -toolhead_led = False \ No newline at end of file +toolhead_led = False +# Input-shaper calibration engine. Valid values: rusty (fast, low-RAM Rust implementation) or classic (Kalico's built-in calibrator). +input_shaper = rusty +# Shaper candidates used by the selected engine. Rusty accepts zv, mzv, zvd, ei, 2hump_ei, 3hump_ei, all, or a comma-separated list. +# Classic accepts one of zv, mzv, zvd, ei, 2hump_ei, 3hump_ei, or blank. Default: mzv. Leave blank to use the Kalico-compatible default set. +input_shapers = mzv From 51cda0e1f71717cd7f1a78c668edd9d1b28ae1a7 Mon Sep 17 00:00:00 2001 From: Paul Swenson Date: Sat, 18 Jul 2026 15:18:42 -0400 Subject: [PATCH 09/20] calibration: skip stages with saved results Add FORCE parameter to CALIBRATE_ALL. The explicit macro defaults to FORCE=1 so it re-runs every stage (same as old default, just explicitly called out). The CHECK_CALIBRATION prompt passes FORCE=0 so the auto-calibrate flow only fills in missing saved stages. Also, move M125 from calibration.cfg to macros.cfg with a generic description so it lives near the other M-code macros (could be useful for other macros to call that want evidence to land in /etc/klipper/logs/klippy.log!) --- .../klipper/files/calibration.cfg | 198 ++++++++++++------ .../recipes-apps/klipper/files/macros.cfg | 5 + 2 files changed, 144 insertions(+), 59 deletions(-) diff --git a/meta-opencentauri/recipes-apps/klipper/files/calibration.cfg b/meta-opencentauri/recipes-apps/klipper/files/calibration.cfg index 593f30fa..ce01f532 100644 --- a/meta-opencentauri/recipes-apps/klipper/files/calibration.cfg +++ b/meta-opencentauri/recipes-apps/klipper/files/calibration.cfg @@ -3,11 +3,6 @@ # UPDATES WILL RESET THIS FILE. YOUR EDITS WILL BE LOST WHEN UPDATING. # ----- -[gcode_macro M125] -description: Write a shaper-calibration status message to the UI and klippy.log. -gcode: - { action_respond_info(rawparams) } - [gcode_macro _CHECK_CALIBRATION_VARS] variable_shaper_calibrated: False variable_bed_mesh_calibrated: False @@ -30,9 +25,9 @@ gcode: {% set bed_mesh_default_exists = 'bed_mesh default' in printer.configfile.settings %} {% set load_cell_probe_reference_tare_counts = printer.configfile.settings.load_cell_probe.reference_tare_counts|default([71000]) %} {% set load_cell_probe_calibrated = (load_cell_probe_reference_tare_counts|sum) != (71000 * load_cell_probe_reference_tare_counts|length) %} - {% set extruder_pid_kp = printer.configfile.settings.extruder.pid_kp %} - {% set extruder_control_type = printer.configfile.settings.extruder.control %} - {% set extruder_pid_calibrated = extruder_control_type != 'pid' or extruder_pid_kp != 24.860427 %} + {% set extruder_control_type = printer.configfile.settings.extruder.control|default('pid')|lower %} + {% set extruder_pid_version = printer.configfile.settings.extruder.pid_version|default(0)|int %} + {% set extruder_pid_calibrated = extruder_control_type != 'pid' or extruder_pid_version >= 1 %} {% set all_calibrated = shaper_calibrated and bed_mesh_default_exists and load_cell_probe_calibrated and extruder_pid_calibrated %} SET_GCODE_VARIABLE MACRO=_CHECK_CALIBRATION_VARS VARIABLE=shaper_calibrated VALUE={shaper_calibrated} @@ -85,73 +80,142 @@ gcode: {% endif %} RESPOND TYPE=command MSG="action:prompt_footer_button Close|_CLOSE_PROMPT" - RESPOND TYPE=command MSG="action:prompt_footer_button Calibrate All|_CALIBRATE_ALL_STEP_1|warning" + RESPOND TYPE=command MSG="action:prompt_footer_button Calibrate All|CALIBRATE_ALL FORCE=0|warning" RESPOND TYPE=command MSG="action:prompt_show" +# CALIBRATE_ALL runs every saved stage by default. Use FORCE=0 after +# the initial setup to only re-run stages that are missing or out of date. +[gcode_macro CALIBRATE_ALL] +description: Calibrate all stages; use FORCE=0 to skip already-saved stages. +gcode: + _CALIBRATE_ALL_STEP_1 FORCE={params.FORCE|default(1)|int} + +[gcode_macro _CALIBRATE_ALL_STATE] +variable_force: False +variable_shaper_ran: False +variable_load_cell_ran: False +gcode: + [gcode_macro _CALIBRATE_ALL_STEP_1] gcode: {% set settings = printer["gcode_macro _COSMOS_SETTINGS"]|default({}) %} {% set hotend_temperature = settings.full_calibrate_hotend_temperature|int %} {% set bed_temperature = settings.full_calibrate_bed_temperature|int %} {% set input_shaper = settings.input_shaper|default('rusty')|lower %} + {% set force = params.FORCE|default(0)|int > 0 %} + {% set shaper_freq_x = printer.configfile.settings.input_shaper.shaper_freq_x|default(0)|float %} + {% set shaper_freq_y = printer.configfile.settings.input_shaper.shaper_freq_y|default(0)|float %} + {% set shaper_calibrated = shaper_freq_x > 0 and shaper_freq_y > 0 %} + {% set bed_mesh_calibrated = 'bed_mesh default' in printer.configfile.settings %} + {% set reference_tare_counts = printer.configfile.settings.load_cell_probe.reference_tare_counts|default([71000]) %} + {% set load_cell_calibrated = (reference_tare_counts|sum) != (71000 * reference_tare_counts|length) %} + {% set extruder_control_type = printer.configfile.settings.extruder.control|default('pid')|lower %} + {% set extruder_pid_version = printer.configfile.settings.extruder.pid_version|default(0)|int %} + {% set extruder_pid_calibrated = extruder_control_type != 'pid' or extruder_pid_version >= 1 %} + {% set needs_shaper = force or not shaper_calibrated %} + {% set needs_mesh = force or not bed_mesh_calibrated %} + {% set needs_load_cell = force or not load_cell_calibrated %} + {% set needs_pid = force or not extruder_pid_calibrated %} + + SET_GCODE_VARIABLE MACRO=_CALIBRATE_ALL_STATE VARIABLE=force VALUE={force} + SET_GCODE_VARIABLE MACRO=_CALIBRATE_ALL_STATE VARIABLE=shaper_ran VALUE=False + SET_GCODE_VARIABLE MACRO=_CALIBRATE_ALL_STATE VARIABLE=load_cell_ran VALUE=False RESPOND TYPE=command MSG="action:prompt_begin Calibrate all" - {% if input_shaper == 'classic' %} + {% if not (needs_shaper or needs_mesh or needs_load_cell or needs_pid) %} + RESPOND TYPE=command MSG="action:prompt_text All saved calibration data is already current. Run CALIBRATE_ALL FORCE=1 to rerun every calibration." + RESPOND TYPE=command MSG="action:prompt_footer_button Close|_CLOSE_PROMPT|primary" + RESPOND TYPE=command MSG="action:prompt_show" + RETURN + {% endif %} + {% if needs_shaper and input_shaper == 'classic' %} RESPOND TYPE=command MSG="action:prompt_text The screen and camera will be disabled during resonance compensation calibration. When the calibration is complete, you will be prompted to save the updated configuration." {% endif %} - RESPOND TYPE=command MSG="action:prompt_text The bed mesh will be calibrated with a bed temperature of {bed_temperature}C. The extruder PID will be calibrated with a hotend temperature of {hotend_temperature}C." - RESPOND TYPE=command MSG="action:prompt_text Please clear out the chamber and bed of any foreign objects and click Ready." + {% if needs_shaper %} + RESPOND TYPE=command MSG="action:prompt_text Resonance compensation will be calibrated. Clear the chamber and bed of loose objects before clicking Ready." + {% endif %} + {% if needs_load_cell %} + RESPOND TYPE=command MSG="action:prompt_text Load cells will be tare calibrated. Keep the bed empty before clicking Ready." + {% endif %} + {% if needs_pid %} + RESPOND TYPE=command MSG="action:prompt_text Extruder PID will be calibrated at {hotend_temperature}C. Keep the toolhead-to-tray path clear." + {% endif %} + {% if needs_mesh %} + RESPOND TYPE=command MSG="action:prompt_text Probe z-offset and the default bed mesh will be calibrated at {bed_temperature}C. Clear and clean the bed." + {% endif %} + {% if force %} + RESPOND TYPE=command MSG="action:prompt_text FORCE=1 reruns all calibration stages, including stages with saved data." + {% endif %} RESPOND TYPE=command MSG="action:prompt_footer_button Back|CHECK_CALIBRATION" RESPOND TYPE=command MSG="action:prompt_footer_button Ready|_CALIBRATE_ALL_STEP_2|primary" RESPOND TYPE=command MSG="action:prompt_show" [gcode_macro _CALIBRATE_ALL_STEP_2] gcode: + {% set reference_tare_counts = printer.configfile.settings.load_cell_probe.reference_tare_counts|default([71000]) %} + {% set load_cell_calibrated = (reference_tare_counts|sum) != (71000 * reference_tare_counts|length) %} + {% set force = printer["gcode_macro _CALIBRATE_ALL_STATE"].force %} + _CLOSE_PROMPT - _CALIBRATE_ALL_STEP_4 + {% if load_cell_calibrated and not force %} + _CALIBRATE_ALL_STEP_4 + {% else %} + SET_GCODE_VARIABLE MACRO=_CALIBRATE_ALL_STATE VARIABLE=load_cell_ran VALUE=True + SET_DISPLAY_TEXT MSG="Calibrating load cells" + LOAD_CELL_CALIBRATE TARE=TRUE SAVE=TRUE + G4 P2000 + _CALIBRATE_ALL_STEP_4 + {% endif %} [gcode_macro _CALIBRATE_ALL_STEP_4] gcode: {% set settings = printer["gcode_macro _COSMOS_SETTINGS"]|default({}) %} - {% set hotend_temperature = settings.full_calibrate_hotend_temperature|int %} - {% set bed_temperature = settings.full_calibrate_bed_temperature|int %} {% set input_shaper = settings.input_shaper|default('rusty')|lower %} {% set input_shapers = settings.input_shapers|default('mzv')|trim %} + {% set force = printer["gcode_macro _CALIBRATE_ALL_STATE"].force %} + {% set shaper_freq_x = printer.configfile.settings.input_shaper.shaper_freq_x|default(0)|float %} + {% set shaper_freq_y = printer.configfile.settings.input_shaper.shaper_freq_y|default(0)|float %} + {% set shaper_calibrated = shaper_freq_x > 0 and shaper_freq_y > 0 %} _CLOSE_PROMPT - SET_DISPLAY_TEXT MSG="Performing shaper calibration" - G28 - M400 - {% if input_shaper == 'rusty' %} - TEST_RESONANCES AXIS=X OUTPUT=raw_data - G4 P1000 - TEST_RESONANCES AXIS=Y OUTPUT=raw_data - G4 P1000 - SET_GCODE_VARIABLE MACRO=_RUSTY_SHAPER_LAUNCH VARIABLE=full_calibration VALUE=True - SET_GCODE_VARIABLE MACRO=_RUSTY_SHAPER_LAUNCH VARIABLE=shapers VALUE="'{input_shapers}'" - UPDATE_DELAYED_GCODE ID=rusty_shaper_start DURATION=0.1 - {% elif input_shaper == 'classic' %} - {% if ',' in input_shapers or input_shapers == 'all' %} - { action_raise_error("Classic input-shaper calibration supports one shaper or a blank input_shapers setting.") } - {% endif %} - RUN_SHELL_COMMAND CMD=CAMERA_STOP - RUN_SHELL_COMMAND CMD=GUI_STOP - {% if input_shapers %} - SHAPER_CALIBRATE AXIS=X FORCE_SHAPER={input_shapers} + {% if shaper_calibrated and not force %} + _CALIBRATE_ALL_STEP_5 + {% else %} + SET_GCODE_VARIABLE MACRO=_CALIBRATE_ALL_STATE VARIABLE=shaper_ran VALUE=True + SET_DISPLAY_TEXT MSG="Performing shaper calibration" + G28 + M400 + {% if input_shaper == 'rusty' %} + TEST_RESONANCES AXIS=X OUTPUT=raw_data G4 P1000 - SHAPER_CALIBRATE AXIS=Y FORCE_SHAPER={input_shapers} - {% else %} - SHAPER_CALIBRATE AXIS=X + TEST_RESONANCES AXIS=Y OUTPUT=raw_data G4 P1000 - SHAPER_CALIBRATE AXIS=Y + SET_GCODE_VARIABLE MACRO=_RUSTY_SHAPER_LAUNCH VARIABLE=full_calibration VALUE=True + SET_GCODE_VARIABLE MACRO=_RUSTY_SHAPER_LAUNCH VARIABLE=shapers VALUE="'{input_shapers}'" + UPDATE_DELAYED_GCODE ID=rusty_shaper_start DURATION=0.1 + {% elif input_shaper == 'classic' %} + {% if ',' in input_shapers or input_shapers == 'all' %} + { action_raise_error("Classic input-shaper calibration supports one shaper or a blank input_shapers setting.") } + {% endif %} + RUN_SHELL_COMMAND CMD=CAMERA_STOP + RUN_SHELL_COMMAND CMD=GUI_STOP + {% if input_shapers %} + SHAPER_CALIBRATE AXIS=X FORCE_SHAPER={input_shapers} + G4 P1000 + SHAPER_CALIBRATE AXIS=Y FORCE_SHAPER={input_shapers} + {% else %} + SHAPER_CALIBRATE AXIS=X + G4 P1000 + SHAPER_CALIBRATE AXIS=Y + {% endif %} + G4 P1000 + RUN_SHELL_COMMAND CMD=GUI_START + RUN_SHELL_COMMAND CMD=CAMERA_START + _CALIBRATE_ALL_STEP_5 + {% else %} + { action_raise_error("Unknown input_shaper setting: " ~ input_shaper) } {% endif %} - G4 P1000 - RUN_SHELL_COMMAND CMD=GUI_START - RUN_SHELL_COMMAND CMD=CAMERA_START - _CALIBRATE_ALL_STEP_5 - {% else %} - { action_raise_error("Unknown input_shaper setting: " ~ input_shaper) } {% endif %} [gcode_macro _CALIBRATE_ALL_STEP_5] @@ -159,23 +223,39 @@ gcode: {% set settings = printer["gcode_macro _COSMOS_SETTINGS"]|default({}) %} {% set hotend_temperature = settings.full_calibrate_hotend_temperature|int %} {% set bed_temperature = settings.full_calibrate_bed_temperature|int %} + {% set state = printer["gcode_macro _CALIBRATE_ALL_STATE"] %} + {% set force = state.force %} + {% set extruder_control_type = printer.configfile.settings.extruder.control|default('pid')|lower %} + {% set extruder_pid_version = printer.configfile.settings.extruder.pid_version|default(0)|int %} + {% set extruder_pid_calibrated = extruder_control_type != 'pid' or extruder_pid_version >= 1 %} + {% set bed_mesh_calibrated = 'bed_mesh default' in printer.configfile.settings %} + {% set needs_pid = force or not extruder_pid_calibrated %} + {% set needs_mesh = force or not bed_mesh_calibrated %} + {% set needs_save = state.shaper_ran or state.load_cell_ran or needs_pid or needs_mesh %} + + {% if needs_pid %} + SET_DISPLAY_TEXT MSG="Performing extruder PID calibration" + MOVE_TO_TRAY + PID_CALIBRATE HEATER=extruder TARGET={hotend_temperature} + M400 + {% endif %} - SET_DISPLAY_TEXT MSG="Performing extruder PID calibration" - MOVE_TO_TRAY - PID_CALIBRATE HEATER=extruder TARGET={hotend_temperature} - CLEAN_NOZZLE - M400 - - SET_DISPLAY_TEXT MSG="Calibrating load cells" - LOAD_CELL_CALIBRATE TARE=TRUE SAVE=TRUE - G4 P2000 - - SET_DISPLAY_TEXT MSG="Performing probe z-offset & bed mesh calibration" - G90 - G1 X128 Y128 F6000 - BED_MESH_CALIBRATE BED_TEMP={bed_temperature} + {% if needs_mesh %} + SET_DISPLAY_TEXT MSG="Performing probe z-offset & bed mesh calibration" + G90 + G1 X128 Y128 F6000 + BED_MESH_CALIBRATE BED_TEMP={bed_temperature} + {% endif %} - _SAVE_CONFIG_PROMPT + SET_GCODE_VARIABLE MACRO=_CALIBRATE_ALL_STATE VARIABLE=force VALUE=False + SET_GCODE_VARIABLE MACRO=_CALIBRATE_ALL_STATE VARIABLE=shaper_ran VALUE=False + SET_GCODE_VARIABLE MACRO=_CALIBRATE_ALL_STATE VARIABLE=load_cell_ran VALUE=False + {% if needs_save %} + _SAVE_CONFIG_PROMPT + {% else %} + SET_DISPLAY_TEXT MSG="Saved calibration data is already current" + { action_respond_info("All saved calibration data is already current. Use CALIBRATE_ALL FORCE=1 to rerun every calibration.") } + {% endif %} [gcode_macro _RUSTY_SHAPER_LAUNCH] variable_full_calibration: False diff --git a/meta-opencentauri/recipes-apps/klipper/files/macros.cfg b/meta-opencentauri/recipes-apps/klipper/files/macros.cfg index 23dcda23..4348d1a7 100644 --- a/meta-opencentauri/recipes-apps/klipper/files/macros.cfg +++ b/meta-opencentauri/recipes-apps/klipper/files/macros.cfg @@ -703,6 +703,11 @@ gcode: {% set index = params.P|default(1)|int %} M106 P{index} S0 +[gcode_macro M125] +description: Echo a message to the console and klippy.log, similar to M118 but always visible in both sinks. +gcode: + { action_respond_info(rawparams) } + [gcode_macro _SHOW_PROMPT] gcode: From 6f900c95b10f10835da643cef3877389e8c226bc Mon Sep 17 00:00:00 2001 From: Paul Swenson Date: Sat, 18 Jul 2026 21:26:25 -0400 Subject: [PATCH 10/20] kalico: persist input-shaper settings and stream raw accelerometer writes - input_shaper: mark [input_shaper] dirty when SET_INPUT_SHAPER changes live parameters so SAVE_CONFIG writes the new shaper_type/freq values. - adxl345: stream raw accelerometer data directly from message batches to CSV instead of flattening into a samples list first. - Wire both patches into kalico_2026.02.00.inc. --- .../files/0002-persist-set-input-shaper.patch | 38 ++++++++++++++ .../0003-adxl345-stream-raw-writes.patch | 51 +++++++++++++++++++ .../klipper/kalico_2026.02.00.inc | 2 + 3 files changed, 91 insertions(+) create mode 100644 meta-opencentauri/recipes-apps/klipper/files/0002-persist-set-input-shaper.patch create mode 100644 meta-opencentauri/recipes-apps/klipper/files/0003-adxl345-stream-raw-writes.patch diff --git a/meta-opencentauri/recipes-apps/klipper/files/0002-persist-set-input-shaper.patch b/meta-opencentauri/recipes-apps/klipper/files/0002-persist-set-input-shaper.patch new file mode 100644 index 00000000..cb489178 --- /dev/null +++ b/meta-opencentauri/recipes-apps/klipper/files/0002-persist-set-input-shaper.patch @@ -0,0 +1,38 @@ +From 0d1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a Mon Sep 17 00:00:00 2001 +From: Paul Swenson +Date: Sat, 18 Jul 2026 19:50:00 +0000 +Subject: [PATCH] input_shaper: persist SET_INPUT_SHAPER values for SAVE_CONFIG + +Klipper's SET_INPUT_SHAPER updates the live shaper parameters but never +marks the config file as dirty, so a later SAVE_CONFIG does not write the +updated shaper_type_*/shaper_freq_* values. This breaks Rusty Shaper and any +other flow that expects the user to hit SAVE_CONFIG after setting shapers via +G-code. + +Call configfile.set() for each axis when the command actually changes +parameters, mirroring the behaviour of shaper_calibrate.save_params(). + +Upstream-Status: Pending + +--- + klippy/extras/input_shaper.py | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/klippy/extras/input_shaper.py b/klippy/extras/input_shaper.py +index 1600d034..179632df 100644 +--- a/klippy/extras/input_shaper.py ++++ b/klippy/extras/input_shaper.py +@@ -212,6 +212,14 @@ class InputShaper: + for shaper in self.shapers: + shaper.update(gcmd) + self._update_input_shaping() ++ configfile = self.printer.lookup_object('configfile') ++ for shaper in self.shapers: ++ status = shaper.params.get_status() ++ axis = shaper.axis ++ configfile.set("input_shaper", "shaper_type_" + axis, ++ status["shaper_type"]) ++ configfile.set("input_shaper", "shaper_freq_" + axis, ++ status["shaper_freq"]) + for shaper in self.shapers: + shaper.report(gcmd) diff --git a/meta-opencentauri/recipes-apps/klipper/files/0003-adxl345-stream-raw-writes.patch b/meta-opencentauri/recipes-apps/klipper/files/0003-adxl345-stream-raw-writes.patch new file mode 100644 index 00000000..6bb34706 --- /dev/null +++ b/meta-opencentauri/recipes-apps/klipper/files/0003-adxl345-stream-raw-writes.patch @@ -0,0 +1,51 @@ +From be88bd00a33ae93a813c21f48aed7a3f167dc35e Mon Sep 17 00:00:00 2001 +From: Paul Swenson +Date: Sat, 18 Jul 2026 16:07:49 -0400 +Subject: [PATCH] adxl345: stream raw accelerometer writes to avoid holding samples in RAM + +write_to_file() previously flattened all captured batches into a Python list +of Accel_Measurement tuples before writing. For long resonance captures this +doubles the peak memory: the batches already in msgs plus the new samples list. + +Stream the same batches directly to the CSV instead. The msgs are already +filtered to the requested time window by handle_batch, so iterate them and +skip samples outside request_start_time/request_end_time exactly as before. +No new allocations for the full capture. + +Upstream-Status: Pending + +--- + +diff --git a/klippy/extras/adxl345.py b/klippy/extras/adxl345.py +index 7909e04a..478647b6 100644 +--- a/klippy/extras/adxl345.py ++++ b/klippy/extras/adxl345.py +@@ -109,18 +109,18 @@ class AccelQueryHelper: + def write_to_file(self, filename): + def write_impl(): + try: +- # Try to re-nice writing process ++ # ponytail: keep re-nice; IO-bound writer should not steal reactor time + os.nice(20) +- except: ++ except Exception: + pass +- f = open(filename, "w") +- f.write("#time,accel_x,accel_y,accel_z\n") +- samples = self.samples or self.get_samples() +- for t, accel_x, accel_y, accel_z in samples: +- f.write( +- "%.6f,%.6f,%.6f,%.6f\n" % (t, accel_x, accel_y, accel_z) +- ) +- f.close() ++ with open(filename, "w") as f: ++ f.write("#time,accel_x,accel_y,accel_z\n") ++ # ponytail: stream from msgs; avoid flattening whole capture into a list ++ for msg in self.msgs: ++ for t, x, y, z in msg["data"]: ++ if t < self.request_start_time or t > self.request_end_time: ++ continue ++ f.write("%.6f,%.6f,%.6f,%.6f\n" % (t, x, y, z)) + + write_proc = multiprocessing.Process(target=write_impl) + write_proc.daemon = True 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 c94c4234..56ca5b05 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc +++ b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc @@ -13,6 +13,8 @@ SRC_URI = "git://github.com/OpenCentauri/kalico.git;protocol=https;branch=hx711s file://0001-probe-home-z-includes-z-offset.patch \ file://0001-force-specific-input-shaper.patch \ file://0001-Memory-optimisation-in-shaper_calibrate.patch \ + file://0002-persist-set-input-shaper.patch \ + file://0003-adxl345-stream-raw-writes.patch \ " SRCREV = "fa59d011f425c5aef6f06abc08efe43f7f6c569d" From b348004bcbea570cf72eb076b6521db7e50cb61e Mon Sep 17 00:00:00 2001 From: Paul Swenson Date: Sat, 18 Jul 2026 23:33:05 -0400 Subject: [PATCH 11/20] calibration: detect only saved extruder PID results Kalico normalizes every PID-controlled extruder with pid_version=1 in configfile.settings, including the image default PID coefficients. Read the raw configfile.config marker instead so the calibration status and saved-stage logic require an explicit saved PID result. --- .../recipes-apps/klipper/files/calibration.cfg | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/meta-opencentauri/recipes-apps/klipper/files/calibration.cfg b/meta-opencentauri/recipes-apps/klipper/files/calibration.cfg index ce01f532..5af3879b 100644 --- a/meta-opencentauri/recipes-apps/klipper/files/calibration.cfg +++ b/meta-opencentauri/recipes-apps/klipper/files/calibration.cfg @@ -25,8 +25,9 @@ gcode: {% set bed_mesh_default_exists = 'bed_mesh default' in printer.configfile.settings %} {% set load_cell_probe_reference_tare_counts = printer.configfile.settings.load_cell_probe.reference_tare_counts|default([71000]) %} {% set load_cell_probe_calibrated = (load_cell_probe_reference_tare_counts|sum) != (71000 * load_cell_probe_reference_tare_counts|length) %} - {% set extruder_control_type = printer.configfile.settings.extruder.control|default('pid')|lower %} - {% set extruder_pid_version = printer.configfile.settings.extruder.pid_version|default(0)|int %} + {% set extruder_config = printer.configfile.config.extruder %} + {% set extruder_control_type = extruder_config.control|default('pid')|lower %} + {% set extruder_pid_version = extruder_config.pid_version|default(0)|int %} {% set extruder_pid_calibrated = extruder_control_type != 'pid' or extruder_pid_version >= 1 %} {% set all_calibrated = shaper_calibrated and bed_mesh_default_exists and load_cell_probe_calibrated and extruder_pid_calibrated %} @@ -109,8 +110,9 @@ gcode: {% set bed_mesh_calibrated = 'bed_mesh default' in printer.configfile.settings %} {% set reference_tare_counts = printer.configfile.settings.load_cell_probe.reference_tare_counts|default([71000]) %} {% set load_cell_calibrated = (reference_tare_counts|sum) != (71000 * reference_tare_counts|length) %} - {% set extruder_control_type = printer.configfile.settings.extruder.control|default('pid')|lower %} - {% set extruder_pid_version = printer.configfile.settings.extruder.pid_version|default(0)|int %} + {% set extruder_config = printer.configfile.config.extruder %} + {% set extruder_control_type = extruder_config.control|default('pid')|lower %} + {% set extruder_pid_version = extruder_config.pid_version|default(0)|int %} {% set extruder_pid_calibrated = extruder_control_type != 'pid' or extruder_pid_version >= 1 %} {% set needs_shaper = force or not shaper_calibrated %} {% set needs_mesh = force or not bed_mesh_calibrated %} @@ -225,8 +227,9 @@ gcode: {% set bed_temperature = settings.full_calibrate_bed_temperature|int %} {% set state = printer["gcode_macro _CALIBRATE_ALL_STATE"] %} {% set force = state.force %} - {% set extruder_control_type = printer.configfile.settings.extruder.control|default('pid')|lower %} - {% set extruder_pid_version = printer.configfile.settings.extruder.pid_version|default(0)|int %} + {% set extruder_config = printer.configfile.config.extruder %} + {% set extruder_control_type = extruder_config.control|default('pid')|lower %} + {% set extruder_pid_version = extruder_config.pid_version|default(0)|int %} {% set extruder_pid_calibrated = extruder_control_type != 'pid' or extruder_pid_version >= 1 %} {% set bed_mesh_calibrated = 'bed_mesh default' in printer.configfile.settings %} {% set needs_pid = force or not extruder_pid_calibrated %} From 4b0ba66a5a0276709a5c220e0aee465c7cf39119 Mon Sep 17 00:00:00 2001 From: Paul Swenson <1937248+pdscomp@users.noreply.github.com> Date: Wed, 22 Jul 2026 21:36:06 -0400 Subject: [PATCH 12/20] kalico: update HX711 reliability revision Track OpenCentauri/kalico paul/hx711-reliability at 9044d16e49295c3c405601a3818c390a9951b213 and bump the recipe revision to r9. This pairs the Cosmos host package with the matching bed-MCU frame format and recovery fixes. --- .../recipes-apps/klipper/kalico_2026.02.00.inc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 56ca5b05..b7730f5d 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc +++ b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc @@ -4,7 +4,7 @@ LIC_FILES_CHKSUM = "file://COPYING;md5=1ebbd3e34237af26da5dc08a4e440464" FILESEXTRAPATHS:prepend := "${THISDIR}/files:" -SRC_URI = "git://github.com/OpenCentauri/kalico.git;protocol=https;branch=hx711s-new \ +SRC_URI = "git://github.com/OpenCentauri/kalico.git;protocol=https;branch=paul/hx711-reliability \ file://0001-remove-save-config-subfile-check.patch \ file://0002-reduce-calibration-difference-tolerance.patch \ file://0001-Reduce-log-rotate-threshold.patch \ @@ -16,9 +16,9 @@ SRC_URI = "git://github.com/OpenCentauri/kalico.git;protocol=https;branch=hx711s file://0002-persist-set-input-shaper.patch \ file://0003-adxl345-stream-raw-writes.patch \ " -SRCREV = "fa59d011f425c5aef6f06abc08efe43f7f6c569d" +SRCREV = "9044d16e49295c3c405601a3818c390a9951b213" -PR = "r8" +PR = "r9" S = "${WORKDIR}/git" From b1ffdff4429acb65c2f6c25c8fbb488efdd949bc Mon Sep 17 00:00:00 2001 From: Paul Swenson <1937248+pdscomp@users.noreply.github.com> Date: Wed, 22 Jul 2026 22:03:47 -0400 Subject: [PATCH 13/20] kalico: retry transient invalid HX711 probe samples Update the recipe to the pushed Kalico probe-layer retry fix. A single invalid HX711 sample is retracted and retried at the probe point while repeated invalid samples and unrelated safety errors remain fatal. --- meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 b7730f5d..961de030 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc +++ b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc @@ -16,9 +16,9 @@ SRC_URI = "git://github.com/OpenCentauri/kalico.git;protocol=https;branch=paul/h file://0002-persist-set-input-shaper.patch \ file://0003-adxl345-stream-raw-writes.patch \ " -SRCREV = "9044d16e49295c3c405601a3818c390a9951b213" +SRCREV = "9c1936efd4a07ea78b97bf4a50a3d23bb786ac29" -PR = "r9" +PR = "r10" S = "${WORKDIR}/git" From 1cd5e28770f3fe525356482d211648e2fe9c4f92 Mon Sep 17 00:00:00 2001 From: Paul Swenson <1937248+pdscomp@users.noreply.github.com> Date: Wed, 22 Jul 2026 23:39:39 -0400 Subject: [PATCH 14/20] kalico: retry single HX711 acquisition error during probing Track the pushed Kalico collector-boundary fix. A single HX711 acquisition error with no bulk overflow is converted to the existing retryable probe error; repeated errors and safety faults remain fatal. --- meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 961de030..b53c5faf 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc +++ b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc @@ -16,9 +16,9 @@ SRC_URI = "git://github.com/OpenCentauri/kalico.git;protocol=https;branch=paul/h file://0002-persist-set-input-shaper.patch \ file://0003-adxl345-stream-raw-writes.patch \ " -SRCREV = "9c1936efd4a07ea78b97bf4a50a3d23bb786ac29" +SRCREV = "64593629b31ab1baa05d30db8474c3e7e5ddb508" -PR = "r10" +PR = "r11" S = "${WORKDIR}/git" From f237e50b98e3a7654f570f1d6529ff16f82acfb7 Mon Sep 17 00:00:00 2001 From: Paul Swenson <1937248+pdscomp@users.noreply.github.com> Date: Thu, 23 Jul 2026 01:00:51 -0400 Subject: [PATCH 15/20] kalico: bump to 13952564 (HX711 timing-miss reclassification) paul/hx711-reliability 13952564 reclassifies HX711 timing misses as non-fatal sample skips with a stuck-ADC streak escalation. Test iteration build for carbon2u bed-mesh validation. --- meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 b53c5faf..78f71c2f 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc +++ b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc @@ -16,9 +16,9 @@ SRC_URI = "git://github.com/OpenCentauri/kalico.git;protocol=https;branch=paul/h file://0002-persist-set-input-shaper.patch \ file://0003-adxl345-stream-raw-writes.patch \ " -SRCREV = "64593629b31ab1baa05d30db8474c3e7e5ddb508" +SRCREV = "13952564f42e95a39b526e44b05b2f46e8fb7e29" -PR = "r11" +PR = "r12" S = "${WORKDIR}/git" From 2922c5e6316a0d2bd6087f74d6c17e4c3b007875 Mon Sep 17 00:00:00 2001 From: Paul Swenson <1937248+pdscomp@users.noreply.github.com> Date: Thu, 23 Jul 2026 08:00:51 -0400 Subject: [PATCH 16/20] kalico: bump to 426ac30b (drift-safety vs trigger-confirm race fix) --- meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 78f71c2f..ef66c7b4 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc +++ b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc @@ -16,9 +16,9 @@ SRC_URI = "git://github.com/OpenCentauri/kalico.git;protocol=https;branch=paul/h file://0002-persist-set-input-shaper.patch \ file://0003-adxl345-stream-raw-writes.patch \ " -SRCREV = "13952564f42e95a39b526e44b05b2f46e8fb7e29" +SRCREV = "426ac30bc9e6a05ddd77abfa19be664df779b6c2" -PR = "r12" +PR = "r13" S = "${WORKDIR}/git" From 87e8b6c17c4e32c6511dc6dacc6b94805c8e9234 Mon Sep 17 00:00:00 2001 From: Paul Swenson <1937248+pdscomp@users.noreply.github.com> Date: Thu, 23 Jul 2026 08:07:55 -0400 Subject: [PATCH 17/20] kalico: bump to a4ceadd3 (first-crossing trigger default) --- meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ef66c7b4..9fdf03f6 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc +++ b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc @@ -16,9 +16,9 @@ SRC_URI = "git://github.com/OpenCentauri/kalico.git;protocol=https;branch=paul/h file://0002-persist-set-input-shaper.patch \ file://0003-adxl345-stream-raw-writes.patch \ " -SRCREV = "426ac30bc9e6a05ddd77abfa19be664df779b6c2" +SRCREV = "a4ceadd374656a54fc222fb10b356063200ee86a" -PR = "r13" +PR = "r14" S = "${WORKDIR}/git" From 939bdbf69d48e57dee0517125897d76cc7958105 Mon Sep 17 00:00:00 2001 From: Paul Swenson <1937248+pdscomp@users.noreply.github.com> Date: Thu, 23 Jul 2026 08:57:54 -0400 Subject: [PATCH 18/20] kalico: bump to 87f1c4f7 (per-channel spike rejection) --- meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 9fdf03f6..f25ac491 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc +++ b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc @@ -16,9 +16,9 @@ SRC_URI = "git://github.com/OpenCentauri/kalico.git;protocol=https;branch=paul/h file://0002-persist-set-input-shaper.patch \ file://0003-adxl345-stream-raw-writes.patch \ " -SRCREV = "a4ceadd374656a54fc222fb10b356063200ee86a" +SRCREV = "87f1c4f710242f9df4cb8d1b8b410d5b5f3f8efb" -PR = "r14" +PR = "r15" S = "${WORKDIR}/git" From 72c4ab1308e611e337c7632c62d72e13958c2cc0 Mon Sep 17 00:00:00 2001 From: Paul Swenson <1937248+pdscomp@users.noreply.github.com> Date: Thu, 23 Jul 2026 09:52:29 -0400 Subject: [PATCH 19/20] macros: relax LOADCELL_Z_HOME cleaning-pass samples_tolerance to 0.1 The edge cleaning probes run right after the squish cycles physically mangle the contact (ooze + bent build plate): a valid sample can read >0.05mm off its siblings, aborting the macro on a step whose purpose is ooze management, not measurement. The final center probe keeps 0.05mm tolerance. Surfaced by the HX711 rewrite mesh campaign (attempt 5, 0.146mm spread at X120 Y-1 with 0.006mm isolated repeatability). --- meta-opencentauri/recipes-apps/klipper/files/macros.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meta-opencentauri/recipes-apps/klipper/files/macros.cfg b/meta-opencentauri/recipes-apps/klipper/files/macros.cfg index 4348d1a7..42d4508f 100644 --- a/meta-opencentauri/recipes-apps/klipper/files/macros.cfg +++ b/meta-opencentauri/recipes-apps/klipper/files/macros.cfg @@ -524,7 +524,7 @@ gcode: {% for _ in range(2) %} G90 G1 X120 Y-1 F9000 - PROBE HOME=Z SAMPLES=3 SAMPLES_TOLERANCE=0.05 BAD_PROBE_RETRIES=3 BAD_PROBE_STRATEGY=RETRY ; Cleaning passes + PROBE HOME=Z SAMPLES=3 SAMPLES_TOLERANCE=0.1 BAD_PROBE_RETRIES=3 BAD_PROBE_STRATEGY=RETRY ; Cleaning passes (ooze management, not measurement: the squish cycles mangle the edge contact, so a valid sample can legitimately deviate >0.05mm) G91 G0 X4 F600 G0 Z0.8 F300 ; Using pressure of bending the build plate to squish filament From caf0bbf40f2ee0fdbb7b542cf313edfc2d1374f8 Mon Sep 17 00:00:00 2001 From: Paul Swenson <1937248+pdscomp@users.noreply.github.com> Date: Thu, 23 Jul 2026 10:56:16 -0400 Subject: [PATCH 20/20] kalico: bump to c368f84e (bulk overflow tolerance) --- meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 f25ac491..3c3caa22 100644 --- a/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc +++ b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc @@ -16,9 +16,9 @@ SRC_URI = "git://github.com/OpenCentauri/kalico.git;protocol=https;branch=paul/h file://0002-persist-set-input-shaper.patch \ file://0003-adxl345-stream-raw-writes.patch \ " -SRCREV = "87f1c4f710242f9df4cb8d1b8b410d5b5f3f8efb" +SRCREV = "c368f84ec516f5d639aeddcb99d2c967957ab8e7" -PR = "r15" +PR = "r16" S = "${WORKDIR}/git"