Skip to content

Commit 231bc25

Browse files
committed
PPK2 based power measurements seem to approximately work
1 parent ff20ad5 commit 231bc25

File tree

6 files changed

+95
-28
lines changed

6 files changed

+95
-28
lines changed

.vscode/settings.json

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"bitmask",
44
"boardid",
55
"Meshtastic",
6+
"milliwatt",
67
"powermon",
78
"pyarrow",
89
"TORADIO",

meshtastic/powermon/power_supply.py

+15-16
Original file line numberDiff line numberDiff line change
@@ -18,28 +18,27 @@ class PowerMeter:
1818
def __init__(self):
1919
"""Initialize the PowerMeter object."""
2020
self.prevPowerTime = datetime.now()
21-
self.prevWattHour = self._getRawWattHour()
2221

2322
def close(self) -> None:
2423
"""Close the power meter."""
2524

26-
def getAverageWatts(self) -> float:
27-
"""Get watts consumed since last call to this method."""
28-
now = datetime.now()
29-
nowWattHour = self._getRawWattHour()
30-
watts = (
31-
(nowWattHour - self.prevWattHour)
32-
/ (now - self.prevPowerTime).total_seconds()
33-
* 3600
34-
)
35-
self.prevPowerTime = now
36-
self.prevWattHour = nowWattHour
37-
return watts
38-
39-
def _getRawWattHour(self) -> float:
40-
"""Get the current watt-hour reading (without any offset correction)."""
25+
def get_average_current_mA(self) -> float:
26+
"""Returns average current of last measurement in mA (since last call to this method)"""
4127
return math.nan
4228

29+
def get_min_current_mA(self):
30+
"""Returns max current in mA (since last call to this method)."""
31+
# Subclasses must override for a better implementation
32+
return self.get_average_current_mA()
33+
34+
def get_max_current_mA(self):
35+
"""Returns max current in mA (since last call to this method)."""
36+
# Subclasses must override for a better implementation
37+
return self.get_average_current_mA()
38+
39+
def reset_measurements(self):
40+
"""Reset current measurements."""
41+
4342

4443
class PowerSupply(PowerMeter):
4544
"""Abstract class for power supplies."""

meshtastic/powermon/ppk2.py

+51-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Classes for logging power consumption of meshtastic devices."""
22

33
import logging
4+
import threading
5+
import time
46
from typing import Optional
57

68
from ppk2_api import ppk2_api # type: ignore[import-untyped]
@@ -31,27 +33,70 @@ def __init__(self, portName: Optional[str] = None):
3133

3234
self.r = r = ppk2_api.PPK2_MP(portName) # serial port will be different for you
3335
r.get_modifiers()
34-
self.r.start_measuring() # start measuring
3536

36-
logging.info("Connected to PPK2 power supply")
37+
self.r.start_measuring() # send command to ppk2
38+
self.current_measurements = [0.0] # reset current measurements to 0mA
39+
self.measuring = True
40+
41+
self.measurement_thread = threading.Thread(
42+
target=self.measurement_loop, daemon=True, name="ppk2 measurement"
43+
)
44+
self.measurement_thread.start()
45+
46+
logging.info("Connected to Power Profiler Kit II (PPK2)")
3747

3848
super().__init__() # we call this late so that the port is already open and _getRawWattHour callback works
3949

50+
def measurement_loop(self):
51+
"""Endless measurement loop will run in a thread."""
52+
while self.measuring:
53+
read_data = self.r.get_data()
54+
if read_data != b"":
55+
samples, _ = self.r.get_samples(read_data)
56+
self.current_measurements += samples
57+
time.sleep(0.001) # FIXME figure out correct sleep duration
58+
59+
def get_min_current_mA(self):
60+
"""Returns max current in mA (since last call to this method)."""
61+
return min(self.current_measurements) / 1000
62+
63+
def get_max_current_mA(self):
64+
"""Returns max current in mA (since last call to this method)."""
65+
return max(self.current_measurements) / 1000
66+
67+
def get_average_current_mA(self):
68+
"""Returns average current in mA (since last call to this method)."""
69+
average_current_mA = (
70+
sum(self.current_measurements) / len(self.current_measurements)
71+
) / 1000 # measurements are in microamperes, divide by 1000
72+
73+
return average_current_mA
74+
75+
def reset_measurements(self):
76+
"""Reset current measurements."""
77+
# Use the last reading as the new only reading (to ensure we always have a valid current reading)
78+
self.current_measurements = [ self.current_measurements[-1] ]
79+
4080
def close(self) -> None:
4181
"""Close the power meter."""
42-
self.r.stop_measuring()
82+
self.measuring = False
83+
self.r.stop_measuring() # send command to ppk2
84+
self.measurement_thread.join() # wait for our thread to finish
4385
super().close()
4486

4587
def setIsSupply(self, s: bool):
4688
"""If in supply mode we will provide power ourself, otherwise we are just an amp meter."""
89+
90+
self.r.set_source_voltage(
91+
int(self.v * 1000)
92+
) # set source voltage in mV BEFORE setting source mode
93+
# Note: source voltage must be set even if we are using the amp meter mode
94+
4795
if (
4896
not s
4997
): # min power outpuf of PPK2. If less than this assume we want just meter mode.
5098
self.r.use_ampere_meter()
5199
else:
52-
self.r.set_source_voltage(
53-
int(self.v * 1000)
54-
) # set source voltage in mV BEFORE setting source mode
55100
self.r.use_source_meter() # set source meter mode
56101

57102
def powerOn(self):

meshtastic/powermon/riden.py

+15
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ def __init__(self, portName: str = "/dev/ttyUSB0"):
2323
f"Connected to Riden power supply: model {r.type}, sn {r.sn}, firmware {r.fw}. Date/time updated."
2424
)
2525
r.set_date_time(datetime.now())
26+
self.prevWattHour = self._getRawWattHour()
27+
self.nowWattHour = self.prevWattHour
2628
super().__init__() # we call this late so that the port is already open and _getRawWattHour callback works
2729

2830
def setMaxCurrent(self, i: float):
@@ -36,6 +38,19 @@ def powerOn(self):
3638
) # my WM1110 devboard header is directly connected to the 3.3V rail
3739
self.r.set_output(1)
3840

41+
def get_average_current_mA(self) -> float:
42+
"""Returns average current of last measurement in mA (since last call to this method)"""
43+
now = datetime.now()
44+
nowWattHour = self._getRawWattHour()
45+
watts = (
46+
(nowWattHour - self.prevWattHour)
47+
/ (now - self.prevPowerTime).total_seconds()
48+
* 3600
49+
)
50+
self.prevPowerTime = now
51+
self.prevWattHour = nowWattHour
52+
return watts / 1000
53+
3954
def _getRawWattHour(self) -> float:
4055
"""Get the current watt-hour reading."""
4156
self.r.update()

meshtastic/powermon/sim.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
class SimPowerSupply(PowerSupply):
1010
"""A simulated power supply for testing."""
1111

12-
def getAverageWatts(self) -> float:
13-
"""Get the total amount of power that is currently being consumed."""
12+
def get_average_current_mA(self) -> float:
13+
"""Returns average current of last measurement in mA (since last call to this method)"""
1414

1515
# Sim a 20mW load that varies sinusoidally
16-
return (20 + 5 * math.sin(time.time())) / 1000
16+
return (20.0 + 5 * math.sin(time.time()))

meshtastic/slog/slog.py

+10-3
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,13 @@ def __init__(self, pMeter: PowerMeter, file_path: str, interval=0.2) -> None:
6565
def _logging_thread(self) -> None:
6666
"""Background thread for logging the current watts reading."""
6767
while self.is_logging:
68-
watts = self.pMeter.getAverageWatts()
69-
d = {"time": datetime.now(), "watts": watts}
68+
d = {
69+
"time": datetime.now(),
70+
"average_mW": self.pMeter.get_average_current_mA(),
71+
"max_mW": self.pMeter.get_max_current_mA(),
72+
"min_mW": self.pMeter.get_min_current_mA(),
73+
}
74+
self.pMeter.reset_measurements()
7075
self.writer.add_row(d)
7176
time.sleep(self.interval)
7277

@@ -164,7 +169,9 @@ def __init__(
164169

165170
logging.info(f"Writing slogs to {dir_name}")
166171

167-
self.slog_logger: Optional[StructuredLogger] = StructuredLogger(client, self.dir_name)
172+
self.slog_logger: Optional[StructuredLogger] = StructuredLogger(
173+
client, self.dir_name
174+
)
168175
self.power_logger: Optional[PowerLogger] = (
169176
None
170177
if not power_meter

0 commit comments

Comments
 (0)