From ea18057c1f8d39cc1b5571150bb62dcbe4940b10 Mon Sep 17 00:00:00 2001 From: Kevin Hester Date: Sun, 23 Jun 2024 18:42:15 -0700 Subject: [PATCH] Add support for NRF PPK2 power testing board. --- .vscode/launch.json | 2 +- meshtastic/__main__.py | 17 +++++++++- meshtastic/powermon/__init__.py | 4 ++- meshtastic/powermon/power_supply.py | 49 +++++++++++++++++++++++++++++ meshtastic/powermon/ppk2.py | 47 +++++++++++++++++++++++++++ meshtastic/powermon/riden.py | 48 +++------------------------- poetry.lock | 16 +++++++++- pyproject.toml | 1 + 8 files changed, 137 insertions(+), 47 deletions(-) create mode 100644 meshtastic/powermon/power_supply.py create mode 100644 meshtastic/powermon/ppk2.py diff --git a/.vscode/launch.json b/.vscode/launch.json index 49a3d71..f833f3a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -172,7 +172,7 @@ "request": "launch", "module": "meshtastic", "justMyCode": false, - "args": ["--power-riden", "/dev/ttyUSB0", "--port", "/dev/ttyACM0", "--noproto", "--seriallog", "stdout"] + "args": ["--power-ppk", "--power-voltage", "3.3", "--port", "/dev/ttyACM0", "--noproto", "--seriallog", "stdout"] }, { "name": "meshtastic test", diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 4a21e85..323d13e 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -21,7 +21,7 @@ from meshtastic import channel_pb2, config_pb2, portnums_pb2, remote_hardware, B from meshtastic.version import get_active_version from meshtastic.ble_interface import BLEInterface from meshtastic.mesh_interface import MeshInterface -from meshtastic.powermon import RidenPowerSupply +from meshtastic.powermon import RidenPowerSupply, PPK2PowerSupply from meshtastic.slog import StructuredLogger def onReceive(packet, interface): @@ -1093,6 +1093,16 @@ def common(): meter = None # assume no power meter if args.power_riden: meter = RidenPowerSupply(args.power_riden) + elif args.power_ppk2: + meter = PPK2PowerSupply() + + if meter and args.power_voltage: + v = float(args.power_voltage) + if v < 1.0 or v >5.0: + meshtastic.util.our_exit("Voltage must be between 1.0 and 5.0") + logging.info(f"Setting power supply to {v} volts") + meter.v = v + meter.powerOn() StructuredLogger(client, meter) @@ -1521,6 +1531,11 @@ def initParser(): action="store_true", ) + group.add_argument( + "--power-voltage", + help="Set the specified voltage on the power-supply. Be VERY careful, you can burn things up.", + ) + group.add_argument( "--power-stress", help="Perform power monitor stress testing, to capture a power consumption profile for the device (also requires --power-mon)", diff --git a/meshtastic/powermon/__init__.py b/meshtastic/powermon/__init__.py index d8b4104..c4aa654 100644 --- a/meshtastic/powermon/__init__.py +++ b/meshtastic/powermon/__init__.py @@ -1,3 +1,5 @@ """Support for logging from power meters/supplies.""" -from .riden import * +from .power_supply import PowerMeter, PowerSupply, PowerError +from .riden import RidenPowerSupply +from .ppk2 import PPK2PowerSupply \ No newline at end of file diff --git a/meshtastic/powermon/power_supply.py b/meshtastic/powermon/power_supply.py new file mode 100644 index 0000000..f87a4b8 --- /dev/null +++ b/meshtastic/powermon/power_supply.py @@ -0,0 +1,49 @@ +"""code logging power consumption of meshtastic devices.""" + +import math +from datetime import datetime + +class PowerError(Exception): + """An exception class for powermon errors""" + def __init__(self, message): + self.message = message + super().__init__(self.message) + + +class PowerMeter: + """Abstract class for power meters.""" + + def __init__(self): + """Initialize the PowerMeter object.""" + self.prevPowerTime = datetime.now() + self.prevWattHour = self._getRawWattHour() + + def getWatts(self) -> float: + """Get the total amount of power that has been consumed since the previous call of 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 + + + +class PowerSupply(PowerMeter): + """Abstract class for power supplies.""" + + def __init__(self): + """Initialize the PowerSupply object.""" + super().__init__() + self.v = 3.3 + + def powerOn(self): + """Turn on the power supply (using the voltage set in self.v).""" diff --git a/meshtastic/powermon/ppk2.py b/meshtastic/powermon/ppk2.py new file mode 100644 index 0000000..4fe4a88 --- /dev/null +++ b/meshtastic/powermon/ppk2.py @@ -0,0 +1,47 @@ +"""code logging power consumption of meshtastic devices.""" + +import logging +from typing import * + +from ppk2_api import ppk2_api +from .power_supply import PowerSupply, PowerError + + + +class PPK2PowerSupply(PowerSupply): + """Interface for talking with the NRF PPK2 high-resolution micro-power supply. + Power Profiler Kit II is what you should google to find it for purchase. + """ + + def __init__(self, portName: Optional[str] = None): + """Initialize the PowerSupply object. + + portName (str, optional): The port name of the power supply. Defaults to "/dev/ttyACM0". + """ + if not portName: + devs = ppk2_api.PPK2_API.list_devices() + if not devs or len(devs) == 0: + raise PowerError("No PPK2 devices found") + elif len(devs) > 1: + raise PowerError("Multiple PPK2 devices found, please specify the portName") + else: + portName = devs[0] + + self.r = r = ppk2_api.PPK2_MP(portName) # serial port will be different for you + r.get_modifiers() + + logging.info("Connected to PPK2 power supply") + + super().__init__() # we call this late so that the port is already open and _getRawWattHour callback works + + def powerOn(self): + """Power on the supply, with reasonable defaults for meshtastic devices.""" + self.r.use_source_meter() # set source meter mode + self.r.set_source_voltage(self.v * 1000) # set source voltage in mV + self.r.toggle_DUT_power("ON") + self.r.start_measuring() # start measuring + + + def _getRawWattHour(self) -> float: + """Get the current watt-hour reading.""" + return 4 # FIXME diff --git a/meshtastic/powermon/riden.py b/meshtastic/powermon/riden.py index 1dd65bc..17d5c02 100644 --- a/meshtastic/powermon/riden.py +++ b/meshtastic/powermon/riden.py @@ -1,52 +1,14 @@ """code logging power consumption of meshtastic devices.""" import logging -import math from datetime import datetime from riden import Riden - -class PowerMeter: - """Abstract class for power meters.""" - - def __init__(self): - """Initialize the PowerMeter object.""" - self.prevPowerTime = datetime.now() - self.prevWattHour = self._getRawWattHour() - - def getWatts(self) -> float: - """Get the total amount of power that has been consumed since the previous call of 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 - - - -class PowerSupply(PowerMeter): - """Abstract class for power supplies.""" - - def setMaxCurrent(self, i: float): - """Set the maximum current the supply will provide.""" - - def powerOn(self, v: float): - """Turn on the power supply.""" - - +from .power_supply import PowerSupply class RidenPowerSupply(PowerSupply): - """Interface for talking to programmable bench-top power supplies. - Currently only the Riden supplies are supported (RD6006 tested) + """Interface for talking to Riden programmable bench-top power supplies. + Only RD6006 tested but others should be similar. """ def __init__(self, portName: str = "/dev/ttyUSB0"): @@ -65,9 +27,9 @@ class RidenPowerSupply(PowerSupply): """Set the maximum current the supply will provide.""" self.r.set_i_set(i) - def powerOn(self, v: float): + def powerOn(self): """Power on the supply, with reasonable defaults for meshtastic devices.""" - self.r.set_v_set(v) # my WM1110 devboard header is directly connected to the 3.3V rail + self.r.set_v_set(self.v) # my WM1110 devboard header is directly connected to the 3.3V rail self.r.set_output(1) def _getRawWattHour(self) -> float: diff --git a/poetry.lock b/poetry.lock index 464c3eb..c18bdfb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -928,6 +928,20 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "ppk2-api" +version = "0.9.2" +description = "API for Nordic Semiconductor's Power Profiler Kit II (PPK 2)." +optional = false +python-versions = "*" +files = [ + {file = "ppk2-api-0.9.2.tar.gz", hash = "sha256:e8fb29f782ba6e5bd2e0286079163c495cfffce336f9b149430348c043b25300"}, + {file = "ppk2_api-0.9.2-py3-none-any.whl", hash = "sha256:b7fb02156f87d8430bbce0006876d38c8309ada671fbcd15848173b431198803"}, +] + +[package.dependencies] +pyserial = "*" + [[package]] name = "protobuf" version = "5.27.1" @@ -1794,4 +1808,4 @@ tunnel = [] [metadata] lock-version = "2.0" python-versions = "^3.9,<3.13" -content-hash = "cc81bcd3e4671bde3375f2f08334d6a3b87b38c97f2676d7266cab980082659e" +content-hash = "7719c01f112a7864acdec0a82e7131aa33a646d84459cc0cc6db0eadf94933c7" diff --git a/pyproject.toml b/pyproject.toml index 368c265..c767dd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ packaging = "^24.0" riden = {git = "https://github.com/geeksville/riden.git#1.2.1"} pandas = "^2.2.2" parse = "^1.20.2" +ppk2-api = "^0.9.2" [tool.poetry.group.dev.dependencies] hypothesis = "^6.103.2"