""" gauge_vid6008.py — Automotive stepper gauge driver for ESP32 / MicroPython Supports two driver modes: 1. STEP/DIR mode (e.g. VID-6008): 2 pins — DIR and STEP Each step requires a pulse on STEP; DIR sets direction. 2. 4-PHASE mode (e.g. VID28/BKA30D/Switec X25): 4 pins — IN1..IN4 Driven via two H-bridge pairs (ULN2003 or direct GPIO). """ from machine import Pin import utime _TOTAL_STEPS = 3780 _OVERRUN_STEPS = 200 _FULL_SEQUENCE = [ (1, 0, 1, 0), (0, 1, 1, 0), (0, 1, 0, 1), (1, 0, 0, 1), ] class Gauge: """ Analog-style stepper gauge driver. Parameters ---------- pins : tuple[int, ...] 2 pins for STEP/DIR mode: (DIR, STEP) 4 pins for 4-phase mode: (IN1, IN2, IN3, IN4) mode : str "stepdir" for VID-6008 style (2 pins), "4phase" for VID28/X25 (4 pins). min_val : float Value that corresponds to the physical lower stop. max_val : float Value that corresponds to the physical upper stop. step_us : int Delay between steps in microseconds (default 200). """ def __init__(self, pins, mode="4phase", min_val=0, max_val=100, step_us=200): self._mode = mode self._step_us = step_us self._min_val = min_val self._max_val = max_val if mode == "stepdir": if len(pins) != 2: raise ValueError("stepdir mode requires 2 pins: (DIR, STEP)") self._pin_dir = Pin(pins[0], Pin.OUT) self._pin_step = Pin(pins[1], Pin.OUT) self._total_steps = _TOTAL_STEPS self._phase = 0 elif mode == "4phase": if len(pins) != 4: raise ValueError("4phase mode requires 4 pins: (IN1, IN2, IN3, IN4)") self._pins = [Pin(p, Pin.OUT) for p in pins] self._total_steps = _TOTAL_STEPS self._sequence = _FULL_SEQUENCE self._phase = 0 else: raise ValueError("mode must be 'stepdir' or '4phase'") self._current_step = 0 self._zeroed = False def zero(self): overrun = _OVERRUN_STEPS if self._mode == "stepdir": self._pin_dir.value(0) utime.sleep_us(10) for _ in range(_TOTAL_STEPS + overrun): self._pulse_step(500) else: for _ in range(_TOTAL_STEPS + overrun): self._step(-1) utime.sleep_ms(2) self._current_step = 0 self._zeroed = True self._release() def set(self, value, release=True): if not self._zeroed: raise RuntimeError("Call zero() before set()") value = max(self._min_val, min(self._max_val, value)) target_step = self._val_to_step(value) delta = target_step - self._current_step if delta == 0: return if self._mode == "stepdir": self._move_steps(delta) else: for _ in range(abs(delta)): self._step(1 if delta > 0 else -1) utime.sleep_ms(self._step_us) self._current_step = target_step if release: self._release() def _move_steps(self, delta): direction = 1 if delta > 0 else 0 self._pin_dir.value(direction) utime.sleep_us(10) for _ in range(abs(delta)): self._pulse_step(self._step_us) def _pulse_step(self, delay_us): self._pin_step.value(1) utime.sleep_us(2) self._pin_step.value(0) utime.sleep_us(delay_us) def get(self): return self._step_to_val(self._current_step) def release(self): self._release() def step(self, direction): if self._mode == "stepdir": self._pin_dir.value(1 if direction > 0 else 0) utime.sleep_us(10) self._pulse_step(self._step_us) self._current_step += direction else: self._step(direction) def _step(self, direction): steps_in_seq = len(self._sequence) self._phase = (self._phase + direction) % steps_in_seq seq = self._sequence[self._phase] for pin, state in zip(self._pins, seq): pin.value(state) self._current_step += direction def _release(self): if self._mode == "stepdir": self._pin_step.value(0) else: for pin in self._pins: pin.value(0) def _val_to_step(self, value): frac = (value - self._min_val) / (self._max_val - self._min_val) return int(round(frac * self._total_steps)) def _step_to_val(self, step): frac = step / self._total_steps return self._min_val + frac * (self._max_val - self._min_val) if __name__ == "__main__": g = Gauge( pins=(12, 13), mode="stepdir", min_val=0, max_val=100, step_us=200, ) print("Zeroing gauge...") g.zero() print("Zero complete.") for target in [25, 50, 75, 100, 50, 0]: print("Moving to {} (currently at {:.1f})".format(target, g.get())) g.set(target) utime.sleep_ms(500) print("Done.")