PPK2 based power measurements seem to approximately work

This commit is contained in:
Kevin Hester
2024-06-25 15:19:21 -07:00
parent ff20ad5d05
commit 231bc25255
6 changed files with 95 additions and 28 deletions

View File

@@ -3,6 +3,7 @@
"bitmask", "bitmask",
"boardid", "boardid",
"Meshtastic", "Meshtastic",
"milliwatt",
"powermon", "powermon",
"pyarrow", "pyarrow",
"TORADIO", "TORADIO",

View File

@@ -18,28 +18,27 @@ class PowerMeter:
def __init__(self): def __init__(self):
"""Initialize the PowerMeter object.""" """Initialize the PowerMeter object."""
self.prevPowerTime = datetime.now() self.prevPowerTime = datetime.now()
self.prevWattHour = self._getRawWattHour()
def close(self) -> None: def close(self) -> None:
"""Close the power meter.""" """Close the power meter."""
def getAverageWatts(self) -> float: def get_average_current_mA(self) -> float:
"""Get watts consumed since last call to this method.""" """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
def _getRawWattHour(self) -> float:
"""Get the current watt-hour reading (without any offset correction)."""
return math.nan 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): class PowerSupply(PowerMeter):
"""Abstract class for power supplies.""" """Abstract class for power supplies."""

View File

@@ -1,6 +1,8 @@
"""Classes for logging power consumption of meshtastic devices.""" """Classes for logging power consumption of meshtastic devices."""
import logging import logging
import threading
import time
from typing import Optional from typing import Optional
from ppk2_api import ppk2_api # type: ignore[import-untyped] 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 self.r = r = ppk2_api.PPK2_MP(portName) # serial port will be different for you
r.get_modifiers() 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 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: def close(self) -> None:
"""Close the power meter.""" """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() super().close()
def setIsSupply(self, s: bool): def setIsSupply(self, s: bool):
"""If in supply mode we will provide power ourself, otherwise we are just an amp meter.""" """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 ( if (
not s not s
): # min power outpuf of PPK2. If less than this assume we want just meter mode. ): # min power outpuf of PPK2. If less than this assume we want just meter mode.
self.r.use_ampere_meter() self.r.use_ampere_meter()
else: 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 self.r.use_source_meter() # set source meter mode
def powerOn(self): def powerOn(self):

View File

@@ -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." f"Connected to Riden power supply: model {r.type}, sn {r.sn}, firmware {r.fw}. Date/time updated."
) )
r.set_date_time(datetime.now()) 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 super().__init__() # we call this late so that the port is already open and _getRawWattHour callback works
def setMaxCurrent(self, i: float): def setMaxCurrent(self, i: float):
@@ -36,6 +38,19 @@ class RidenPowerSupply(PowerSupply):
) # my WM1110 devboard header is directly connected to the 3.3V rail ) # my WM1110 devboard header is directly connected to the 3.3V rail
self.r.set_output(1) 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: def _getRawWattHour(self) -> float:
"""Get the current watt-hour reading.""" """Get the current watt-hour reading."""
self.r.update() self.r.update()

View File

@@ -9,8 +9,8 @@ from .power_supply import PowerSupply
class SimPowerSupply(PowerSupply): class SimPowerSupply(PowerSupply):
"""A simulated power supply for testing.""" """A simulated power supply for testing."""
def getAverageWatts(self) -> float: def get_average_current_mA(self) -> float:
"""Get the total amount of power that is currently being consumed.""" """Returns average current of last measurement in mA (since last call to this method)"""
# Sim a 20mW load that varies sinusoidally # Sim a 20mW load that varies sinusoidally
return (20 + 5 * math.sin(time.time())) / 1000 return (20.0 + 5 * math.sin(time.time()))

View File

@@ -65,8 +65,13 @@ class PowerLogger:
def _logging_thread(self) -> None: def _logging_thread(self) -> None:
"""Background thread for logging the current watts reading.""" """Background thread for logging the current watts reading."""
while self.is_logging: while self.is_logging:
watts = self.pMeter.getAverageWatts() d = {
d = {"time": datetime.now(), "watts": watts} "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) self.writer.add_row(d)
time.sleep(self.interval) time.sleep(self.interval)
@@ -164,7 +169,9 @@ class LogSet:
logging.info(f"Writing slogs to {dir_name}") 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] = ( self.power_logger: Optional[PowerLogger] = (
None None
if not power_meter if not power_meter