from machine import PWM, Pin from array import array class CalibratedMeter: def __init__(self, pin, freq=1000, input_min=0, input_max=100, output_min=0, output_max=65535): self._pwm = PWM(Pin(pin), freq=freq) self._input_min = input_min self._input_max = input_max self._output_min = output_min self._output_max = output_max self._cal_inputs = array('f', [input_min, input_max]) self._cal = array('f', [output_min, output_max]) # --- Input range property --- @property def input_range(self): return (self._input_min, self._input_max) @input_range.setter def input_range(self, value): self._input_min, self._input_max = value # --- Output range property --- @property def output_range(self): return (self._output_min, self._output_max) @output_range.setter def output_range(self, value): self._output_min, self._output_max = value # --- Calibration --- def set_calibration(self, points): """ points: list of (input_value, duty_cycle) tuples input_value: in real-world units duty_cycle: must be within output_min and output_max Example: meter.set_calibration([ (0, 0), (25, 170), (50, 335), (75, 508), (100, 1023), ]) """ points = sorted(points, key=lambda p: p[0]) # Sanity checks for i, (inp, out) in enumerate(points): if not (self._output_min <= out <= self._output_max): raise ValueError( f"Calibration point {i}: duty {out} outside output range " f"[{self._output_min}, {self._output_max}]" ) if not (self._input_min <= inp <= self._input_max): raise ValueError( f"Calibration point {i}: input {inp} outside input range " f"[{self._input_min}, {self._input_max}]" ) self._cal_inputs = array('f', [p[0] for p in points]) self._cal = array('f', [p[1] for p in points]) # --- Internal helpers --- def _interpolate(self, value): """Linearly interpolate duty cycle from calibration table.""" if value <= self._cal_inputs[0]: return self._cal[0] if value >= self._cal_inputs[-1]: return self._cal[-1] for i in range(len(self._cal_inputs) - 1): x0, x1 = self._cal_inputs[i], self._cal_inputs[i + 1] if x0 <= value <= x1: y0, y1 = self._cal[i], self._cal[i + 1] t = (value - x0) / (x1 - x0) return y0 + t * (y1 - y0) return self._cal[-1] # --- Output --- def write(self, value): """Write a value in real-world units to the meter.""" value = max(self._input_min, min(self._input_max, value)) duty = int(self._interpolate(value)) duty = max(self._output_min, min(self._output_max, duty)) self._pwm.duty_u16(duty) def off(self): """Set meter to zero.""" self._pwm.duty_u16(self._output_min) def full(self): """Set meter to full scale.""" self._pwm.duty_u16(self._output_max)