Files
Micropython-IOT/mipyiot/calibratedmeter.py

96 lines
3.3 KiB
Python

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)