Skip to content

Commit 79cfcba

Browse files
committed
Analog input can now accept multiple triggers
1 parent df3c8f0 commit 79cfcba

File tree

2 files changed

+85
-38
lines changed

2 files changed

+85
-38
lines changed

devices/rotary_encoder.py

+3-6
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ def __init__(
99
name,
1010
sampling_rate,
1111
output="velocity",
12-
threshold=None,
13-
rising_event=None,
14-
falling_event=None,
12+
triggers=None,
1513
bytes_per_sample=2,
1614
reverse=False,
1715
):
@@ -28,14 +26,13 @@ def __init__(
2826
self.position = 0
2927
self.velocity = 0
3028
self.sampling_rate = sampling_rate
29+
3130
Analog_input.__init__(
3231
self,
3332
None,
3433
name,
3534
int(sampling_rate),
36-
threshold,
37-
rising_event,
38-
falling_event,
35+
triggers,
3936
data_type={2: "h", 4: "i"}[bytes_per_sample],
4037
)
4138

source/pyControl/hardware.py

+82-32
Original file line numberDiff line numberDiff line change
@@ -235,14 +235,8 @@ class Analog_input(IO_object):
235235
# streams data to computer. Optionally can generate framework events when voltage
236236
# goes above / below specified value theshold.
237237

238-
def __init__(self, pin, name, sampling_rate, threshold=None, rising_event=None, falling_event=None, data_type="H"):
239-
if rising_event or falling_event:
240-
if threshold is None:
241-
raise ValueError("A threshold must be specified if rising or falling events are defined.")
242-
self.threshold_watcher = Analog_threshold_watcher(threshold, rising_event, falling_event)
243-
else:
244-
self.threshold_watcher = False
245-
238+
def __init__(self, pin, name, sampling_rate, triggers=None, data_type="H"):
239+
self.triggers = triggers
246240
self.timer = pyb.Timer(available_timers.pop())
247241
if pin: # pin argument can be None when Analog_input subclassed.
248242
self.ADC = pyb.ADC(pin)
@@ -255,8 +249,6 @@ def _run_start(self):
255249
# Start sampling timer, initialise threshold, aquire first sample.
256250
self.timer.init(freq=self.channel.sampling_rate)
257251
self.timer.callback(self._timer_ISR)
258-
if self.threshold_watcher:
259-
self.threshold_watcher.run_start(self.read_sample())
260252
self._timer_ISR(0)
261253

262254
def _run_stop(self):
@@ -267,11 +259,9 @@ def _timer_ISR(self, t):
267259
# Read a sample to the buffer, update write index.
268260
sample = self.read_sample()
269261
self.channel.put(sample)
270-
if self.threshold_watcher:
271-
self.threshold_watcher.check(sample)
272-
273-
def change_threshold(self, new_threshold):
274-
self.threshold_watcher.set_threshold(new_threshold)
262+
if self.triggers:
263+
for trigger in self.triggers:
264+
trigger.check(sample)
275265

276266
def record(self): # For backward compatibility.
277267
pass
@@ -356,33 +346,89 @@ def send_buffer(self, run_stop=False):
356346
fw.usb_serial.send(self.buffers[buffer_n])
357347

358348

349+
class ValueTrigger(IO_object):
350+
# Generates framework events when an analog signal goes above or below specified threshold value.
351+
352+
def __init__(self, threshold, rising_event=None, falling_event=None):
353+
if rising_event is None and falling_event is None:
354+
raise ValueError("Either rising_event or falling_event or both must be specified.")
355+
self.rising_event = rising_event
356+
self.falling_event = falling_event
357+
self.set_threshold(threshold)
358+
self.timestamp = 0
359+
self.crossing_direction = False
360+
assign_ID(self)
361+
362+
def _initialise(self):
363+
# Set event codes for rising and falling events.
364+
self.rising_event_ID = sm.events[self.rising_event] if self.rising_event in sm.events else False
365+
self.falling_event_ID = sm.events[self.falling_event] if self.falling_event in sm.events else False
366+
self.threshold_active = self.rising_event_ID or self.falling_event_ID
367+
368+
def _process_interrupt(self):
369+
# Put event generated by threshold crossing in event queue.
370+
if self.crossing_direction:
371+
fw.event_queue.put(fw.Datatuple(self.timestamp, fw.EVENT_TYP, "i", self.rising_event_ID))
372+
else:
373+
fw.event_queue.put(fw.Datatuple(self.timestamp, fw.EVENT_TYP, "i", self.falling_event_ID))
374+
375+
@micropython.native
376+
def check(self, sample):
377+
if self.reset_above_threshold:
378+
# this gets run when the first sample is taken and whenever the threshold is changed
379+
self.reset_above_threshold = False
380+
self.above_threshold = sample > self.threshold
381+
return
382+
new_above_threshold = sample > self.threshold
383+
if new_above_threshold != self.above_threshold: # Threshold crossing.
384+
self.above_threshold = new_above_threshold
385+
if (self.above_threshold and self.rising_event_ID) or (not self.above_threshold and self.falling_event_ID):
386+
self.timestamp = fw.current_time
387+
self.crossing_direction = self.above_threshold
388+
389+
interrupt_queue.put(self.ID)
390+
391+
def set_threshold(self, threshold):
392+
if not isinstance(threshold, int):
393+
raise ValueError(f"Threshold must be an integer, got {type(threshold).__name__}.")
394+
self.threshold = threshold
395+
self.reset_above_threshold = True
396+
397+
359398
class Crossing:
360399
above = "above"
361400
below = "below"
362401
none = "none"
363402

364403

365-
class Analog_threshold_watcher(IO_object):
404+
class SchmittTrigger(IO_object):
366405
"""
367-
Generates framework events when an analog signal goes above or below specified threshold.
368-
If given single threshold value, rising event is triggered when signal > threshold, falling event is triggered when signal <= threshold.
369-
If given tuple of two threshold values, rising event is triggered when signal > upper bound, falling event is triggered when signal < lower bound.
406+
Generates framework events when an analog signal goes above an upper threshold and/or below a lower threshold.
407+
The rising event is triggered when signal > upper bound, falling event is triggered when signal < lower bound.
408+
409+
This trigger implements hysteresis, which is a technique to prevent rapid oscillations or "bouncing" of events:
410+
- Hysteresis creates a "dead zone" between the upper and lower thresholds
411+
- Once a rising event is triggered (when signal crosses above the upper bound),
412+
it cannot be triggered again until the signal has fallen below the lower bound
413+
- Similarly, once a falling event is triggered (when signal crosses below the lower bound),
414+
it cannot be triggered again until the signal has risen above the upper bound
415+
416+
This behavior is particularly useful for noisy signals that might otherwise rapidly cross a single threshold
417+
multiple times, generating unwanted repeated events.
370418
"""
371419

372-
def __init__(self, threshold, rising_event=None, falling_event=None):
373-
self.set_threshold(threshold)
420+
def __init__(self, bounds, rising_event=None, falling_event=None):
421+
if rising_event is None and falling_event is None:
422+
raise ValueError("Either rising_event or falling_event or both must be specified.")
423+
self.set_bounds(bounds)
374424
self.rising_event = rising_event
375425
self.falling_event = falling_event
376426
self.timestamp = 0
377-
self.last_crossing = Crossing.none
378427
assign_ID(self)
379428

380-
def set_threshold(self, threshold):
381-
if isinstance(threshold, int): # single threshold value
382-
self.upper_threshold = threshold
383-
self.lower_threshold = threshold + 1 # +1 so falling event is triggered when crossing into <= threshold
384-
elif isinstance(threshold, tuple):
385-
threshold_requirements_str = "The threshold must be a single integer or a tuple of two integers (lower_bound, upper_bound) where lower_bound <= upper_bound."
429+
def set_bounds(self, threshold):
430+
if isinstance(threshold, tuple):
431+
threshold_requirements_str = "The threshold must be a tuple of two integers (lower_bound, upper_bound) where lower_bound <= upper_bound."
386432
if len(threshold) != 2:
387433
raise ValueError("{} is not a valid threshold. {}".format(threshold, threshold_requirements_str))
388434
lower, upper = threshold
@@ -396,17 +442,14 @@ def set_threshold(self, threshold):
396442
self.lower_threshold = lower
397443
else:
398444
raise ValueError("{} is not a valid threshold. {}".format(threshold, threshold_requirements_str))
445+
self.reset_crossing = True
399446

400447
def _initialise(self):
401448
# Set event codes for rising and falling events.
402449
self.rising_event_ID = sm.events[self.rising_event] if self.rising_event in sm.events else False
403450
self.falling_event_ID = sm.events[self.falling_event] if self.falling_event in sm.events else False
404451
self.threshold_active = self.rising_event_ID or self.falling_event_ID
405452

406-
def run_start(self, sample):
407-
self.was_above = sample > self.upper_threshold
408-
self.was_below = sample < self.lower_threshold
409-
410453
def _process_interrupt(self):
411454
# Put event generated by threshold crossing in event queue.
412455
if self.was_above:
@@ -416,6 +459,13 @@ def _process_interrupt(self):
416459

417460
@micropython.native
418461
def check(self, sample):
462+
if self.reset_crossing:
463+
# this gets run when the first sample is taken and whenever the threshold is changed
464+
self.reset_crossing = False
465+
self.was_above = sample > self.upper_threshold
466+
self.was_below = sample < self.lower_threshold
467+
self.last_crossing = Crossing.none
468+
return
419469
is_above_threshold = sample > self.upper_threshold
420470
is_below_threshold = sample < self.lower_threshold
421471

0 commit comments

Comments
 (0)