"""Read-only digital logic probe for a conditioned ESP32-C3 GPIO input. The ESP32 pin is always configured as an input with no internal pull. This is for observation only: it must receive a signal already conditioned to the ESP32-C3 input range. It cannot make an unknown coffee-machine wire safe. """ from machine import Pin, time_pulse_us import time class DigitalProbe: """Measure level, rising-edge frequency, pulse widths, and transitions.""" def __init__(self, pin_id): self.pin_id = int(pin_id) # Do not enable a pull resistor: the observed circuit must determine # the level, and the probe must not intentionally bias it. self.signal = Pin(self.pin_id, Pin.IN) self._edge_count = 0 def _on_rising_edge(self, _pin): # Keep IRQ work deliberately minimal. Do not print or allocate here. self._edge_count += 1 def state(self): """Print and return the present digital level (0 or 1).""" value = self.signal.value() print("GPIO%d = %d" % (self.pin_id, value)) return value def frequency(self, seconds=2): """Count rising edges during a fixed window and return approximate Hz.""" duration_ms = int(float(seconds) * 1000) if duration_ms <= 0: raise ValueError("seconds must be greater than zero") self._edge_count = 0 start = time.ticks_ms() self.signal.irq(trigger=Pin.IRQ_RISING, handler=self._on_rising_edge) try: time.sleep_ms(duration_ms) finally: self.signal.irq(handler=None) elapsed_ms = max(1, time.ticks_diff(time.ticks_ms(), start)) count = self._edge_count hz = (count * 1000.0) / elapsed_ms print("Freq: %.2f Hz (%d rising edges / %.3fs)" % ( hz, count, elapsed_ms / 1000.0)) return hz def pulse(self, timeout_us=1_000_000): """Measure one HIGH then one LOW pulse; return their timing data. ``time_pulse_us`` returns -2 when a requested level never arrives and -1 when that level does not end before the timeout. """ timeout_us = int(timeout_us) if timeout_us <= 0: raise ValueError("timeout_us must be greater than zero") high_us = time_pulse_us(self.signal, 1, timeout_us) low_us = time_pulse_us(self.signal, 0, timeout_us) if high_us < 0 or low_us < 0: print("No complete pulse: HIGH=%d LOW=%d" % (high_us, low_us)) print("-2 = level did not arrive; -1 = pulse did not end in time") return None period_us = high_us + low_us if period_us <= 0: print("No complete pulse") return None hz = 1_000_000.0 / period_us duty = (high_us * 100.0) / period_us result = { "high_us": high_us, "low_us": low_us, "period_us": period_us, "frequency_hz": hz, "duty_percent": duty, } print("HIGH: %dus LOW: %dus" % (high_us, low_us)) print("Freq: %.2f Hz Duty: %.1f%%" % (hz, duty)) return result def watch(self, seconds=5, max_events=200): """Poll and print transitions with microsecond timestamps. This is intended for short diagnostic captures. Narrow pulses can be missed because output and polling run in Python; use ``frequency`` or dedicated capture hardware for high-rate or timing-critical signals. """ duration_ms = int(float(seconds) * 1000) max_events = int(max_events) if duration_ms <= 0: raise ValueError("seconds must be greater than zero") if max_events <= 0: raise ValueError("max_events must be greater than zero") last = self.signal.value() start_us = time.ticks_us() deadline_ms = time.ticks_add(time.ticks_ms(), duration_ms) events = 0 while time.ticks_diff(deadline_ms, time.ticks_ms()) > 0: value = self.signal.value() if value == last: continue now_us = time.ticks_us() print("%9dus %d" % (time.ticks_diff(now_us, start_us), value)) last = value events += 1 if events >= max_events: print("Capture stopped after %d events" % max_events) break return events def open_probe(pin_id): """Create a probe and print the interactive REPL commands.""" probe = DigitalProbe(pin_id) print("ESP32-C3 read-only logic probe on GPIO%d" % probe.pin_id) print("Use: probe.state(), probe.frequency(), probe.pulse(), probe.watch()") return probe