diff --git a/meta-opencentauri/images/files/overlayfs-etc-preinit.sh.in b/meta-opencentauri/images/files/overlayfs-etc-preinit.sh.in index d3a48249..34fc34d9 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/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/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/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/files/calibration.cfg b/meta-opencentauri/recipes-apps/klipper/files/calibration.cfg index 05cf425e..5af3879b 100644 --- a/meta-opencentauri/recipes-apps/klipper/files/calibration.cfg +++ b/meta-opencentauri/recipes-apps/klipper/files/calibration.cfg @@ -25,9 +25,10 @@ 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_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 %} SET_GCODE_VARIABLE MACRO=_CHECK_CALIBRATION_VARS VARIABLE=shaper_calibrated VALUE={shaper_calibrated} @@ -80,59 +81,237 @@ 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_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 %} + {% 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" - 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." - 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 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 %} + {% 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 + {% 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 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 + + {% 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 + 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 %} + {% 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 state = printer["gcode_macro _CALIBRATE_ALL_STATE"] %} + {% set force = state.force %} + {% 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 %} + {% 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 %} - _CLOSE_PROMPT + {% 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 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 - - 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} - - _SAVE_CONFIG_PROMPT + {% 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 %} + + 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 +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 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/files/macros.cfg b/meta-opencentauri/recipes-apps/klipper/files/macros.cfg index 23dcda23..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 @@ -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: 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/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/klipper/kalico_2026.02.00.inc b/meta-opencentauri/recipes-apps/klipper/kalico_2026.02.00.inc index 5767cedc..3c3caa22 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 \ @@ -12,10 +12,13 @@ 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 \ + file://0002-persist-set-input-shaper.patch \ + file://0003-adxl345-stream-raw-writes.patch \ " -SRCREV = "fa59d011f425c5aef6f06abc08efe43f7f6c569d" +SRCREV = "c368f84ec516f5d639aeddcb99d2c967957ab8e7" -PR = "r7" +PR = "r16" S = "${WORKDIR}/git" 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 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-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" 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." 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" 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 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 \