From 9047e043a826aa16e9f9b112469ef4b0368d0f15 Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Tue, 17 Mar 2026 01:54:34 +0100 Subject: [PATCH] Added calibrated meter (usually for ampere meter) --- mipyiot/calibratedmeter.py | 95 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 mipyiot/calibratedmeter.py diff --git a/mipyiot/calibratedmeter.py b/mipyiot/calibratedmeter.py new file mode 100644 index 0000000..2dfca3b --- /dev/null +++ b/mipyiot/calibratedmeter.py @@ -0,0 +1,95 @@ +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)