Merge pull request #607 from geeksville/pr-powermon

PowerMon/StructuredLogging support
This commit is contained in:
Ian McEwen
2024-07-07 20:56:28 -07:00
committed by GitHub
34 changed files with 3695 additions and 167 deletions

View File

@@ -13,7 +13,6 @@ jobs:
strategy:
matrix:
python-version:
- "3.8"
- "3.9"
- "3.10"
- "3.11"
@@ -56,7 +55,6 @@ jobs:
strategy:
matrix:
python-version:
- "3.8"
- "3.9"
- "3.10"
- "3.11"

3
.gitignore vendored
View File

@@ -16,4 +16,5 @@ __pycache__
examples/__pycache__
meshtastic.spec
.hypothesis/
coverage.xml
coverage.xml
.ipynb_checkpoints

View File

@@ -23,7 +23,7 @@ ignore-patterns=mqtt_pb2.py,channel_pb2.py,telemetry_pb2.py,admin_pb2.py,config_
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
#
disable=invalid-name,fixme,logging-fstring-interpolation,too-many-statements,too-many-branches,too-many-locals,no-member,f-string-without-interpolation,protected-access,pointless-string-statement,too-few-public-methods,broad-except,no-else-return,no-else-raise,bare-except,too-many-public-methods
disable=invalid-name,fixme,logging-fstring-interpolation,too-many-statements,too-many-branches,too-many-locals,no-member,f-string-without-interpolation,protected-access,pointless-string-statement,too-few-public-methods,broad-except,no-else-return,no-else-raise,bare-except,too-many-public-methods,nested-min-max
[BASIC]

80
.vscode/launch.json vendored
View File

@@ -6,11 +6,11 @@
"configurations": [
{
"name": "meshtastic BLE",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": false,
"args": ["--ble", "Meshtastic_9f6e"]
"args": ["--ble", "--info", "--seriallog", "stdout"]
},
{
"name": "meshtastic BLE scan",
@@ -22,7 +22,7 @@
},
{
"name": "meshtastic admin",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
@@ -30,7 +30,7 @@
},
{
"name": "meshtastic tunnel",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
@@ -38,7 +38,7 @@
},
{
"name": "meshtastic set chan",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
@@ -46,7 +46,7 @@
},
{
"name": "meshtastic debug",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
@@ -54,7 +54,7 @@
},
{
"name": "meshtastic listen",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
@@ -62,7 +62,7 @@
},
{
"name": "meshtastic debug getPref",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
@@ -70,7 +70,7 @@
},
{
"name": "meshtastic debug getPref telemetry",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
@@ -78,7 +78,7 @@
},
{
"name": "meshtastic debug info",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
@@ -94,7 +94,7 @@
},
{
"name": "meshtastic debug set region",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
@@ -102,7 +102,7 @@
},
{
"name": "meshtastic debug set bluetooth fixed pin",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
@@ -110,7 +110,7 @@
},
{
"name": "meshtastic debug get bluetooth fixed pin",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
@@ -118,15 +118,15 @@
},
{
"name": "meshtastic debug setPref",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
"args": ["--debug", "--set", "power.is_power_saving", "1"]
"args": ["--set", "power.powermon_enables", "65527"]
},
{
"name": "meshtastic debug setPref telemetry.environment_measurement_enabled",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
@@ -134,7 +134,7 @@
},
{
"name": "meshtastic debug setPref telemetry.environment_screen_enabled",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
@@ -142,7 +142,7 @@
},
{
"name": "meshtastic debug setPref telemetry",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
@@ -150,7 +150,7 @@
},
{
"name": "meshtastic setpref",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
@@ -158,7 +158,7 @@
},
{
"name": "meshtastic --ch-set",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
@@ -167,7 +167,7 @@
{
"name": "meshtastic seturl",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
@@ -176,15 +176,39 @@
},
{
"name": "meshtastic shell",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
"args": ["--debug", "--seriallog"]
"justMyCode": false,
"args": ["--noproto", "--seriallog", "stdout"]
},
{
"name": "meshtastic powermon sim",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": false,
"args": ["--slog-out", "default", "--power-sim", "--power-voltage", "3.3", "--port", "/dev/ttyUSB0", "--noproto", "--seriallog", "stdout"]
},
{
"name": "meshtastic powermon ppk2",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": false,
"args": ["--slog-out", "default", "--power-ppk2-meter", "--power-wait", "--power-voltage", "3.3", "--noproto", "--seriallog", "stdout"]
},
{
"name": "meshtastic stress ppk2",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": false,
"args": ["--slog-out", "default", "--power-ppk2-meter", "--power-stress", "--power-voltage", "3.3", "--seriallog"]
},
{
"name": "meshtastic test",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
@@ -192,7 +216,7 @@
},
{
"name": "meshtastic settime",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
@@ -200,7 +224,7 @@
},
{
"name": "meshtastic sendtext",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,
@@ -208,7 +232,7 @@
},
{
"name": "meshtastic showNodes",
"type": "python",
"type": "debugpy",
"request": "launch",
"module": "meshtastic",
"justMyCode": true,

10
.vscode/settings.json vendored
View File

@@ -1,8 +1,16 @@
{
"cSpell.words": [
"bitmask",
"boardid",
"Meshtastic",
"milliwatt",
"portnums",
"powermon",
"POWERSTRESS",
"pyarrow",
"TORADIO",
"Vids"
],
"python.pythonPath": "/usr/bin/python3"
"python.pythonPath": "/usr/bin/python3",
"flake8.enabled" : false // we are using trunk for formatting/linting rules, don't yell at us about line length
}

View File

@@ -2,6 +2,12 @@ set -e
# You may consider running: "pytest -m smoke1" instead of this test.
echo "Linting"
poetry run pylint meshtastic examples/ --ignore-patterns ".*_pb2.pyi?$"
echo "Checking types"
poetry run mypy meshtastic/
echo "Running (crude) prerelease tests to verify sanity"
# Use the python environment created by poetry

View File

@@ -31,6 +31,7 @@ type of packet, you should subscribe to the full topic name. If you want to see
- meshtastic.receive.user(packet)
- meshtastic.receive.data.portnum(packet) (where portnum is an integer or well known PortNum enum)
- meshtastic.node.updated(node = NodeInfo) - published when a node in the DB changes (appears, location changed, username changed, etc...)
- meshtastic.log.line(line) - a raw unparsed log line from the radio
We receive position, user, or data packets from the mesh. You probably only care about meshtastic.receive.data. The first argument for
that publish will be the packet. Text or binary data packets (from sendData or sendText) will both arrive this way. If you print packet
@@ -96,6 +97,7 @@ from .protobuf import (
remote_hardware_pb2,
storeforward_pb2,
telemetry_pb2,
powermon_pb2
)
from . import (
util,
@@ -228,6 +230,9 @@ protocols = {
portnums_pb2.PortNum.TRACEROUTE_APP: KnownProtocol(
"traceroute", mesh_pb2.RouteDiscovery
),
portnums_pb2.PortNum.POWERSTRESS_APP: KnownProtocol(
"powerstress", powermon_pb2.PowerStressMessage
),
portnums_pb2.PortNum.WAYPOINT_APP: KnownProtocol("waypoint", mesh_pb2.Waypoint),
portnums_pb2.PortNum.PAXCOUNTER_APP: KnownProtocol("paxcounter", paxcount_pb2.Paxcount),
portnums_pb2.PortNum.STORE_FORWARD_APP: KnownProtocol("storeforward", storeforward_pb2.StoreAndForward),

View File

@@ -1,13 +1,17 @@
#!python3
""" Main Meshtastic
"""
# We just hit the 1600 line limit for main.py, but I currently have a huge set of powermon/structured logging changes
# later we can have a separate changelist to refactor main.py into smaller files
# pylint: disable=too-many-lines
import argparse
import logging
import os
import platform
import sys
import time
from typing import Optional
import pyqrcode # type: ignore[import-untyped]
import yaml
@@ -22,6 +26,10 @@ from meshtastic import remote_hardware, BROADCAST_ADDR
from meshtastic.version import get_active_version
from meshtastic.ble_interface import BLEInterface
from meshtastic.mesh_interface import MeshInterface
from meshtastic.powermon import RidenPowerSupply, PPK2PowerSupply, SimPowerSupply, PowerStress, PowerMeter
from meshtastic.slog import LogSet
meter: Optional[PowerMeter] = None
def onReceive(packet, interface):
"""Callback invoked when a packet arrives"""
@@ -847,6 +855,15 @@ def onConnected(interface):
qr = pyqrcode.create(url)
print(qr.terminal())
if args.slog or args.power_stress:
# Setup loggers
global meter # pylint: disable=global-variable-not-assigned
LogSet(interface, args.slog if args.slog != 'default' else None, meter)
if args.power_stress:
stress = PowerStress(interface)
stress.run()
if args.listen:
closeNow = False
@@ -984,9 +1001,32 @@ def export_config(interface):
print(config)
return config
def create_power_meter():
"""Setup the power meter."""
global meter # pylint: disable=global-statement
args = mt_config.args
if args.power_riden:
meter = RidenPowerSupply(args.power_riden)
elif args.power_ppk2_supply or args.power_ppk2_meter:
meter = PPK2PowerSupply()
meter.setIsSupply(args.power_ppk2_supply)
elif args.power_sim:
meter = SimPowerSupply()
if meter and args.power_voltage:
v = float(args.power_voltage)
if v < 0.5 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()
if args.power_wait:
input("Powered on, press enter to continue...")
def common():
"""Shared code for all of our command line wrappers"""
"""Shared code for all of our command line wrappers."""
logfile = None
args = mt_config.args
parser = mt_config.parser
@@ -1003,6 +1043,8 @@ def common():
meshtastic.util.support_info()
meshtastic.util.our_exit("", 0)
create_power_meter()
if args.ch_index is not None:
channelIndex = int(args.ch_index)
mt_config.channel_index = channelIndex
@@ -1084,7 +1126,6 @@ def common():
f"Error connecting to localhost:{ex}", 1
)
# We assume client is fully connected now
onConnected(client)
@@ -1092,8 +1133,11 @@ def common():
if (
args.noproto or args.reply or (have_tunnel and args.tunnel) or args.listen
): # loop until someone presses ctrlc
while True:
time.sleep(1000)
try:
while True:
time.sleep(1000)
except KeyboardInterrupt:
logging.info("Exiting due to keyboard interrupt")
# don't call exit, background threads might be running still
# sys.exit(0)
@@ -1498,6 +1542,58 @@ def initParser():
action="store_true",
)
power_group = parser.add_argument_group('Power Testing', 'Options for power testing/logging.')
power_supply_group = power_group.add_mutually_exclusive_group()
power_supply_group.add_argument(
"--power-riden",
help="Talk to a Riden power-supply. You must specify the device path, i.e. /dev/ttyUSBxxx",
)
power_supply_group.add_argument(
"--power-ppk2-meter",
help="Talk to a Nordic Power Profiler Kit 2 (in meter mode)",
action="store_true",
)
power_supply_group.add_argument(
"--power-ppk2-supply",
help="Talk to a Nordic Power Profiler Kit 2 (in supply mode)",
action="store_true",
)
power_supply_group.add_argument(
"--power-sim",
help="Use a simulated power meter (for development)",
action="store_true",
)
power_group.add_argument(
"--power-voltage",
help="Set the specified voltage on the power-supply. Be VERY careful, you can burn things up.",
)
power_group.add_argument(
"--power-stress",
help="Perform power monitor stress testing, to capture a power consumption profile for the device (also requires --power-mon)",
action="store_true",
)
power_group.add_argument(
"--power-wait",
help="Prompt the user to wait for device reset before looking for device serial ports (some boards kill power to USB serial port)",
action="store_true",
)
power_group.add_argument(
"--slog",
help="Store structured-logs (slogs) for this run, optionally you can specifiy a destination directory",
nargs="?",
default=None,
const="default"
)
group.add_argument(
"--ble-scan",
help="Scan for Meshtastic BLE devices",

View File

@@ -0,0 +1 @@
"""Post-run analysis tools for meshtastic."""

View File

@@ -7,7 +7,6 @@ import struct
import time
from threading import Thread
from typing import List, Optional
import print_color # type: ignore[import-untyped]
from bleak import BleakClient, BleakScanner, BLEDevice
from bleak.exc import BleakDBusError, BleakError
@@ -98,30 +97,11 @@ class BLEInterface(MeshInterface):
return
message = f'[{log_record.source}] {log_record.message}' if log_record.source else log_record.message
if log_record.DEBUG:
print_color.print(message, color="cyan", end=None)
elif log_record.INFO:
print_color.print(message, color="white", end=None)
elif log_record.WARNING:
print_color.print(message, color="yellow", end=None)
elif log_record.ERROR:
print_color.print(message, color="red", end=None)
else:
print_color.print(message, end=None)
self._handleLogLine(message)
async def legacy_log_radio_handler(self, _, b): # pylint: disable=C0116
log_radio = b.decode("utf-8").replace("\n", "")
if log_radio.startswith("DEBUG"):
print_color.print(log_radio, color="cyan", end=None)
elif log_radio.startswith("INFO"):
print_color.print(log_radio, color="white", end=None)
elif log_radio.startswith("WARN"):
print_color.print(log_radio, color="yellow", end=None)
elif log_radio.startswith("ERROR"):
print_color.print(log_radio, color="red", end=None)
else:
print_color.print(log_radio, end=None)
self._handleLogLine(log_radio)
@staticmethod
def scan() -> List[BLEDevice]:

View File

@@ -16,6 +16,7 @@ from typing import Any, Callable, Dict, List, Optional, Union
import google.protobuf.json_format
from pubsub import pub # type: ignore[import-untyped]
from tabulate import tabulate
import print_color # type: ignore[import-untyped]
import meshtastic.node
@@ -31,7 +32,7 @@ from meshtastic import (
NODELESS_WANT_CONFIG_ID,
ResponseHandler,
protocols,
publishingThread,
publishingThread
)
from meshtastic.util import (
Acknowledgment,
@@ -43,7 +44,6 @@ from meshtastic.util import (
message_to_json,
)
def _timeago(delta_secs: int) -> str:
"""Convert a number of seconds in the past into a short, friendly string
e.g. "now", "30 sec ago", "1 hour ago"
@@ -117,6 +117,12 @@ class MeshInterface: # pylint: disable=R0902
self.queue: collections.OrderedDict = collections.OrderedDict()
self._localChannels = None
# We could have just not passed in debugOut to MeshInterface, and instead told consumers to subscribe to
# the meshtastic.log.line publish instead. Alas though changing that now would be a breaking API change
# for any external consumers of the library.
if debugOut:
pub.subscribe(MeshInterface._printLogLine, "meshtastic.log.line")
def close(self):
"""Shutdown this interface"""
if self.heartbeatTimer:
@@ -136,6 +142,33 @@ class MeshInterface: # pylint: disable=R0902
logging.error(f"Traceback: {traceback}")
self.close()
@staticmethod
def _printLogLine(line, interface):
"""Print a line of log output."""
if interface.debugOut == sys.stdout:
# this isn't quite correct (could cause false positives), but currently our formatting differs between different log representations
if "DEBUG" in line:
print_color.print(line, color="cyan", end=None)
elif "INFO" in line:
print_color.print(line, color="white", end=None)
elif "WARN" in line:
print_color.print(line, color="yellow", end=None)
elif "ERR" in line:
print_color.print(line, color="red", end=None)
else:
print_color.print(line, end=None)
else:
interface.debugOut.write(line + "\n")
def _handleLogLine(self, line: str) -> None:
"""Handle a line of log output from the device."""
pub.sendMessage("meshtastic.log.line", line=line, interface=self)
def _handleLogRecord(self, record: mesh_pb2.LogRecord) -> None:
"""Handle a log record which was received encapsulated in a protobuf."""
# For now we just try to format the line as if it had come in over the serial port
self._handleLogLine(record.message)
def showInfo(self, file=sys.stdout) -> str: # pylint: disable=W0613
"""Show human readable summary about this object"""
owner = f"Owner: {self.getLongName()} ({self.getShortName()})"
@@ -924,7 +957,8 @@ class MeshInterface: # pylint: disable=R0902
self._handleChannel(fromRadio.channel)
elif fromRadio.HasField("packet"):
self._handlePacketFromRadio(fromRadio.packet)
elif fromRadio.HasField("log_record"):
self._handleLogRecord(fromRadio.log_record)
elif fromRadio.HasField("queueStatus"):
self._handleQueueStatusFromRadio(fromRadio.queueStatus)

View File

@@ -0,0 +1,7 @@
"""Support for logging from power meters/supplies."""
from .power_supply import PowerError, PowerMeter, PowerSupply
from .ppk2 import PPK2PowerSupply
from .riden import RidenPowerSupply
from .sim import SimPowerSupply
from .stress import PowerStress

View File

@@ -0,0 +1,52 @@
"""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()
def close(self) -> None:
"""Close the power meter."""
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."""
def __init__(self):
"""Initialize the PowerSupply object."""
super().__init__()
self.v = 0.0
def powerOn(self):
"""Turn on the power supply (using the voltage set in self.v)."""

156
meshtastic/powermon/ppk2.py Normal file
View File

@@ -0,0 +1,156 @@
"""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]
from .power_supply import PowerError, PowerSupply
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.measuring = False
self.current_max = 0
self.current_min = 0
self.current_sum = 0
self.current_num_samples = 0
# for tracking avera data read length (to determine if we are sleeping efficiently in measurement_loop)
self.total_data_len = 0
self.num_data_reads = 0
self.max_data_len = 0
self.r = r = ppk2_api.PPK2_API(
portName
) # serial port will be different for you
r.get_modifiers()
self.measurement_thread = threading.Thread(
target=self.measurement_loop, daemon=True, name="ppk2 measurement"
)
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:
# always reads 4096 bytes, even if there is no new samples - or possibly the python single thread (because of global interpreter lock)
# is always behind and thefore we are inherently dropping samples semi randomly!!!
read_data = self.r.get_data()
if read_data != b"":
samples, _ = self.r.get_samples(read_data)
# update invariants
if len(samples) > 0:
if self.current_num_samples == 0:
self.current_min = samples[
0
] # we need at least one sample to get an initial min
self.current_max = max(self.current_max, max(samples))
self.current_min = min(self.current_min, min(samples))
self.current_sum += sum(samples)
self.current_num_samples += len(samples)
# logging.debug(f"PPK2 data_len={len(read_data)}, sample_len={len(samples)}")
self.num_data_reads += 1
self.total_data_len += len(read_data)
self.max_data_len = max(self.max_data_len, len(read_data))
time.sleep(0.01) # FIXME figure out correct sleep duration
def get_min_current_mA(self):
"""Returns max current in mA (since last call to this method)."""
return self.current_min / 1000
def get_max_current_mA(self):
"""Returns max current in mA (since last call to this method)."""
return self.current_max / 1000
def get_average_current_mA(self):
"""Returns average current in mA (since last call to this method)."""
if self.current_num_samples == 0:
return 0
else:
return (
self.current_sum / self.current_num_samples / 1000
) # measurements are in microamperes, divide by 1000
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_max = 0
self.current_min = 0
self.current_sum = 0
self.current_num_samples = 0
# if self.num_data_reads:
# logging.debug(f"max data len = {self.max_data_len},avg {self.total_data_len/self.num_data_reads}, num reads={self.num_data_reads}")
# Summary stats for performance monitoring
self.num_data_reads = 0
self.total_data_len = 0
self.max_data_len = 0
def close(self) -> None:
"""Close the power meter."""
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
# must be after setting source voltage and before setting mode
self.r.start_measuring() # send command to ppk2
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.use_source_meter() # set source meter mode
if not self.measurement_thread.is_alive():
self.measuring = True
self.reset_measurements()
# We can't start reading from the thread until vdd is set, so start running the thread now
self.measurement_thread.start()
time.sleep(
0.2
) # FIXME - crufty way to ensure we do one set of reads to discard bogus fake power readings in the FIFO
self.reset_measurements()
def powerOn(self):
"""Power on the supply."""
self.r.toggle_DUT_power("ON")
def powerOff(self):
"""Power off the supply."""
self.r.toggle_DUT_power("OFF")

View File

@@ -0,0 +1,57 @@
"""code logging power consumption of meshtastic devices."""
import logging
from datetime import datetime
from riden import Riden
from .power_supply import PowerSupply
class RidenPowerSupply(PowerSupply):
"""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"):
"""Initialize the RidenPowerSupply object.
portName (str, optional): The port name of the power supply. Defaults to "/dev/ttyUSB0".
"""
self.r = r = Riden(port=portName, baudrate=115200, address=1)
logging.info(
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):
"""Set the maximum current the supply will provide."""
self.r.set_i_set(i)
def powerOn(self):
"""Power on the supply, with reasonable defaults for meshtastic devices."""
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 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()
return self.r.wh

View File

@@ -0,0 +1,16 @@
"""code logging power consumption of meshtastic devices."""
import math
import time
from .power_supply import PowerSupply
class SimPowerSupply(PowerSupply):
"""A simulated power supply for testing."""
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.0 + 5 * math.sin(time.time()))

View File

@@ -0,0 +1,81 @@
"""Power stress testing support.
"""
import logging
import time
from ..protobuf import ( portnums_pb2, powermon_pb2 )
def onPowerStressResponse(packet, interface):
"""Delete me? FIXME"""
logging.debug(f"packet:{packet} interface:{interface}")
# interface.gotResponse = True
class PowerStressClient:
"""
The client stub for talking to the firmware PowerStress module.
"""
def __init__(self, iface, node_id=None):
"""
Create a new PowerStressClient instance.
iface is the already open MeshInterface instance
"""
self.iface = iface
if not node_id:
node_id = iface.myInfo.my_node_num
self.node_id = node_id
# No need to subscribe - because we
# pub.subscribe(onGPIOreceive, "meshtastic.receive.powerstress")
def sendPowerStress(
self,
cmd: powermon_pb2.PowerStressMessage.Opcode.ValueType,
num_seconds: float = 0.0,
onResponse=None,
):
"""Client goo for talking with the device side agent."""
r = powermon_pb2.PowerStressMessage()
r.cmd = cmd
r.num_seconds = num_seconds
return self.iface.sendData(
r,
self.node_id,
portnums_pb2.POWERSTRESS_APP,
wantAck=True,
wantResponse=True,
onResponse=onResponse,
onResponseAckPermitted=True,
)
class PowerStress:
"""Walk the UUT through a set of power states so we can capture repeatable power consumption measurements."""
def __init__(self, iface):
self.client = PowerStressClient(iface)
def run(self):
"""Run the power stress test."""
# Send the power stress command
gotAck = False
def onResponse(packet: dict): # pylint: disable=unused-argument
nonlocal gotAck
gotAck = True
logging.info("Starting power stress test, attempting to contact UUT...")
self.client.sendPowerStress(
powermon_pb2.PowerStressMessage.PRINT_INFO, onResponse=onResponse
)
# Wait for the response
while not gotAck:
time.sleep(0.1)
logging.info("Power stress test complete.")

View File

File diff suppressed because one or more lines are too long

View File

@@ -56,6 +56,7 @@ class Config(google.protobuf.message.Message):
ROUTER_CLIENT: Config.DeviceConfig._Role.ValueType # 3
"""
Description: Combination of both ROUTER and CLIENT. Not for mobile devices.
Deprecated in v2.3.15 because improper usage is impacting public meshes: Use ROUTER or CLIENT instead.
"""
REPEATER: Config.DeviceConfig._Role.ValueType # 4
"""
@@ -131,6 +132,7 @@ class Config(google.protobuf.message.Message):
ROUTER_CLIENT: Config.DeviceConfig.Role.ValueType # 3
"""
Description: Combination of both ROUTER and CLIENT. Not for mobile devices.
Deprecated in v2.3.15 because improper usage is impacting public meshes: Use ROUTER or CLIENT instead.
"""
REPEATER: Config.DeviceConfig.Role.ValueType # 4
"""

View File

@@ -14,12 +14,11 @@ _sym_db = _symbol_database.Default()
from meshtastic.protobuf import channel_pb2 as meshtastic_dot_protobuf_dot_channel__pb2
from meshtastic.protobuf import localonly_pb2 as meshtastic_dot_protobuf_dot_localonly__pb2
from meshtastic.protobuf import mesh_pb2 as meshtastic_dot_protobuf_dot_mesh__pb2
from meshtastic.protobuf import module_config_pb2 as meshtastic_dot_protobuf_dot_module__config__pb2
from meshtastic.protobuf import telemetry_pb2 as meshtastic_dot_protobuf_dot_telemetry__pb2
import nanopb_pb2 as nanopb__pb2
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$meshtastic/protobuf/deviceonly.proto\x12\x13meshtastic.protobuf\x1a!meshtastic/protobuf/channel.proto\x1a#meshtastic/protobuf/localonly.proto\x1a\x1emeshtastic/protobuf/mesh.proto\x1a\'meshtastic/protobuf/module_config.proto\x1a#meshtastic/protobuf/telemetry.proto\x1a\x0cnanopb.proto\"\x99\x01\n\x0cPositionLite\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\x0c\n\x04time\x18\x04 \x01(\x07\x12@\n\x0flocation_source\x18\x05 \x01(\x0e\x32\'.meshtastic.protobuf.Position.LocSource\"\xa1\x02\n\x0cNodeInfoLite\x12\x0b\n\x03num\x18\x01 \x01(\r\x12\'\n\x04user\x18\x02 \x01(\x0b\x32\x19.meshtastic.protobuf.User\x12\x33\n\x08position\x18\x03 \x01(\x0b\x32!.meshtastic.protobuf.PositionLite\x12\x0b\n\x03snr\x18\x04 \x01(\x02\x12\x12\n\nlast_heard\x18\x05 \x01(\x07\x12:\n\x0e\x64\x65vice_metrics\x18\x06 \x01(\x0b\x32\".meshtastic.protobuf.DeviceMetrics\x12\x0f\n\x07\x63hannel\x18\x07 \x01(\r\x12\x10\n\x08via_mqtt\x18\x08 \x01(\x08\x12\x11\n\thops_away\x18\t \x01(\r\x12\x13\n\x0bis_favorite\x18\n \x01(\x08\"\x82\x04\n\x0b\x44\x65viceState\x12\x30\n\x07my_node\x18\x02 \x01(\x0b\x32\x1f.meshtastic.protobuf.MyNodeInfo\x12(\n\x05owner\x18\x03 \x01(\x0b\x32\x19.meshtastic.protobuf.User\x12\x36\n\rreceive_queue\x18\x05 \x03(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12\x0f\n\x07version\x18\x08 \x01(\r\x12\x38\n\x0frx_text_message\x18\x07 \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12\x13\n\x07no_save\x18\t \x01(\x08\x42\x02\x18\x01\x12\x15\n\rdid_gps_reset\x18\x0b \x01(\x08\x12\x34\n\x0brx_waypoint\x18\x0c \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12M\n\x19node_remote_hardware_pins\x18\r \x03(\x0b\x32*.meshtastic.protobuf.NodeRemoteHardwarePin\x12\x63\n\x0cnode_db_lite\x18\x0e \x03(\x0b\x32!.meshtastic.protobuf.NodeInfoLiteB*\x92?\'\x92\x01$std::vector<meshtastic_NodeInfoLite>\"N\n\x0b\x43hannelFile\x12.\n\x08\x63hannels\x18\x01 \x03(\x0b\x32\x1c.meshtastic.protobuf.Channel\x12\x0f\n\x07version\x18\x02 \x01(\r\"\xb2\x02\n\x08OEMStore\x12\x16\n\x0eoem_icon_width\x18\x01 \x01(\r\x12\x17\n\x0foem_icon_height\x18\x02 \x01(\r\x12\x15\n\roem_icon_bits\x18\x03 \x01(\x0c\x12\x32\n\x08oem_font\x18\x04 \x01(\x0e\x32 .meshtastic.protobuf.ScreenFonts\x12\x10\n\x08oem_text\x18\x05 \x01(\t\x12\x13\n\x0boem_aes_key\x18\x06 \x01(\x0c\x12:\n\x10oem_local_config\x18\x07 \x01(\x0b\x32 .meshtastic.protobuf.LocalConfig\x12G\n\x17oem_local_module_config\x18\x08 \x01(\x0b\x32&.meshtastic.protobuf.LocalModuleConfig*>\n\x0bScreenFonts\x12\x0e\n\nFONT_SMALL\x10\x00\x12\x0f\n\x0b\x46ONT_MEDIUM\x10\x01\x12\x0e\n\nFONT_LARGE\x10\x02\x42m\n\x13\x63om.geeksville.meshB\nDeviceOnlyZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x92?\x0b\xc2\x01\x08<vector>b\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n$meshtastic/protobuf/deviceonly.proto\x12\x13meshtastic.protobuf\x1a!meshtastic/protobuf/channel.proto\x1a#meshtastic/protobuf/localonly.proto\x1a\x1emeshtastic/protobuf/mesh.proto\x1a#meshtastic/protobuf/telemetry.proto\x1a\x0cnanopb.proto\"\x99\x01\n\x0cPositionLite\x12\x12\n\nlatitude_i\x18\x01 \x01(\x0f\x12\x13\n\x0blongitude_i\x18\x02 \x01(\x0f\x12\x10\n\x08\x61ltitude\x18\x03 \x01(\x05\x12\x0c\n\x04time\x18\x04 \x01(\x07\x12@\n\x0flocation_source\x18\x05 \x01(\x0e\x32\'.meshtastic.protobuf.Position.LocSource\"\xa1\x02\n\x0cNodeInfoLite\x12\x0b\n\x03num\x18\x01 \x01(\r\x12\'\n\x04user\x18\x02 \x01(\x0b\x32\x19.meshtastic.protobuf.User\x12\x33\n\x08position\x18\x03 \x01(\x0b\x32!.meshtastic.protobuf.PositionLite\x12\x0b\n\x03snr\x18\x04 \x01(\x02\x12\x12\n\nlast_heard\x18\x05 \x01(\x07\x12:\n\x0e\x64\x65vice_metrics\x18\x06 \x01(\x0b\x32\".meshtastic.protobuf.DeviceMetrics\x12\x0f\n\x07\x63hannel\x18\x07 \x01(\r\x12\x10\n\x08via_mqtt\x18\x08 \x01(\x08\x12\x11\n\thops_away\x18\t \x01(\r\x12\x13\n\x0bis_favorite\x18\n \x01(\x08\"\x82\x04\n\x0b\x44\x65viceState\x12\x30\n\x07my_node\x18\x02 \x01(\x0b\x32\x1f.meshtastic.protobuf.MyNodeInfo\x12(\n\x05owner\x18\x03 \x01(\x0b\x32\x19.meshtastic.protobuf.User\x12\x36\n\rreceive_queue\x18\x05 \x03(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12\x0f\n\x07version\x18\x08 \x01(\r\x12\x38\n\x0frx_text_message\x18\x07 \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12\x13\n\x07no_save\x18\t \x01(\x08\x42\x02\x18\x01\x12\x15\n\rdid_gps_reset\x18\x0b \x01(\x08\x12\x34\n\x0brx_waypoint\x18\x0c \x01(\x0b\x32\x1f.meshtastic.protobuf.MeshPacket\x12M\n\x19node_remote_hardware_pins\x18\r \x03(\x0b\x32*.meshtastic.protobuf.NodeRemoteHardwarePin\x12\x63\n\x0cnode_db_lite\x18\x0e \x03(\x0b\x32!.meshtastic.protobuf.NodeInfoLiteB*\x92?\'\x92\x01$std::vector<meshtastic_NodeInfoLite>\"N\n\x0b\x43hannelFile\x12.\n\x08\x63hannels\x18\x01 \x03(\x0b\x32\x1c.meshtastic.protobuf.Channel\x12\x0f\n\x07version\x18\x02 \x01(\r\"\xb2\x02\n\x08OEMStore\x12\x16\n\x0eoem_icon_width\x18\x01 \x01(\r\x12\x17\n\x0foem_icon_height\x18\x02 \x01(\r\x12\x15\n\roem_icon_bits\x18\x03 \x01(\x0c\x12\x32\n\x08oem_font\x18\x04 \x01(\x0e\x32 .meshtastic.protobuf.ScreenFonts\x12\x10\n\x08oem_text\x18\x05 \x01(\t\x12\x13\n\x0boem_aes_key\x18\x06 \x01(\x0c\x12:\n\x10oem_local_config\x18\x07 \x01(\x0b\x32 .meshtastic.protobuf.LocalConfig\x12G\n\x17oem_local_module_config\x18\x08 \x01(\x0b\x32&.meshtastic.protobuf.LocalModuleConfig*>\n\x0bScreenFonts\x12\x0e\n\nFONT_SMALL\x10\x00\x12\x0f\n\x0b\x46ONT_MEDIUM\x10\x01\x12\x0e\n\nFONT_LARGE\x10\x02\x42m\n\x13\x63om.geeksville.meshB\nDeviceOnlyZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x92?\x0b\xc2\x01\x08<vector>b\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -31,16 +30,16 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_DEVICESTATE.fields_by_name['no_save']._serialized_options = b'\030\001'
_DEVICESTATE.fields_by_name['node_db_lite']._options = None
_DEVICESTATE.fields_by_name['node_db_lite']._serialized_options = b'\222?\'\222\001$std::vector<meshtastic_NodeInfoLite>'
_globals['_SCREENFONTS']._serialized_start=1611
_globals['_SCREENFONTS']._serialized_end=1673
_globals['_POSITIONLITE']._serialized_start=258
_globals['_POSITIONLITE']._serialized_end=411
_globals['_NODEINFOLITE']._serialized_start=414
_globals['_NODEINFOLITE']._serialized_end=703
_globals['_DEVICESTATE']._serialized_start=706
_globals['_DEVICESTATE']._serialized_end=1220
_globals['_CHANNELFILE']._serialized_start=1222
_globals['_CHANNELFILE']._serialized_end=1300
_globals['_OEMSTORE']._serialized_start=1303
_globals['_OEMSTORE']._serialized_end=1609
_globals['_SCREENFONTS']._serialized_start=1570
_globals['_SCREENFONTS']._serialized_end=1632
_globals['_POSITIONLITE']._serialized_start=217
_globals['_POSITIONLITE']._serialized_end=370
_globals['_NODEINFOLITE']._serialized_start=373
_globals['_NODEINFOLITE']._serialized_end=662
_globals['_DEVICESTATE']._serialized_start=665
_globals['_DEVICESTATE']._serialized_end=1179
_globals['_CHANNELFILE']._serialized_start=1181
_globals['_CHANNELFILE']._serialized_end=1259
_globals['_OEMSTORE']._serialized_start=1262
_globals['_OEMSTORE']._serialized_end=1568
# @@protoc_insertion_point(module_scope)

View File

File diff suppressed because one or more lines are too long

View File

@@ -307,6 +307,23 @@ class _HardwareModelEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._
"""
Heltec Capsule Sensor V3 with ESP32-S3 CPU, Portable LoRa device that can replace GNSS modules or sensors
"""
HELTEC_VISION_MASTER_T190: _HardwareModel.ValueType # 66
"""
Heltec Vision Master T190 with ESP32-S3 CPU, and a 1.90 inch TFT display
"""
HELTEC_VISION_MASTER_E213: _HardwareModel.ValueType # 67
"""
Heltec Vision Master E213 with ESP32-S3 CPU, and a 2.13 inch E-Ink display
"""
HELTEC_VISION_MASTER_E290: _HardwareModel.ValueType # 68
"""
Heltec Vision Master E290 with ESP32-S3 CPU, and a 2.9 inch E-Ink display
"""
HELTEC_MESH_NODE_T114: _HardwareModel.ValueType # 69
"""
Heltec Mesh Node T114 board with nRF52840 CPU, and a 1.14 inch TFT display, Ultimate low-power design,
specifically adapted for the Meshtatic project
"""
PRIVATE_HW: _HardwareModel.ValueType # 255
"""
------------------------------------------------------------------------------------------------------------------------------------------
@@ -598,6 +615,23 @@ HELTEC_CAPSULE_SENSOR_V3: HardwareModel.ValueType # 65
"""
Heltec Capsule Sensor V3 with ESP32-S3 CPU, Portable LoRa device that can replace GNSS modules or sensors
"""
HELTEC_VISION_MASTER_T190: HardwareModel.ValueType # 66
"""
Heltec Vision Master T190 with ESP32-S3 CPU, and a 1.90 inch TFT display
"""
HELTEC_VISION_MASTER_E213: HardwareModel.ValueType # 67
"""
Heltec Vision Master E213 with ESP32-S3 CPU, and a 2.13 inch E-Ink display
"""
HELTEC_VISION_MASTER_E290: HardwareModel.ValueType # 68
"""
Heltec Vision Master E290 with ESP32-S3 CPU, and a 2.9 inch E-Ink display
"""
HELTEC_MESH_NODE_T114: HardwareModel.ValueType # 69
"""
Heltec Mesh Node T114 board with nRF52840 CPU, and a 1.14 inch TFT display, Ultimate low-power design,
specifically adapted for the Meshtatic project
"""
PRIVATE_HW: HardwareModel.ValueType # 255
"""
------------------------------------------------------------------------------------------------------------------------------------------
@@ -2058,6 +2092,7 @@ class FromRadio(google.protobuf.message.Message):
XMODEMPACKET_FIELD_NUMBER: builtins.int
METADATA_FIELD_NUMBER: builtins.int
MQTTCLIENTPROXYMESSAGE_FIELD_NUMBER: builtins.int
FILEINFO_FIELD_NUMBER: builtins.int
id: builtins.int
"""
The packet id, used to allow the phone to request missing read packets from the FIFO,
@@ -2145,6 +2180,12 @@ class FromRadio(google.protobuf.message.Message):
MQTT Client Proxy Message (device sending to client / phone for publishing to MQTT)
"""
@property
def fileInfo(self) -> global___FileInfo:
"""
File system manifest messages
"""
def __init__(
self,
*,
@@ -2162,13 +2203,42 @@ class FromRadio(google.protobuf.message.Message):
xmodemPacket: meshtastic.protobuf.xmodem_pb2.XModem | None = ...,
metadata: global___DeviceMetadata | None = ...,
mqttClientProxyMessage: global___MqttClientProxyMessage | None = ...,
fileInfo: global___FileInfo | None = ...,
) -> None: ...
def HasField(self, field_name: typing.Literal["channel", b"channel", "config", b"config", "config_complete_id", b"config_complete_id", "log_record", b"log_record", "metadata", b"metadata", "moduleConfig", b"moduleConfig", "mqttClientProxyMessage", b"mqttClientProxyMessage", "my_info", b"my_info", "node_info", b"node_info", "packet", b"packet", "payload_variant", b"payload_variant", "queueStatus", b"queueStatus", "rebooted", b"rebooted", "xmodemPacket", b"xmodemPacket"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["channel", b"channel", "config", b"config", "config_complete_id", b"config_complete_id", "id", b"id", "log_record", b"log_record", "metadata", b"metadata", "moduleConfig", b"moduleConfig", "mqttClientProxyMessage", b"mqttClientProxyMessage", "my_info", b"my_info", "node_info", b"node_info", "packet", b"packet", "payload_variant", b"payload_variant", "queueStatus", b"queueStatus", "rebooted", b"rebooted", "xmodemPacket", b"xmodemPacket"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["payload_variant", b"payload_variant"]) -> typing.Literal["packet", "my_info", "node_info", "config", "log_record", "config_complete_id", "rebooted", "moduleConfig", "channel", "queueStatus", "xmodemPacket", "metadata", "mqttClientProxyMessage"] | None: ...
def HasField(self, field_name: typing.Literal["channel", b"channel", "config", b"config", "config_complete_id", b"config_complete_id", "fileInfo", b"fileInfo", "log_record", b"log_record", "metadata", b"metadata", "moduleConfig", b"moduleConfig", "mqttClientProxyMessage", b"mqttClientProxyMessage", "my_info", b"my_info", "node_info", b"node_info", "packet", b"packet", "payload_variant", b"payload_variant", "queueStatus", b"queueStatus", "rebooted", b"rebooted", "xmodemPacket", b"xmodemPacket"]) -> builtins.bool: ...
def ClearField(self, field_name: typing.Literal["channel", b"channel", "config", b"config", "config_complete_id", b"config_complete_id", "fileInfo", b"fileInfo", "id", b"id", "log_record", b"log_record", "metadata", b"metadata", "moduleConfig", b"moduleConfig", "mqttClientProxyMessage", b"mqttClientProxyMessage", "my_info", b"my_info", "node_info", b"node_info", "packet", b"packet", "payload_variant", b"payload_variant", "queueStatus", b"queueStatus", "rebooted", b"rebooted", "xmodemPacket", b"xmodemPacket"]) -> None: ...
def WhichOneof(self, oneof_group: typing.Literal["payload_variant", b"payload_variant"]) -> typing.Literal["packet", "my_info", "node_info", "config", "log_record", "config_complete_id", "rebooted", "moduleConfig", "channel", "queueStatus", "xmodemPacket", "metadata", "mqttClientProxyMessage", "fileInfo"] | None: ...
global___FromRadio = FromRadio
@typing.final
class FileInfo(google.protobuf.message.Message):
"""
Individual File info for the device
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
FILE_NAME_FIELD_NUMBER: builtins.int
SIZE_BYTES_FIELD_NUMBER: builtins.int
file_name: builtins.str
"""
The fully qualified path of the file
"""
size_bytes: builtins.int
"""
The size of the file in bytes
"""
def __init__(
self,
*,
file_name: builtins.str = ...,
size_bytes: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["file_name", b"file_name", "size_bytes", b"size_bytes"]) -> None: ...
global___FileInfo = FileInfo
@typing.final
class ToRadio(google.protobuf.message.Message):
"""

View File

@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/portnums.proto\x12\x13meshtastic.protobuf*\x8d\x04\n\x07PortNum\x12\x0f\n\x0bUNKNOWN_APP\x10\x00\x12\x14\n\x10TEXT_MESSAGE_APP\x10\x01\x12\x17\n\x13REMOTE_HARDWARE_APP\x10\x02\x12\x10\n\x0cPOSITION_APP\x10\x03\x12\x10\n\x0cNODEINFO_APP\x10\x04\x12\x0f\n\x0bROUTING_APP\x10\x05\x12\r\n\tADMIN_APP\x10\x06\x12\x1f\n\x1bTEXT_MESSAGE_COMPRESSED_APP\x10\x07\x12\x10\n\x0cWAYPOINT_APP\x10\x08\x12\r\n\tAUDIO_APP\x10\t\x12\x18\n\x14\x44\x45TECTION_SENSOR_APP\x10\n\x12\r\n\tREPLY_APP\x10 \x12\x11\n\rIP_TUNNEL_APP\x10!\x12\x12\n\x0ePAXCOUNTER_APP\x10\"\x12\x0e\n\nSERIAL_APP\x10@\x12\x15\n\x11STORE_FORWARD_APP\x10\x41\x12\x12\n\x0eRANGE_TEST_APP\x10\x42\x12\x11\n\rTELEMETRY_APP\x10\x43\x12\x0b\n\x07ZPS_APP\x10\x44\x12\x11\n\rSIMULATOR_APP\x10\x45\x12\x12\n\x0eTRACEROUTE_APP\x10\x46\x12\x14\n\x10NEIGHBORINFO_APP\x10G\x12\x0f\n\x0b\x41TAK_PLUGIN\x10H\x12\x12\n\x0eMAP_REPORT_APP\x10I\x12\x10\n\x0bPRIVATE_APP\x10\x80\x02\x12\x13\n\x0e\x41TAK_FORWARDER\x10\x81\x02\x12\x08\n\x03MAX\x10\xff\x03\x42]\n\x13\x63om.geeksville.meshB\x08PortnumsZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/portnums.proto\x12\x13meshtastic.protobuf*\xa2\x04\n\x07PortNum\x12\x0f\n\x0bUNKNOWN_APP\x10\x00\x12\x14\n\x10TEXT_MESSAGE_APP\x10\x01\x12\x17\n\x13REMOTE_HARDWARE_APP\x10\x02\x12\x10\n\x0cPOSITION_APP\x10\x03\x12\x10\n\x0cNODEINFO_APP\x10\x04\x12\x0f\n\x0bROUTING_APP\x10\x05\x12\r\n\tADMIN_APP\x10\x06\x12\x1f\n\x1bTEXT_MESSAGE_COMPRESSED_APP\x10\x07\x12\x10\n\x0cWAYPOINT_APP\x10\x08\x12\r\n\tAUDIO_APP\x10\t\x12\x18\n\x14\x44\x45TECTION_SENSOR_APP\x10\n\x12\r\n\tREPLY_APP\x10 \x12\x11\n\rIP_TUNNEL_APP\x10!\x12\x12\n\x0ePAXCOUNTER_APP\x10\"\x12\x0e\n\nSERIAL_APP\x10@\x12\x15\n\x11STORE_FORWARD_APP\x10\x41\x12\x12\n\x0eRANGE_TEST_APP\x10\x42\x12\x11\n\rTELEMETRY_APP\x10\x43\x12\x0b\n\x07ZPS_APP\x10\x44\x12\x11\n\rSIMULATOR_APP\x10\x45\x12\x12\n\x0eTRACEROUTE_APP\x10\x46\x12\x14\n\x10NEIGHBORINFO_APP\x10G\x12\x0f\n\x0b\x41TAK_PLUGIN\x10H\x12\x12\n\x0eMAP_REPORT_APP\x10I\x12\x13\n\x0fPOWERSTRESS_APP\x10J\x12\x10\n\x0bPRIVATE_APP\x10\x80\x02\x12\x13\n\x0e\x41TAK_FORWARDER\x10\x81\x02\x12\x08\n\x03MAX\x10\xff\x03\x42]\n\x13\x63om.geeksville.meshB\x08PortnumsZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -22,5 +22,5 @@ if _descriptor._USE_C_DESCRIPTORS == False:
DESCRIPTOR._options = None
DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\010PortnumsZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000'
_globals['_PORTNUM']._serialized_start=60
_globals['_PORTNUM']._serialized_end=585
_globals['_PORTNUM']._serialized_end=606
# @@protoc_insertion_point(module_scope)

View File

@@ -171,6 +171,10 @@ class _PortNumEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTy
"""
Provides unencrypted information about a node for consumption by a map via MQTT
"""
POWERSTRESS_APP: _PortNum.ValueType # 74
"""
PowerStress based monitoring support (for automated power consumption testing)
"""
PRIVATE_APP: _PortNum.ValueType # 256
"""
Private applications should use portnums >= 256.
@@ -352,6 +356,10 @@ MAP_REPORT_APP: PortNum.ValueType # 73
"""
Provides unencrypted information about a node for consumption by a map via MQTT
"""
POWERSTRESS_APP: PortNum.ValueType # 74
"""
PowerStress based monitoring support (for automated power consumption testing)
"""
PRIVATE_APP: PortNum.ValueType # 256
"""
Private applications should use portnums >= 256.

View File

@@ -13,7 +13,7 @@ _sym_db = _symbol_database.Default()
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/powermon.proto\x12\x13meshtastic.protobuf\"\xe0\x01\n\x08PowerMon\"\xd3\x01\n\x05State\x12\x08\n\x04None\x10\x00\x12\x11\n\rCPU_DeepSleep\x10\x01\x12\x12\n\x0e\x43PU_LightSleep\x10\x02\x12\x0c\n\x08Vext1_On\x10\x04\x12\r\n\tLora_RXOn\x10\x08\x12\r\n\tLora_TXOn\x10\x10\x12\x11\n\rLora_RXActive\x10 \x12\t\n\x05\x42T_On\x10@\x12\x0b\n\x06LED_On\x10\x80\x01\x12\x0e\n\tScreen_On\x10\x80\x02\x12\x13\n\x0eScreen_Drawing\x10\x80\x04\x12\x0c\n\x07Wifi_On\x10\x80\x08\x12\x0f\n\nGPS_Active\x10\x80\x10\x42\x63\n\x13\x63om.geeksville.meshB\x0ePowerMonProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"meshtastic/protobuf/powermon.proto\x12\x13meshtastic.protobuf\"\xe0\x01\n\x08PowerMon\"\xd3\x01\n\x05State\x12\x08\n\x04None\x10\x00\x12\x11\n\rCPU_DeepSleep\x10\x01\x12\x12\n\x0e\x43PU_LightSleep\x10\x02\x12\x0c\n\x08Vext1_On\x10\x04\x12\r\n\tLora_RXOn\x10\x08\x12\r\n\tLora_TXOn\x10\x10\x12\x11\n\rLora_RXActive\x10 \x12\t\n\x05\x42T_On\x10@\x12\x0b\n\x06LED_On\x10\x80\x01\x12\x0e\n\tScreen_On\x10\x80\x02\x12\x13\n\x0eScreen_Drawing\x10\x80\x04\x12\x0c\n\x07Wifi_On\x10\x80\x08\x12\x0f\n\nGPS_Active\x10\x80\x10\"\x88\x03\n\x12PowerStressMessage\x12;\n\x03\x63md\x18\x01 \x01(\x0e\x32..meshtastic.protobuf.PowerStressMessage.Opcode\x12\x13\n\x0bnum_seconds\x18\x02 \x01(\x02\"\x9f\x02\n\x06Opcode\x12\t\n\x05UNSET\x10\x00\x12\x0e\n\nPRINT_INFO\x10\x01\x12\x0f\n\x0b\x46ORCE_QUIET\x10\x02\x12\r\n\tEND_QUIET\x10\x03\x12\r\n\tSCREEN_ON\x10\x10\x12\x0e\n\nSCREEN_OFF\x10\x11\x12\x0c\n\x08\x43PU_IDLE\x10 \x12\x11\n\rCPU_DEEPSLEEP\x10!\x12\x0e\n\nCPU_FULLON\x10\"\x12\n\n\x06LED_ON\x10\x30\x12\x0b\n\x07LED_OFF\x10\x31\x12\x0c\n\x08LORA_OFF\x10@\x12\x0b\n\x07LORA_TX\x10\x41\x12\x0b\n\x07LORA_RX\x10\x42\x12\n\n\x06\x42T_OFF\x10P\x12\t\n\x05\x42T_ON\x10Q\x12\x0c\n\x08WIFI_OFF\x10`\x12\x0b\n\x07WIFI_ON\x10\x61\x12\x0b\n\x07GPS_OFF\x10p\x12\n\n\x06GPS_ON\x10qBc\n\x13\x63om.geeksville.meshB\x0ePowerMonProtosZ\"github.com/meshtastic/go/generated\xaa\x02\x14Meshtastic.Protobufs\xba\x02\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -25,4 +25,8 @@ if _descriptor._USE_C_DESCRIPTORS == False:
_globals['_POWERMON']._serialized_end=284
_globals['_POWERMON_STATE']._serialized_start=73
_globals['_POWERMON_STATE']._serialized_end=284
_globals['_POWERSTRESSMESSAGE']._serialized_start=287
_globals['_POWERSTRESSMESSAGE']._serialized_end=679
_globals['_POWERSTRESSMESSAGE_OPCODE']._serialized_start=392
_globals['_POWERSTRESSMESSAGE_OPCODE']._serialized_end=679
# @@protoc_insertion_point(module_scope)

View File

@@ -95,3 +95,127 @@ class PowerMon(google.protobuf.message.Message):
) -> None: ...
global___PowerMon = PowerMon
@typing.final
class PowerStressMessage(google.protobuf.message.Message):
"""
PowerStress testing support via the C++ PowerStress module
"""
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class _Opcode:
ValueType = typing.NewType("ValueType", builtins.int)
V: typing_extensions.TypeAlias = ValueType
class _OpcodeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[PowerStressMessage._Opcode.ValueType], builtins.type):
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
UNSET: PowerStressMessage._Opcode.ValueType # 0
"""
Unset/unused
"""
PRINT_INFO: PowerStressMessage._Opcode.ValueType # 1
"""Print board version slog and send an ack that we are alive and ready to process commands"""
FORCE_QUIET: PowerStressMessage._Opcode.ValueType # 2
"""Try to turn off all automatic processing of packets, screen, sleeping, etc (to make it easier to measure in isolation)"""
END_QUIET: PowerStressMessage._Opcode.ValueType # 3
"""Stop powerstress processing - probably by just rebooting the board"""
SCREEN_ON: PowerStressMessage._Opcode.ValueType # 16
"""Turn the screen on"""
SCREEN_OFF: PowerStressMessage._Opcode.ValueType # 17
"""Turn the screen off"""
CPU_IDLE: PowerStressMessage._Opcode.ValueType # 32
"""Let the CPU run but we assume mostly idling for num_seconds"""
CPU_DEEPSLEEP: PowerStressMessage._Opcode.ValueType # 33
"""Force deep sleep for FIXME seconds"""
CPU_FULLON: PowerStressMessage._Opcode.ValueType # 34
"""Spin the CPU as fast as possible for num_seconds"""
LED_ON: PowerStressMessage._Opcode.ValueType # 48
"""Turn the LED on for num_seconds (and leave it on - for baseline power measurement purposes)"""
LED_OFF: PowerStressMessage._Opcode.ValueType # 49
"""Force the LED off for num_seconds"""
LORA_OFF: PowerStressMessage._Opcode.ValueType # 64
"""Completely turn off the LORA radio for num_seconds"""
LORA_TX: PowerStressMessage._Opcode.ValueType # 65
"""Send Lora packets for num_seconds"""
LORA_RX: PowerStressMessage._Opcode.ValueType # 66
"""Receive Lora packets for num_seconds (node will be mostly just listening, unless an external agent is helping stress this by sending packets on the current channel)"""
BT_OFF: PowerStressMessage._Opcode.ValueType # 80
"""Turn off the BT radio for num_seconds"""
BT_ON: PowerStressMessage._Opcode.ValueType # 81
"""Turn on the BT radio for num_seconds"""
WIFI_OFF: PowerStressMessage._Opcode.ValueType # 96
"""Turn off the WIFI radio for num_seconds"""
WIFI_ON: PowerStressMessage._Opcode.ValueType # 97
"""Turn on the WIFI radio for num_seconds"""
GPS_OFF: PowerStressMessage._Opcode.ValueType # 112
"""Turn off the GPS radio for num_seconds"""
GPS_ON: PowerStressMessage._Opcode.ValueType # 113
"""Turn on the GPS radio for num_seconds"""
class Opcode(_Opcode, metaclass=_OpcodeEnumTypeWrapper):
"""
What operation would we like the UUT to perform.
note: senders should probably set want_response in their request packets, so that they can know when the state
machine has started processing their request
"""
UNSET: PowerStressMessage.Opcode.ValueType # 0
"""
Unset/unused
"""
PRINT_INFO: PowerStressMessage.Opcode.ValueType # 1
"""Print board version slog and send an ack that we are alive and ready to process commands"""
FORCE_QUIET: PowerStressMessage.Opcode.ValueType # 2
"""Try to turn off all automatic processing of packets, screen, sleeping, etc (to make it easier to measure in isolation)"""
END_QUIET: PowerStressMessage.Opcode.ValueType # 3
"""Stop powerstress processing - probably by just rebooting the board"""
SCREEN_ON: PowerStressMessage.Opcode.ValueType # 16
"""Turn the screen on"""
SCREEN_OFF: PowerStressMessage.Opcode.ValueType # 17
"""Turn the screen off"""
CPU_IDLE: PowerStressMessage.Opcode.ValueType # 32
"""Let the CPU run but we assume mostly idling for num_seconds"""
CPU_DEEPSLEEP: PowerStressMessage.Opcode.ValueType # 33
"""Force deep sleep for FIXME seconds"""
CPU_FULLON: PowerStressMessage.Opcode.ValueType # 34
"""Spin the CPU as fast as possible for num_seconds"""
LED_ON: PowerStressMessage.Opcode.ValueType # 48
"""Turn the LED on for num_seconds (and leave it on - for baseline power measurement purposes)"""
LED_OFF: PowerStressMessage.Opcode.ValueType # 49
"""Force the LED off for num_seconds"""
LORA_OFF: PowerStressMessage.Opcode.ValueType # 64
"""Completely turn off the LORA radio for num_seconds"""
LORA_TX: PowerStressMessage.Opcode.ValueType # 65
"""Send Lora packets for num_seconds"""
LORA_RX: PowerStressMessage.Opcode.ValueType # 66
"""Receive Lora packets for num_seconds (node will be mostly just listening, unless an external agent is helping stress this by sending packets on the current channel)"""
BT_OFF: PowerStressMessage.Opcode.ValueType # 80
"""Turn off the BT radio for num_seconds"""
BT_ON: PowerStressMessage.Opcode.ValueType # 81
"""Turn on the BT radio for num_seconds"""
WIFI_OFF: PowerStressMessage.Opcode.ValueType # 96
"""Turn off the WIFI radio for num_seconds"""
WIFI_ON: PowerStressMessage.Opcode.ValueType # 97
"""Turn on the WIFI radio for num_seconds"""
GPS_OFF: PowerStressMessage.Opcode.ValueType # 112
"""Turn off the GPS radio for num_seconds"""
GPS_ON: PowerStressMessage.Opcode.ValueType # 113
"""Turn on the GPS radio for num_seconds"""
CMD_FIELD_NUMBER: builtins.int
NUM_SECONDS_FIELD_NUMBER: builtins.int
cmd: global___PowerStressMessage.Opcode.ValueType
"""
What type of HardwareMessage is this?
"""
num_seconds: builtins.float
def __init__(
self,
*,
cmd: global___PowerStressMessage.Opcode.ValueType = ...,
num_seconds: builtins.float = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["cmd", b"cmd", "num_seconds", b"num_seconds"]) -> None: ...
global___PowerStressMessage = PowerStressMessage

View File

@@ -0,0 +1,3 @@
"""Structured logging framework (see dev docs for more info)."""
from .slog import LogSet

79
meshtastic/slog/arrow.py Normal file
View File

@@ -0,0 +1,79 @@
"""Utilities for Apache Arrow serialization."""
import logging
import os
import pyarrow as pa
from pyarrow import feather
chunk_size = 1000 # disk writes are batched based on this number of rows
class ArrowWriter:
"""Writes an arrow file in a streaming fashion"""
def __init__(self, file_name: str):
"""Create a new ArrowWriter object.
file_name (str): The name of the file to write to.
"""
self.sink = pa.OSFile(file_name, "wb") # type: ignore
self.new_rows: list[dict] = []
self.schema: pa.Schema | None = None # haven't yet learned the schema
self.writer: pa.RecordBatchFileWriter | None = None
def close(self):
"""Close the stream and writes the file as needed."""
self._write()
if self.writer:
self.writer.close()
self.sink.close()
def _write(self):
"""Write the new rows to the file."""
if len(self.new_rows) > 0:
if self.schema is None:
# only need to look at the first row to learn the schema
self.schema = pa.Table.from_pylist([self.new_rows[0]]).schema
self.writer = pa.ipc.new_stream(self.sink, self.schema)
self.writer.write_batch(pa.RecordBatch.from_pylist(self.new_rows))
self.new_rows = []
def add_row(self, row_dict: dict):
"""Add a row to the arrow file.
We will automatically learn the schema from the first row. But all rows must use that schema.
"""
self.new_rows.append(row_dict)
if len(self.new_rows) >= chunk_size:
self._write()
class FeatherWriter(ArrowWriter):
"""A smaller more interoperable version of arrow files.
Uses a temporary .arrow file (which could be huge) but converts to a much smaller (but still fast)
feather file.
"""
def __init__(self, file_name: str):
super().__init__(file_name + ".arrow")
self.base_file_name = file_name
def close(self):
super().close()
src_name = self.base_file_name + ".arrow"
dest_name = self.base_file_name + ".feather"
if os.path.getsize(src_name) == 0:
logging.warning(f"Discarding empty file: {src_name}")
os.remove(src_name)
else:
logging.info(f"Compressing log data into {dest_name}")
# note: must use open_stream, not open_file/read_table because the streaming layout is different
# data = feather.read_table(src_name)
with pa.memory_map(src_name) as source:
array = pa.ipc.open_stream(source).read_all()
# See https://stackoverflow.com/a/72406099 for more info and performance testing measurements
feather.write_feather(array, dest_name, compression="zstd")
os.remove(src_name)

208
meshtastic/slog/slog.py Normal file
View File

@@ -0,0 +1,208 @@
"""code logging power consumption of meshtastic devices."""
import atexit
import io
import logging
import os
import re
import threading
import time
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
import parse # type: ignore[import-untyped]
import platformdirs
from pubsub import pub # type: ignore[import-untyped]
from meshtastic.mesh_interface import MeshInterface
from meshtastic.powermon import PowerMeter
from .arrow import FeatherWriter
@dataclass(init=False)
class LogDef:
"""Log definition."""
code: str # i.e. PM or B or whatever... see meshtastic slog documentation
format: parse.Parser # A format string that can be used to parse the arguments
def __init__(self, code: str, fmt: str) -> None:
"""Initialize the LogDef object.
code (str): The code.
format (str): The format.
"""
self.code = code
self.format = parse.compile(fmt)
"""A dictionary mapping from logdef code to logdef"""
log_defs = {
d.code: d
for d in [
LogDef("B", "{boardid:d},{version}"),
LogDef("PM", "{bitmask:d},{reason}"),
]
}
log_regex = re.compile(".*S:([0-9A-Za-z]+):(.*)")
class PowerLogger:
"""Logs current watts reading periodically using PowerMeter and ArrowWriter."""
def __init__(self, pMeter: PowerMeter, file_path: str, interval=0.2) -> None:
"""Initialize the PowerLogger object."""
self.pMeter = pMeter
self.writer = FeatherWriter(file_path)
self.interval = interval
self.is_logging = True
self.thread = threading.Thread(
target=self._logging_thread, name="PowerLogger", daemon=True
)
self.thread.start()
def _logging_thread(self) -> None:
"""Background thread for logging the current watts reading."""
while self.is_logging:
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)
def close(self) -> None:
"""Close the PowerLogger and stop logging."""
if self.is_logging:
self.pMeter.close()
self.is_logging = False
self.thread.join()
self.writer.close()
# FIXME move these defs somewhere else
TOPIC_MESHTASTIC_LOG_LINE = "meshtastic.log.line"
class StructuredLogger:
"""Sniffs device logs for structured log messages, extracts those into apache arrow format.
Also writes the raw log messages to raw.txt"""
def __init__(self, client: MeshInterface, dir_path: str) -> None:
"""Initialize the StructuredLogger object.
client (MeshInterface): The MeshInterface object to monitor.
"""
self.client = client
self.writer = FeatherWriter(f"{dir_path}/slog")
self.raw_file: Optional[
io.TextIOWrapper
] = open( # pylint: disable=consider-using-with
f"{dir_path}/raw.txt", "w", encoding="utf8"
)
# We need a closure here because the subscription API is very strict about exact arg matching
def listen_glue(line, interface): # pylint: disable=unused-argument
self._onLogMessage(line)
self.listener = pub.subscribe(listen_glue, TOPIC_MESHTASTIC_LOG_LINE)
def close(self) -> None:
"""Stop logging."""
pub.unsubscribe(self.listener, TOPIC_MESHTASTIC_LOG_LINE)
self.writer.close()
f = self.raw_file
self.raw_file = None # mark that we are shutting down
if f:
f.close() # Close the raw.txt file
def _onLogMessage(self, line: str) -> None:
"""Handle log messages.
line (str): the line of log output
"""
m = log_regex.match(line)
if m:
src = m.group(1)
args = m.group(2)
args += " " # append a space so that if the last arg is an empty str it will still be accepted as a match
logging.debug(f"SLog {src}, reason: {args}")
if src != "PM":
logging.warning(f"Not yet handling structured log {src} (FIXME)")
else:
d = log_defs.get(src)
if d:
r = d.format.parse(args) # get the values with the correct types
if r:
di = r.named
di["time"] = datetime.now()
self.writer.add_row(di)
else:
logging.warning(f"Failed to parse slog {line} with {d.format}")
else:
logging.warning(f"Unknown Structured Log: {line}")
if self.raw_file:
self.raw_file.write(line + "\n") # Write the raw log
class LogSet:
"""A complete set of meshtastic log/metadata for a particular run."""
def __init__(
self,
client: MeshInterface,
dir_name: Optional[str] = None,
power_meter: Optional[PowerMeter] = None,
) -> None:
"""Initialize the PowerMonClient object.
power (PowerSupply): The power supply object.
client (MeshInterface): The MeshInterface object to monitor.
"""
if not dir_name:
app_name = "meshtastic"
app_author = "meshtastic"
app_dir = platformdirs.user_data_dir(app_name, app_author)
dir_name = f"{app_dir}/slogs/{datetime.now().strftime('%Y%m%d-%H%M%S')}"
os.makedirs(dir_name, exist_ok=True)
# Also make a 'latest' directory that always points to the most recent logs
# symlink might fail on some platforms, if it does fail silently
if os.path.exists(f"{app_dir}/slogs/latest"):
os.unlink(f"{app_dir}/slogs/latest")
os.symlink(dir_name, f"{app_dir}/slogs/latest", target_is_directory=True)
self.dir_name = dir_name
logging.info(f"Writing slogs to {dir_name}")
self.slog_logger: Optional[StructuredLogger] = StructuredLogger(
client, self.dir_name
)
self.power_logger: Optional[PowerLogger] = (
None
if not power_meter
else PowerLogger(power_meter, f"{self.dir_name}/power")
)
# Store a lambda so we can find it again to unregister
self.atexit_handler = lambda: self.close() # pylint: disable=unnecessary-lambda
def close(self) -> None:
"""Close the log set."""
if self.slog_logger:
logging.info(f"Closing slogs in {self.dir_name}")
atexit.unregister(
self.atexit_handler
) # docs say it will silently ignore if not found
self.slog_logger.close()
if self.power_logger:
self.power_logger.close()
self.slog_logger = None

View File

@@ -39,9 +39,10 @@ class StreamInterface(MeshInterface):
self._wantExit = False
self.is_windows11 = is_windows11()
self.cur_log_line = ""
# FIXME, figure out why daemon=True causes reader thread to exit too early
self._rxThread = threading.Thread(target=self.__reader, args=(), daemon=True)
self._rxThread = threading.Thread(target=self.__reader, args=(), daemon=True, name="stream reader")
MeshInterface.__init__(self, debugOut=debugOut, noProto=noProto, noNodes=noNodes)
@@ -124,6 +125,23 @@ class StreamInterface(MeshInterface):
if self._rxThread != threading.current_thread():
self._rxThread.join() # wait for it to exit
def _handleLogByte(self, b):
"""Handle a byte that is part of a log message from the device."""
utf = "?" # assume we might fail
try:
utf = b.decode("utf-8")
except:
pass
if utf == "\r":
pass # ignore
elif utf == "\n":
self._handleLogLine(self.cur_log_line)
self.cur_log_line = ""
else:
self.cur_log_line += utf
def __reader(self):
"""The reader thread that reads bytes from our stream"""
logging.debug("in __reader()")
@@ -146,11 +164,9 @@ class StreamInterface(MeshInterface):
if ptr == 0: # looking for START1
if c != START1:
self._rxBuf = empty # failed to find start
if self.debugOut is not None:
try:
self.debugOut.write(b.decode("utf-8"))
except:
self.debugOut.write("?")
# This must be a log message from the device
self._handleLogByte(b)
elif ptr == 1: # looking for START2
if c != START2:

View File

@@ -25,9 +25,13 @@ from meshtastic.supported_device import supported_devices
from meshtastic.version import get_active_version
"""Some devices such as a seger jlink or st-link we never want to accidentally open
0x1915 NordicSemi (PPK2)
0483 STMicroelectronics ST-LINK/V2
0136 SEGGER J-Link
1915 NordicSemi (PPK2)
0925 Lakeview Research Saleae Logic (logic analyzer)
04b4:602a Cypress Semiconductor Corp. Hantek DSO-6022BL (oscilloscope)
"""
blacklistVids = dict.fromkeys([0x1366, 0x0483, 0x1915])
blacklistVids = dict.fromkeys([0x1366, 0x0483, 0x1915, 0x0925, 0x04b4])
"""Some devices are highly likely to be meshtastic.
0x239a RAK4631
@@ -266,9 +270,10 @@ class Acknowledgment:
class DeferredExecution:
"""A thread that accepts closures to run, and runs them as they are received"""
def __init__(self, name=None):
def __init__(self, name):
self.queue = Queue()
self.thread = threading.Thread(target=self._run, args=(), name=name)
# this thread must be marked as daemon, otherwise it will prevent clients from exiting
self.thread = threading.Thread(target=self._run, args=(), name=name, daemon=True)
self.thread.daemon = True
self.thread.start()

2456
poetry.lock generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ license = "GPL-3.0-only"
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.8,<3.13" # was 3.7 for production but, 3.8 is needed for pytap2, 3.9 is needed for pandas, bleak requires a max of 3.13 for some reason
python = "^3.9,<3.13" # 3.9 is needed for pandas, bleak requires a max of 3.13 for some reason
pyserial = "^3.5"
protobuf = ">=5.26.0"
dotmap = "^1.3.30"
@@ -21,6 +21,11 @@ pyyaml = "^6.0.1"
pypubsub = "^4.0.3"
bleak = "^0.21.1"
packaging = "^24.0"
riden = { git = "https://github.com/geeksville/riden.git#1.2.1" }
parse = "^1.20.2"
ppk2-api = "^0.9.2"
pyarrow = "^16.1.0"
platformdirs = "^4.2.2"
print-color = "^0.4.6"
[tool.poetry.group.dev.dependencies]
@@ -39,6 +44,26 @@ types-tabulate = "^0.9.0.20240106"
types-requests = "^2.31.0.20240406"
types-setuptools = "^69.5.0.20240423"
types-pyyaml = "^6.0.12.20240311"
pyarrow-stubs = "^10.0.1.7"
# If you are doing power analysis you probably want these extra devtools
[tool.poetry.group.analysis]
optional = true
[tool.poetry.group.analysis.dependencies]
jupyterlab = "^4.2.2"
mypy = "^1.10.0"
mypy-protobuf = "^3.6.0"
types-protobuf = "^5.26.0.20240422"
types-tabulate = "^0.9.0.20240106"
types-requests = "^2.31.0.20240406"
types-setuptools = "^69.5.0.20240423"
types-pyyaml = "^6.0.12.20240311"
matplotlib = "^3.9.0"
ipympl = "^0.9.4"
ipywidgets = "^8.1.3"
jupyterlab-widgets = "^3.0.11"
dash = "^2.17.1"
[tool.poetry.extras]
tunnel = ["pytap2"]
@@ -47,6 +72,13 @@ tunnel = ["pytap2"]
meshtastic = "meshtastic.__main__:main"
mesh-tunnel = "meshtastic.__main__:tunnelMain [tunnel]"
# "Poe the poet" (optional) provides an easy way of running non python tools inside the poetry virtualenv
# if you would like to use it run "pipx install poe"
# then you can do stuff like "poe code" to run vscode inside this environment
[tool.poe.tasks]
code = "code ."
juypter = "poetry run jupyter lab"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"