diff --git a/.vscode/settings.json b/.vscode/settings.json index f393e65..9b0d16e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,7 @@ "bitmask", "boardid", "Meshtastic", + "milliwatt", "powermon", "pyarrow", "TORADIO", diff --git a/meshtastic/powermon/power_supply.py b/meshtastic/powermon/power_supply.py index 50a3a83..89e61b0 100644 --- a/meshtastic/powermon/power_supply.py +++ b/meshtastic/powermon/power_supply.py @@ -18,28 +18,27 @@ class PowerMeter: def __init__(self): """Initialize the PowerMeter object.""" self.prevPowerTime = datetime.now() - self.prevWattHour = self._getRawWattHour() def close(self) -> None: """Close the power meter.""" - def getAverageWatts(self) -> float: - """Get watts consumed since last call to this method.""" - now = datetime.now() - nowWattHour = self._getRawWattHour() - watts = ( - (nowWattHour - self.prevWattHour) - / (now - self.prevPowerTime).total_seconds() - * 3600 - ) - self.prevPowerTime = now - self.prevWattHour = nowWattHour - return watts - - def _getRawWattHour(self) -> float: - """Get the current watt-hour reading (without any offset correction).""" + def get_average_current_mA(self) -> float: + """Returns average current of last measurement in mA (since last call to this method)""" return math.nan + def get_min_current_mA(self): + """Returns max current in mA (since last call to this method).""" + # Subclasses must override for a better implementation + return self.get_average_current_mA() + + def get_max_current_mA(self): + """Returns max current in mA (since last call to this method).""" + # Subclasses must override for a better implementation + return self.get_average_current_mA() + + def reset_measurements(self): + """Reset current measurements.""" + class PowerSupply(PowerMeter): """Abstract class for power supplies.""" diff --git a/meshtastic/powermon/ppk2.py b/meshtastic/powermon/ppk2.py index 3ebf44f..53285fa 100644 --- a/meshtastic/powermon/ppk2.py +++ b/meshtastic/powermon/ppk2.py @@ -1,6 +1,8 @@ """Classes for logging power consumption of meshtastic devices.""" import logging +import threading +import time from typing import Optional from ppk2_api import ppk2_api # type: ignore[import-untyped] @@ -31,27 +33,70 @@ class PPK2PowerSupply(PowerSupply): self.r = r = ppk2_api.PPK2_MP(portName) # serial port will be different for you r.get_modifiers() - self.r.start_measuring() # start measuring - logging.info("Connected to PPK2 power supply") + self.r.start_measuring() # send command to ppk2 + self.current_measurements = [0.0] # reset current measurements to 0mA + self.measuring = True + + self.measurement_thread = threading.Thread( + target=self.measurement_loop, daemon=True, name="ppk2 measurement" + ) + self.measurement_thread.start() + + logging.info("Connected to Power Profiler Kit II (PPK2)") super().__init__() # we call this late so that the port is already open and _getRawWattHour callback works + def measurement_loop(self): + """Endless measurement loop will run in a thread.""" + while self.measuring: + read_data = self.r.get_data() + if read_data != b"": + samples, _ = self.r.get_samples(read_data) + self.current_measurements += samples + time.sleep(0.001) # FIXME figure out correct sleep duration + + def get_min_current_mA(self): + """Returns max current in mA (since last call to this method).""" + return min(self.current_measurements) / 1000 + + def get_max_current_mA(self): + """Returns max current in mA (since last call to this method).""" + return max(self.current_measurements) / 1000 + + def get_average_current_mA(self): + """Returns average current in mA (since last call to this method).""" + average_current_mA = ( + sum(self.current_measurements) / len(self.current_measurements) + ) / 1000 # measurements are in microamperes, divide by 1000 + + return average_current_mA + + def reset_measurements(self): + """Reset current measurements.""" + # Use the last reading as the new only reading (to ensure we always have a valid current reading) + self.current_measurements = [ self.current_measurements[-1] ] + def close(self) -> None: """Close the power meter.""" - self.r.stop_measuring() + self.measuring = False + self.r.stop_measuring() # send command to ppk2 + self.measurement_thread.join() # wait for our thread to finish super().close() def setIsSupply(self, s: bool): """If in supply mode we will provide power ourself, otherwise we are just an amp meter.""" + + self.r.set_source_voltage( + int(self.v * 1000) + ) # set source voltage in mV BEFORE setting source mode + # Note: source voltage must be set even if we are using the amp meter mode + if ( not s ): # min power outpuf of PPK2. If less than this assume we want just meter mode. self.r.use_ampere_meter() else: - self.r.set_source_voltage( - int(self.v * 1000) - ) # set source voltage in mV BEFORE setting source mode self.r.use_source_meter() # set source meter mode def powerOn(self): diff --git a/meshtastic/powermon/riden.py b/meshtastic/powermon/riden.py index c425855..fd84b3b 100644 --- a/meshtastic/powermon/riden.py +++ b/meshtastic/powermon/riden.py @@ -23,6 +23,8 @@ class RidenPowerSupply(PowerSupply): f"Connected to Riden power supply: model {r.type}, sn {r.sn}, firmware {r.fw}. Date/time updated." ) r.set_date_time(datetime.now()) + self.prevWattHour = self._getRawWattHour() + self.nowWattHour = self.prevWattHour super().__init__() # we call this late so that the port is already open and _getRawWattHour callback works def setMaxCurrent(self, i: float): @@ -36,6 +38,19 @@ class RidenPowerSupply(PowerSupply): ) # my WM1110 devboard header is directly connected to the 3.3V rail self.r.set_output(1) + def get_average_current_mA(self) -> float: + """Returns average current of last measurement in mA (since last call to this method)""" + now = datetime.now() + nowWattHour = self._getRawWattHour() + watts = ( + (nowWattHour - self.prevWattHour) + / (now - self.prevPowerTime).total_seconds() + * 3600 + ) + self.prevPowerTime = now + self.prevWattHour = nowWattHour + return watts / 1000 + def _getRawWattHour(self) -> float: """Get the current watt-hour reading.""" self.r.update() diff --git a/meshtastic/powermon/sim.py b/meshtastic/powermon/sim.py index e7e8484..796065b 100644 --- a/meshtastic/powermon/sim.py +++ b/meshtastic/powermon/sim.py @@ -9,8 +9,8 @@ from .power_supply import PowerSupply class SimPowerSupply(PowerSupply): """A simulated power supply for testing.""" - def getAverageWatts(self) -> float: - """Get the total amount of power that is currently being consumed.""" + def get_average_current_mA(self) -> float: + """Returns average current of last measurement in mA (since last call to this method)""" # Sim a 20mW load that varies sinusoidally - return (20 + 5 * math.sin(time.time())) / 1000 + return (20.0 + 5 * math.sin(time.time())) diff --git a/meshtastic/slog/slog.py b/meshtastic/slog/slog.py index 31e3a14..49ed62c 100644 --- a/meshtastic/slog/slog.py +++ b/meshtastic/slog/slog.py @@ -65,8 +65,13 @@ class PowerLogger: def _logging_thread(self) -> None: """Background thread for logging the current watts reading.""" while self.is_logging: - watts = self.pMeter.getAverageWatts() - d = {"time": datetime.now(), "watts": watts} + d = { + "time": datetime.now(), + "average_mW": self.pMeter.get_average_current_mA(), + "max_mW": self.pMeter.get_max_current_mA(), + "min_mW": self.pMeter.get_min_current_mA(), + } + self.pMeter.reset_measurements() self.writer.add_row(d) time.sleep(self.interval) @@ -164,7 +169,9 @@ class LogSet: logging.info(f"Writing slogs to {dir_name}") - self.slog_logger: Optional[StructuredLogger] = StructuredLogger(client, self.dir_name) + self.slog_logger: Optional[StructuredLogger] = StructuredLogger( + client, self.dir_name + ) self.power_logger: Optional[PowerLogger] = ( None if not power_meter