From 147022dee2e18b1b98ab478a5f00e390bcb58b30 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 20 Jan 2026 22:16:22 -0500 Subject: [PATCH] trigger_analog: Update to support generic trigger types Rework the trigger_analog code to support different "trigger" conditions. This merges in features of ldc1612.c into trigger_analog.c, such as error code reporting in the MCU. This is in preparation for using trigger_analog with ldc1612. Signed-off-by: Kevin O'Connor --- klippy/extras/ads1220.py | 3 + klippy/extras/hx71x.py | 3 + klippy/extras/load_cell_probe.py | 2 +- klippy/extras/trigger_analog.py | 87 ++++++---- src/trigger_analog.c | 265 +++++++++++++++++-------------- src/trigger_analog.h | 1 + 6 files changed, 202 insertions(+), 159 deletions(-) diff --git a/klippy/extras/ads1220.py b/klippy/extras/ads1220.py index 809c44435..891783922 100644 --- a/klippy/extras/ads1220.py +++ b/klippy/extras/ads1220.py @@ -122,6 +122,9 @@ class ADS1220: def get_samples_per_second(self): return self.sps + def lookup_sensor_error(self, error_code): + return "Unknown ads1220 error" % (error_code,) + # returns a tuple of the minimum and maximum value of the sensor, used to # detect if a data value is saturated def get_range(self): diff --git a/klippy/extras/hx71x.py b/klippy/extras/hx71x.py index e9d7bf605..a1bc20529 100644 --- a/klippy/extras/hx71x.py +++ b/klippy/extras/hx71x.py @@ -80,6 +80,9 @@ class HX71xBase: def get_samples_per_second(self): return self.sps + def lookup_sensor_error(self, error_code): + return "Unknown hx71x error %d" % (error_code,) + # returns a tuple of the minimum and maximum value of the sensor, used to # detect if a data value is saturated def get_range(self): diff --git a/klippy/extras/load_cell_probe.py b/klippy/extras/load_cell_probe.py index c15a57fd7..5f2a3c111 100644 --- a/klippy/extras/load_cell_probe.py +++ b/klippy/extras/load_cell_probe.py @@ -322,7 +322,7 @@ class LoadCellProbingMove: safety_min, safety_max = self._config_helper.get_safety_range(gcmd) self._mcu_trigger_analog.set_raw_range(safety_min, safety_max) trigger_val = self._config_helper.get_trigger_force_grams(gcmd) - self._mcu_trigger_analog.set_trigger_value(trigger_val) + self._mcu_trigger_analog.set_trigger("abs_ge", trigger_val) # update internal tare value gpc = self._config_helper.get_grams_per_count() sos_filter = self._mcu_trigger_analog.get_sos_filter() diff --git a/klippy/extras/trigger_analog.py b/klippy/extras/trigger_analog.py index 4e149cdf0..2a6952ccb 100644 --- a/klippy/extras/trigger_analog.py +++ b/klippy/extras/trigger_analog.py @@ -213,30 +213,28 @@ class MCU_SosFilter: # MCU_trigger_analog is the interface to `trigger_analog` on the MCU class MCU_trigger_analog: MONITOR_MAX = 3 - ERROR_SAFETY_RANGE = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 - ERROR_OVERFLOW = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 2 - ERROR_WATCHDOG = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 3 - ERROR_MAP = { - mcu.MCU_trsync.REASON_COMMS_TIMEOUT: "Communication timeout during " - "homing", - ERROR_SAFETY_RANGE: "sensor exceeds safety limit", - ERROR_OVERFLOW: "fixed point math overflow", - ERROR_WATCHDOG: "timed out waiting for sensor data" - } - + REASON_TRIGGER_ANALOG = mcu.MCU_trsync.REASON_COMMS_TIMEOUT + 1 def __init__(self, sensor_inst): self._printer = sensor_inst.get_mcu().get_printer() self._sensor = sensor_inst self._mcu = self._sensor.get_mcu() self._sos_filter = None - # configure MCU objects self._dispatch = mcu.TriggerDispatch(self._mcu) - self._oid = self._mcu.create_oid() + self._last_trigger_time = 0. + # Raw range checking self._raw_min = self._raw_max = 0 self._last_range_args = None + # Trigger type + self._trigger_type = "unspecified" self._trigger_value = 0. - self._last_trigger_time = 0. - self._home_cmd = self._query_cmd = self._set_range_cmd = None + self._last_trigger_args = None + # Error codes from MCU + self._error_map = {} + self._sensor_specific_error = 0 + # Configure MCU objects + self._oid = self._mcu.create_oid() + self._home_cmd = self._query_state_cmd = None + self._set_raw_range_cmd = self._set_trigger_cmd = None self._mcu.register_config_callback(self._build_config) def setup_sos_filter(self, sos_filter): @@ -251,17 +249,24 @@ class MCU_trigger_analog: "config_trigger_analog oid=%d sos_filter_oid=%d" % ( self._oid, self._sos_filter.get_oid())) # Lookup commands - self._query_cmd = self._mcu.lookup_query_command( + self._query_state_cmd = self._mcu.lookup_query_command( "trigger_analog_query_state oid=%c", - "trigger_analog_state oid=%c is_homing_trigger=%c " - "trigger_ticks=%u", oid=self._oid, cq=cmd_queue) - self._set_range_cmd = self._mcu.lookup_command( - "trigger_analog_set_range oid=%c safety_counts_min=%i" - " safety_counts_max=%i trigger_value=%i", cq=cmd_queue) + "trigger_analog_state oid=%c homing=%c trigger_clock=%u", + oid=self._oid, cq=cmd_queue) + self._set_raw_range_cmd = self._mcu.lookup_command( + "trigger_analog_set_raw_range oid=%c raw_min=%i raw_max=%i", + cq=cmd_queue) + self._set_trigger_cmd = self._mcu.lookup_command( + "trigger_analog_set_trigger oid=%c trigger_analog_type=%c" + " trigger_value=%i", cq=cmd_queue) self._home_cmd = self._mcu.lookup_command( "trigger_analog_home oid=%c trsync_oid=%c trigger_reason=%c" - " error_reason=%c clock=%u rest_ticks=%u timeout=%u", + " error_reason=%c clock=%u monitor_ticks=%u monitor_max=%u", cq=cmd_queue) + # Load errors from mcu + errors = self._mcu.get_enumerations().get("trigger_analog_error:", {}) + self._error_map = {v: k for k, v in errors.items()} + self._sensor_specific_error = errors.get("SENSOR_SPECIFIC", 0) def get_oid(self): return self._oid @@ -278,7 +283,8 @@ class MCU_trigger_analog: def get_last_trigger_time(self): return self._last_trigger_time - def set_trigger_value(self, trigger_value): + def set_trigger(self, trigger_type, trigger_value): + self._trigger_type = trigger_type self._trigger_value = trigger_value def set_raw_range(self, raw_min, raw_max): @@ -286,21 +292,24 @@ class MCU_trigger_analog: self._raw_max = raw_max def _reset_filter(self): - # Update parameters in mcu (if they have changed) - tval32 = self._sos_filter.convert_value(self._trigger_value) - args = [self._oid, self._raw_min, self._raw_max, tval32] + # Update raw range parameters in mcu (if they have changed) + args = [self._oid, self._raw_min, self._raw_max] if args != self._last_range_args: - self._set_range_cmd.send(args) + self._set_raw_range_cmd.send(args) self._last_range_args = args + # Update trigger in mcu (if it has changed) + tval32 = self._sos_filter.convert_value(self._trigger_value) + args = [self._oid, self._trigger_type, tval32] + if args != self._last_trigger_args: + self._set_trigger_cmd.send(args) + self._last_trigger_args = args # Update sos filter in mcu self._sos_filter.reset_filter() def _clear_home(self): - params = self._query_cmd.send([self._oid]) - # The time of the first sample that triggered is in "trigger_ticks" - trigger_ticks = self._mcu.clock32_to_clock64(params['trigger_ticks']) - # clear trsync from load_cell_endstop self._home_cmd.send([self._oid, 0, 0, 0, 0, 0, 0, 0]) + params = self._query_state_cmd.send([self._oid]) + trigger_ticks = self._mcu.clock32_to_clock64(params['trigger_clock']) return self._mcu.clock_to_print_time(trigger_ticks) def get_steppers(self): @@ -315,8 +324,8 @@ class MCU_trigger_analog: sensor_update = 1. / self._sensor.get_samples_per_second() sm_ticks = self._mcu.seconds_to_clock(sensor_update) self._home_cmd.send([self._oid, self._dispatch.get_oid(), - mcu.MCU_trsync.REASON_ENDSTOP_HIT, self.ERROR_SAFETY_RANGE, clock, - sm_ticks, self.MONITOR_MAX], reqclock=clock) + mcu.MCU_trsync.REASON_ENDSTOP_HIT, self.REASON_TRIGGER_ANALOG, + clock, sm_ticks, self.MONITOR_MAX], reqclock=clock) return trigger_completion def home_wait(self, home_end_time): @@ -326,8 +335,16 @@ class MCU_trigger_analog: # clear the homing state so it stops processing samples trigger_time = self._clear_home() if res >= mcu.MCU_trsync.REASON_COMMS_TIMEOUT: - defmsg = "unknown reason code %i" % (res,) - error_msg = self.ERROR_MAP.get(res, defmsg) + if res == mcu.MCU_trsync.REASON_COMMS_TIMEOUT: + raise self._printer.command_error( + "Communication timeout during homing") + error_code = res - self.REASON_TRIGGER_ANALOG + if error_code >= self._sensor_specific_error: + sensor_err = error_code - self._sensor_specific_error + error_msg = self._sensor.lookup_sensor_error(sensor_err) + else: + defmsg = "Unknown code %i" % (error_code,) + error_msg = self._error_map.get(error_code, defmsg) raise self._printer.command_error("Trigger analog error: %s" % (error_msg,)) if res != mcu.MCU_trsync.REASON_ENDSTOP_HIT: diff --git a/src/trigger_analog.c b/src/trigger_analog.c index fafc63fc6..f7ec477ec 100644 --- a/src/trigger_analog.c +++ b/src/trigger_analog.c @@ -1,11 +1,13 @@ // Support homing/probing "trigger" notification from analog sensors // // Copyright (C) 2025 Gareth Farrington +// Copyright (C) 2024-2026 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. #include // abs #include "basecmd.h" // oid_alloc +#include "board/io.h" // writeb #include "board/misc.h" // timer_read_time #include "command.h" // DECL_COMMAND #include "sched.h" // shutdown @@ -13,133 +15,149 @@ #include "trigger_analog.h" // trigger_analog_update #include "trsync.h" // trsync_do_trigger -#define ERROR_SAFETY_RANGE 0 -#define ERROR_OVERFLOW 1 -#define ERROR_WATCHDOG 2 - -// Flags -enum {FLAG_IS_HOMING = 1 << 0 - , FLAG_IS_HOMING_TRIGGER = 1 << 1 - , FLAG_AWAIT_HOMING = 1 << 2 -}; - -// Endstop Structure +// Main trigger_analog storage struct trigger_analog { - struct timer time; - uint32_t trigger_ticks, last_sample_ticks, rest_ticks; - uint32_t homing_start_time; - struct trsync *ts; - int32_t safety_counts_min, safety_counts_max; - uint8_t flags, trigger_reason, error_reason, watchdog_max, watchdog_count; - int32_t trigger_value; + // Raw value range check + int32_t raw_min, raw_max; + // Filtering struct sos_filter *sf; + // Trigger value checking + int32_t trigger_value; + uint8_t trigger_type; + // Trsync triggering + uint8_t flags, trigger_reason, error_reason; + struct trsync *ts; + uint32_t homing_clock, trigger_clock; + // Sensor activity monitoring + uint8_t monitor_max, monitor_count; + struct timer time; + uint32_t monitor_ticks; }; -static inline uint8_t -is_flag_set(const uint8_t mask, struct trigger_analog *ta) -{ - return !!(mask & ta->flags); -} +// Homing flags +enum { + TA_AWAIT_HOMING = 1<<1, TA_CAN_TRIGGER = 1<<2 +}; -static inline void -set_flag(uint8_t mask, struct trigger_analog *ta) -{ - ta->flags |= mask; -} +// Trigger types +enum { + TT_ABS_GE, TT_GT +}; +DECL_ENUMERATION("trigger_analog_type", "abs_ge", TT_ABS_GE); +DECL_ENUMERATION("trigger_analog_type", "gt", TT_GT); -static inline void -clear_flag(uint8_t mask, struct trigger_analog *ta) -{ - ta->flags &= ~mask; -} +// Sample errors sent via trsync error code +enum { + TE_RAW_RANGE, TE_OVERFLOW, TE_MONITOR, TE_SENSOR_SPECIFIC +}; +DECL_ENUMERATION("trigger_analog_error:", "RAW_RANGE", TE_RAW_RANGE); +DECL_ENUMERATION("trigger_analog_error:", "OVERFLOW", TE_OVERFLOW); +DECL_ENUMERATION("trigger_analog_error:", "MONITOR", TE_MONITOR); +DECL_ENUMERATION("trigger_analog_error:", "SENSOR_SPECIFIC" + , TE_SENSOR_SPECIFIC); -void -try_trigger(struct trigger_analog *ta, uint32_t ticks) +// Timer callback that monitors for sensor timeouts +static uint_fast8_t +monitor_event(struct timer *t) { - uint8_t is_homing_triggered = is_flag_set(FLAG_IS_HOMING_TRIGGER, ta); - if (!is_homing_triggered) { - // the first triggering sample when homing sets the trigger time - ta->trigger_ticks = ticks; - // this flag latches until a reset, disabling further triggering - set_flag(FLAG_IS_HOMING_TRIGGER, ta); - trsync_do_trigger(ta->ts, ta->trigger_reason); + struct trigger_analog *ta = container_of(t, struct trigger_analog, time); + + if (!(ta->flags & TA_CAN_TRIGGER)) + return SF_DONE; + + if (ta->monitor_count > ta->monitor_max) { + trsync_do_trigger(ta->ts, ta->error_reason + TE_MONITOR); + return SF_DONE; } + + // A sample was recently delivered, continue monitoring + ta->monitor_count++; + ta->time.waketime += ta->monitor_ticks; + return SF_RESCHEDULE; } -void -trigger_error(struct trigger_analog *ta, uint8_t error_code) +// Note recent activity +static void +monitor_note_activity(struct trigger_analog *ta) { + writeb(&ta->monitor_count, 0); +} + +// Check if a value should signal a "trigger" event +static int +check_trigger(struct trigger_analog *ta, int32_t value) +{ + switch (ta->trigger_type) { + case TT_ABS_GE: + return abs(value) >= ta->trigger_value; + case TT_GT: + return value > ta->trigger_value; + } + return 0; +} + +// Stop homing due to an error +static void +cancel_homing(struct trigger_analog *ta, uint8_t error_code) +{ + if (!(ta->flags & TA_CAN_TRIGGER)) + return; + ta->flags = 0; trsync_do_trigger(ta->ts, ta->error_reason + error_code); } +// Handle an error reported by the sensor +void +trigger_analog_note_error(struct trigger_analog *ta, uint8_t sensor_code) +{ + if (!ta) + return; + cancel_homing(ta, sensor_code + TE_SENSOR_SPECIFIC); +} + // Used by Sensors to report new raw ADC sample void -trigger_analog_update(struct trigger_analog *ta, const int32_t sample) +trigger_analog_update(struct trigger_analog *ta, int32_t sample) { + // Check homing is active if (!ta) return; + uint8_t flags = ta->flags; + if (!(flags & TA_CAN_TRIGGER)) + return; - // only process samples when homing - uint8_t is_homing = is_flag_set(FLAG_IS_HOMING, ta); - if (!is_homing) { + // Check if homing has started + uint32_t time = timer_read_time(); + if ((flags & TA_AWAIT_HOMING) && timer_is_before(time, ta->homing_clock)) + return; + flags &= ~TA_AWAIT_HOMING; + + // Reset the sensor timeout checking + monitor_note_activity(ta); + + // Check that raw value is in range + if (sample < ta->raw_min || sample > ta->raw_max) { + cancel_homing(ta, TE_RAW_RANGE); return; } - // save new sample - uint32_t ticks = timer_read_time(); - ta->last_sample_ticks = ticks; - ta->watchdog_count = 0; - - // do not trigger before homing start time - uint8_t await_homing = is_flag_set(FLAG_AWAIT_HOMING, ta); - if (await_homing && timer_is_before(ticks, ta->homing_start_time)) { - return; - } - clear_flag(FLAG_AWAIT_HOMING, ta); - - // check for safety limit violations - const uint8_t is_safety_trigger = sample <= ta->safety_counts_min - || sample >= ta->safety_counts_max; - // too much force, this is an error while homing - if (is_safety_trigger) { - trigger_error(ta, ERROR_SAFETY_RANGE); - return; - } - - // perform filtering + // Perform filtering int32_t filtered_value = sample; int ret = sos_filter_apply(ta->sf, &filtered_value); if (ret) { - trigger_error(ta, ERROR_OVERFLOW); + cancel_homing(ta, TE_OVERFLOW); return; } - // update trigger state - if (abs(filtered_value) >= ta->trigger_value) { - try_trigger(ta, ta->last_sample_ticks); - } -} - -// Timer callback that monitors for timeouts -static uint_fast8_t -watchdog_event(struct timer *t) -{ - struct trigger_analog *ta = container_of(t, struct trigger_analog, time); - uint8_t is_homing = is_flag_set(FLAG_IS_HOMING, ta); - uint8_t is_homing_trigger = is_flag_set(FLAG_IS_HOMING_TRIGGER, ta); - // the watchdog stops when not homing or when trsync becomes triggered - if (!is_homing || is_homing_trigger) { - return SF_DONE; + // Check if this is a "trigger" + ret = check_trigger(ta, filtered_value); + if (ret) { + trsync_do_trigger(ta->ts, ta->trigger_reason); + ta->trigger_clock = time; + flags = 0; } - if (ta->watchdog_count > ta->watchdog_max) { - trigger_error(ta, ERROR_WATCHDOG); - } - ta->watchdog_count += 1; - - // A sample was recently delivered, continue monitoring - ta->time.waketime += ta->rest_ticks; - return SF_RESCHEDULE; + ta->flags = flags; } // Create a trigger_analog @@ -160,17 +178,27 @@ trigger_analog_oid_lookup(uint8_t oid) return oid_lookup(oid, command_config_trigger_analog); } -// Set the triggering range and tare value +// Set valid raw range void -command_trigger_analog_set_range(uint32_t *args) +command_trigger_analog_set_raw_range(uint32_t *args) { struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); - ta->safety_counts_min = args[1]; - ta->safety_counts_max = args[2]; - ta->trigger_value = args[3]; + ta->raw_min = args[1]; + ta->raw_max = args[2]; } -DECL_COMMAND(command_trigger_analog_set_range, "trigger_analog_set_range" - " oid=%c safety_counts_min=%i safety_counts_max=%i trigger_value=%i"); +DECL_COMMAND(command_trigger_analog_set_raw_range, + "trigger_analog_set_raw_range oid=%c raw_min=%i raw_max=%i"); + +// Set the triggering type and value +void +command_trigger_analog_set_trigger(uint32_t *args) +{ + struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); + ta->trigger_type = args[1]; + ta->trigger_value = args[2]; +} +DECL_COMMAND(command_trigger_analog_set_trigger, "trigger_analog_set_trigger" + " oid=%c trigger_analog_type=%c trigger_value=%i"); // Home an axis void @@ -178,42 +206,33 @@ command_trigger_analog_home(uint32_t *args) { struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); sched_del_timer(&ta->time); - // clear the homing trigger flag - clear_flag(FLAG_IS_HOMING_TRIGGER, ta); - clear_flag(FLAG_IS_HOMING, ta); - ta->trigger_ticks = 0; - ta->ts = NULL; - // 0 samples indicates homing is finished - if (args[3] == 0) { - // Disable end stop checking + ta->monitor_ticks = args[5]; + if (!ta->monitor_ticks) { + ta->flags = 0; + ta->ts = NULL; return; } ta->ts = trsync_oid_lookup(args[1]); ta->trigger_reason = args[2]; ta->error_reason = args[3]; - ta->time.waketime = args[4]; - ta->homing_start_time = args[4]; - ta->rest_ticks = args[5]; - ta->watchdog_max = args[6]; - ta->watchdog_count = 0; - ta->time.func = watchdog_event; - set_flag(FLAG_IS_HOMING, ta); - set_flag(FLAG_AWAIT_HOMING, ta); + ta->time.waketime = ta->homing_clock = args[4]; + ta->monitor_max = args[6]; + ta->monitor_count = 0; + ta->time.func = monitor_event; + ta->flags = TA_AWAIT_HOMING | TA_CAN_TRIGGER; sched_add_timer(&ta->time); } DECL_COMMAND(command_trigger_analog_home, "trigger_analog_home oid=%c trsync_oid=%c trigger_reason=%c" - " error_reason=%c clock=%u rest_ticks=%u timeout=%u"); + " error_reason=%c clock=%u monitor_ticks=%u monitor_max=%u"); void command_trigger_analog_query_state(uint32_t *args) { uint8_t oid = args[0]; struct trigger_analog *ta = trigger_analog_oid_lookup(args[0]); - sendf("trigger_analog_state oid=%c is_homing_trigger=%c trigger_ticks=%u" - , oid - , is_flag_set(FLAG_IS_HOMING_TRIGGER, ta) - , ta->trigger_ticks); + sendf("trigger_analog_state oid=%c homing=%c trigger_clock=%u" + , oid, !!(ta->flags & TA_CAN_TRIGGER), ta->trigger_clock); } DECL_COMMAND(command_trigger_analog_query_state , "trigger_analog_query_state oid=%c"); diff --git a/src/trigger_analog.h b/src/trigger_analog.h index 9867095e8..53b36ce63 100644 --- a/src/trigger_analog.h +++ b/src/trigger_analog.h @@ -4,6 +4,7 @@ #include // uint8_t struct trigger_analog *trigger_analog_oid_lookup(uint8_t oid); +void trigger_analog_note_error(struct trigger_analog *ta, uint8_t sensor_code); void trigger_analog_update(struct trigger_analog *ta, int32_t sample); #endif // trigger_analog.h