mirror of
https://github.com/meshtastic/python.git
synced 2026-05-19 14:01:09 -04:00
PPK2 based power measurements seem to approximately work
This commit is contained in:
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -3,6 +3,7 @@
|
|||||||
"bitmask",
|
"bitmask",
|
||||||
"boardid",
|
"boardid",
|
||||||
"Meshtastic",
|
"Meshtastic",
|
||||||
|
"milliwatt",
|
||||||
"powermon",
|
"powermon",
|
||||||
"pyarrow",
|
"pyarrow",
|
||||||
"TORADIO",
|
"TORADIO",
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user