diff --git a/.vscode/launch.json b/.vscode/launch.json index aca86dd..b093786 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -106,7 +106,7 @@ "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", @@ -164,7 +164,15 @@ "request": "launch", "module": "meshtastic", "justMyCode": true, - "args": ["--debug", "--seriallog", "stdout"] + "args": ["--noproto", "--seriallog", "stdout"] + }, + { + "name": "meshtastic powermon", + "type": "python", + "request": "launch", + "module": "meshtastic", + "justMyCode": false, + "args": ["--power-mon", "/dev/ttyUSB1", "--port", "/dev/ttyUSB0", "--noproto", "--seriallog", "stdout"] }, { "name": "meshtastic test", diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index fa71adc..3db80ba 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -21,6 +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.power_mon import PowerMonClient def onReceive(packet, interface): """Callback invoked when a packet arrives""" @@ -1089,6 +1090,10 @@ def common(): # We assume client is fully connected now onConnected(client) + if args.power_mon: + PowerMonClient(args.power_mon, client) + + have_tunnel = platform.system() == "Linux" if ( args.noproto or args.reply or (have_tunnel and args.tunnel) or args.listen @@ -1500,6 +1505,17 @@ def initParser(): action="store_true", ) + group.add_argument( + "--power-mon", + help="Capture any power monitor records. You must use --power-mon /dev/ttyUSBxxx to specify which port the power supply is on", + ) + + 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", + ) + group.add_argument( "--ble-scan", help="Scan for Meshtastic BLE devices", diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index 5b3403c..0bc67ba 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -29,7 +29,7 @@ from meshtastic import ( NODELESS_WANT_CONFIG_ID, ResponseHandler, protocols, - publishingThread, + publishingThread ) from meshtastic.util import ( Acknowledgment, @@ -40,7 +40,7 @@ from meshtastic.util import ( stripnl, message_to_json, ) - +from meshtastic.observable import Observable class MeshInterface: # pylint: disable=R0902 """Interface class for meshtastic devices @@ -71,6 +71,7 @@ class MeshInterface: # pylint: disable=R0902 self.nodes: Optional[Dict[str,Dict]] = None # FIXME self.isConnected: threading.Event = threading.Event() self.noProto: bool = noProto + self.onLogMessage = Observable() self.localNode: meshtastic.node.Node = meshtastic.node.Node(self, -1) # We fixup nodenum later self.myInfo: Optional[mesh_pb2.MyNodeInfo] = None # We don't have device info yet self.metadata: Optional[mesh_pb2.DeviceMetadata] = None # We don't have device metadata yet @@ -111,6 +112,10 @@ class MeshInterface: # pylint: disable=R0902 logging.error(f"Traceback: {traceback}") self.close() + def _handleLogLine(self, line): + """Handle a line of log output from the device.""" + self.onLogMessage.fire(message=line) + def showInfo(self, file=sys.stdout) -> str: # pylint: disable=W0613 """Show human readable summary about this object""" owner = f"Owner: {self.getLongName()} ({self.getShortName()})" diff --git a/meshtastic/observable.py b/meshtastic/observable.py new file mode 100644 index 0000000..2bd2a5b --- /dev/null +++ b/meshtastic/observable.py @@ -0,0 +1,35 @@ + + +class Event(object): + """A simple event class.""" + +class Observable(object): + """A class that represents an observable object. + + To publish an event call fire(type="progress", percent=50) or whatever. It will call + """ + + def __init__(self): + """Initialize the Observable object.""" + self.callbacks = [] + + def subscribe(self, callback): + """Subscribe to the observable object. + + Args: + callback (function): The callback function to be called when the event is fired. + """ + self.callbacks.append(callback) + + def fire(self, **attrs): + """Fire the event. + + Args: + **attrs: Arbitrary keyword arguments to be passed to the callback functions. + """ + e = Event() + e.source = self + for k, v in attrs.items(): + setattr(e, k, v) + for fn in self.callbacks: + fn(e) \ No newline at end of file diff --git a/meshtastic/power_mon.py b/meshtastic/power_mon.py new file mode 100644 index 0000000..377ba33 --- /dev/null +++ b/meshtastic/power_mon.py @@ -0,0 +1,143 @@ +"""code logging power consumption of meshtastic devices.""" + +import logging +import re +import atexit +from datetime import datetime + +import pandas as pd +from riden import Riden + +from meshtastic.mesh_interface import MeshInterface +from meshtastic.observable import Event + + +class PowerSupply: + """Interface for talking to programmable bench-top power supplies. + Currently only the Riden supplies are supported (RD6006 tested) + """ + + def __init__(self, portName: str = "/dev/ttyUSB0"): + """Initialize the PowerSupply object.""" + 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()) + + def powerOn(self): + """Power on the supply, with reasonable defaults for meshtastic devices.""" + self.r.set_i_set( + 0.300 + ) # Set current limit to 300mA - hopefully enough to power any board but not break things if there is a short circuit + + # self.r.set_v_set(3.7) # default to a nominal LiPo voltage + self.r.set_v_set(3.3) # my WM1110 devboard header is directly connected to the 3.3V rail + self.r.set_output(1) + + """Get current watts out. + But for most applications you probably want getWattHour() instead (to prevent integration errors from accumulating). + """ + return self.r.get_p_out() + + def getWattHour(self): + """Get current Wh out, since power was turned on.""" + # FIXME: Individual reads seem busted in the riden lib. So for now I just read everything. + self.r.update() + return self.r.wh + # return self.r.get_wh() + + def clearWattHour(self): + """Clear the watt-hour counter FIXME.""" + + +"""Used to match power mon log lines: +INFO | ??:??:?? 7 [Blink] S:PM:0x00000080,reason +""" +logRegex = re.compile(".*S:PM:0x([0-9A-Fa-f]+),(.*)") + + +class PowerMonClient: + """Client for monitoring power consumption of meshtastic devices.""" + + def __init__(self, portName: str, client: MeshInterface) -> None: + """Initialize the PowerMonClient object. + + Args: + client (MeshInterface): The MeshInterface object to monitor. + + """ + self.client = client + self.state = 0 # The current power mon state bitfields + self.columns = ["time", "power", "reason", "bitmask"] + self.rawData = pd.DataFrame(columns=self.columns) # use time as the index + + # for efficiency reasons we keep new data in a list - only adding to rawData when needfed + self.newData = [] + + self.power = power = PowerSupply(portName) + power.powerOn() + + # Used to calculate watts over an interval + self.prevPowerTime = datetime.now() + self.prevWattHour = power.getWattHour() + atexit.register(self._exitHandler) + client.onLogMessage.subscribe(self._onLogMessage) + + def getRawData(self) -> pd.DataFrame: + """Get the raw data. + + Returns: + pd.DataFrame: The raw data. + + """ + df = pd.DataFrame(self.newData, columns=self.columns) + self.rawData = pd.concat([self.rawData, df], ignore_index=True) + self.newData = [] + + return self.rawData + + def _exitHandler(self) -> None: + """Exit handler.""" + fn = "/tmp/powermon.csv" # Find a better place + logging.info(f"Storing PowerMon raw data in {fn}") + self.getRawData().to_csv(fn) + + def _onLogMessage(self, ev: Event) -> None: + """Callback function for handling log messages. + + Args: + message (str): The log message. + + """ + m = logRegex.match(ev.message) + if m: + mask = int(m.group(1), 16) + reason = m.group(2) + logging.debug(f"PowerMon state: 0x{mask:x}, reason: {reason}") + if mask != self.state: + self._storeRecord(mask, reason) + + def _storeRecord(self, mask: int, reason: str) -> None: + """Store a power mon record. + + Args: + mask (int): The power mon state bitfields. + reason (str): The reason for the power mon state change. + + """ + + now = datetime.now() + nowWattHour = self.power.getWattHour() + watts = ( + (nowWattHour - self.prevWattHour) + / (now - self.prevPowerTime).total_seconds() + * 3600 + ) + self.prevPowerTime = now + self.prevWattHour = nowWattHour + self.state = mask + + self.newData.append( + {"time": now, "power": watts, "reason": reason, "bitmask": mask}) + # self.getRawData() diff --git a/meshtastic/powermon_pb2.py b/meshtastic/powermon_pb2.py new file mode 100644 index 0000000..bd91da4 --- /dev/null +++ b/meshtastic/powermon_pb2.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: meshtastic/powermon.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19meshtastic/powermon.proto\x12\x13meshtastic.PowerMon\"\'\n\x05\x45vent\x12\x13\n\x06states\x18\x01 \x01(\x04H\x00\x88\x01\x01\x42\t\n\x07_states*\xfb\x01\n\x05State\x12\x08\n\x04None\x10\x00\x12\x11\n\rCPU_DeepSleep\x10\x01\x12\r\n\tCPU_Sleep\x10\x02\x12\r\n\tCPU_Awake\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\x11\n\x0cGPS_LowPower\x10\x80\x10\x12\x14\n\x0fGPS_MediumPower\x10\x80 \x12\x12\n\rGPS_HighPower\x10\x80@Bc\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) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'meshtastic.powermon_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\n\023com.geeksville.meshB\016PowerMonProtosZ\"github.com/meshtastic/go/generated\252\002\024Meshtastic.Protobufs\272\002\000' + _globals['_STATE']._serialized_start=92 + _globals['_STATE']._serialized_end=343 + _globals['_EVENT']._serialized_start=50 + _globals['_EVENT']._serialized_end=89 +# @@protoc_insertion_point(module_scope) diff --git a/meshtastic/powermon_pb2.pyi b/meshtastic/powermon_pb2.pyi new file mode 100644 index 0000000..df6971a --- /dev/null +++ b/meshtastic/powermon_pb2.pyi @@ -0,0 +1,84 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" + +import builtins +import google.protobuf.descriptor +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import sys +import typing + +if sys.version_info >= (3, 10): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +class _State: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + +class _StateEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_State.ValueType], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + CPU_DeepSleep: _State.ValueType # 1 + CPU_Sleep: _State.ValueType # 2 + CPU_Awake: _State.ValueType # 4 + Lora_RXOn: _State.ValueType # 8 + Lora_TXOn: _State.ValueType # 16 + Lora_RXActive: _State.ValueType # 32 + BT_On: _State.ValueType # 64 + LED_On: _State.ValueType # 128 + Screen_On: _State.ValueType # 256 + Screen_Drawing: _State.ValueType # 512 + Wifi_On: _State.ValueType # 1024 + GPS_LowPower: _State.ValueType # 2048 + GPS_MediumPower: _State.ValueType # 4096 + GPS_HighPower: _State.ValueType # 8192 + +class State(_State, metaclass=_StateEnumTypeWrapper): + """Any significant power changing event in meshtastic should be tagged with a powermon state transition. + If you are making new meshtastic features feel free to add new entries at the end of this definition. + """ + +CPU_DeepSleep: State.ValueType # 1 +CPU_Sleep: State.ValueType # 2 +CPU_Awake: State.ValueType # 4 +Lora_RXOn: State.ValueType # 8 +Lora_TXOn: State.ValueType # 16 +Lora_RXActive: State.ValueType # 32 +BT_On: State.ValueType # 64 +LED_On: State.ValueType # 128 +Screen_On: State.ValueType # 256 +Screen_Drawing: State.ValueType # 512 +Wifi_On: State.ValueType # 1024 +GPS_LowPower: State.ValueType # 2048 +GPS_MediumPower: State.ValueType # 4096 +GPS_HighPower: State.ValueType # 8192 +global___State = State + +@typing.final +class Event(google.protobuf.message.Message): + """ + the log messages will be short and complete (see PowerMon.Event in the protobufs for details). + something like "PwrMon,C,0x00001234,REASON" where the hex number is the bitmask of all current states. + (We use a bitmask for states so that if a log message gets lost it won't be fatal) + """ + + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + STATES_FIELD_NUMBER: builtins.int + states: builtins.int + """Bitwise-OR of States""" + def __init__( + self, + *, + states: builtins.int | None = ..., + ) -> None: ... + def HasField(self, field_name: typing.Literal["_states", b"_states", "states", b"states"]) -> builtins.bool: ... + def ClearField(self, field_name: typing.Literal["_states", b"_states", "states", b"states"]) -> None: ... + def WhichOneof(self, oneof_group: typing.Literal["_states", b"_states"]) -> typing.Literal["states"] | None: ... + +global___Event = Event diff --git a/meshtastic/stream_interface.py b/meshtastic/stream_interface.py index dea6923..c05a318 100644 --- a/meshtastic/stream_interface.py +++ b/meshtastic/stream_interface.py @@ -39,6 +39,7 @@ 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) @@ -124,6 +125,26 @@ 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 + + if self.debugOut is not None: + self.debugOut.write(utf) + def __reader(self): """The reader thread that reads bytes from our stream""" logging.debug("in __reader()") @@ -146,11 +167,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: diff --git a/poetry.lock b/poetry.lock index e7286ba..a045de4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -227,6 +227,20 @@ files = [ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -607,6 +621,20 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +[[package]] +name = "modbus-tk" +version = "1.1.3" +description = "Implementation of modbus protocol in python" +optional = false +python-versions = "*" +files = [ + {file = "modbus_tk-1.1.3-py3-none-any.whl", hash = "sha256:2b7afca05292a58371e7a7e4ec2931f2bb9b8a1a7c0295ada758740e72985aef"}, + {file = "modbus_tk-1.1.3.tar.gz", hash = "sha256:690fa7bb86ea978992465d2d61c8b5acc639ce0e8b833a0aa96d4dd172c5644a"}, +] + +[package.dependencies] +pyserial = ">=3.1" + [[package]] name = "mypy" version = "1.10.0" @@ -680,6 +708,60 @@ files = [ protobuf = ">=4.25.3" types-protobuf = ">=4.24" +[[package]] +name = "numpy" +version = "2.0.0" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-2.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:04494f6ec467ccb5369d1808570ae55f6ed9b5809d7f035059000a37b8d7e86f"}, + {file = "numpy-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2635dbd200c2d6faf2ef9a0d04f0ecc6b13b3cad54f7c67c61155138835515d2"}, + {file = "numpy-2.0.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:0a43f0974d501842866cc83471bdb0116ba0dffdbaac33ec05e6afed5b615238"}, + {file = "numpy-2.0.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:8d83bb187fb647643bd56e1ae43f273c7f4dbcdf94550d7938cfc32566756514"}, + {file = "numpy-2.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79e843d186c8fb1b102bef3e2bc35ef81160ffef3194646a7fdd6a73c6b97196"}, + {file = "numpy-2.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d7696c615765091cc5093f76fd1fa069870304beaccfd58b5dcc69e55ef49c1"}, + {file = "numpy-2.0.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b4c76e3d4c56f145d41b7b6751255feefae92edbc9a61e1758a98204200f30fc"}, + {file = "numpy-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:acd3a644e4807e73b4e1867b769fbf1ce8c5d80e7caaef0d90dcdc640dfc9787"}, + {file = "numpy-2.0.0-cp310-cp310-win32.whl", hash = "sha256:cee6cc0584f71adefe2c908856ccc98702baf95ff80092e4ca46061538a2ba98"}, + {file = "numpy-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:ed08d2703b5972ec736451b818c2eb9da80d66c3e84aed1deeb0c345fefe461b"}, + {file = "numpy-2.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad0c86f3455fbd0de6c31a3056eb822fc939f81b1618f10ff3406971893b62a5"}, + {file = "numpy-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7f387600d424f91576af20518334df3d97bc76a300a755f9a8d6e4f5cadd289"}, + {file = "numpy-2.0.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:34f003cb88b1ba38cb9a9a4a3161c1604973d7f9d5552c38bc2f04f829536609"}, + {file = "numpy-2.0.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:b6f6a8f45d0313db07d6d1d37bd0b112f887e1369758a5419c0370ba915b3871"}, + {file = "numpy-2.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f64641b42b2429f56ee08b4f427a4d2daf916ec59686061de751a55aafa22e4"}, + {file = "numpy-2.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7039a136017eaa92c1848152827e1424701532ca8e8967fe480fe1569dae581"}, + {file = "numpy-2.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46e161722e0f619749d1cd892167039015b2c2817296104487cd03ed4a955995"}, + {file = "numpy-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0e50842b2295ba8414c8c1d9d957083d5dfe9e16828b37de883f51fc53c4016f"}, + {file = "numpy-2.0.0-cp311-cp311-win32.whl", hash = "sha256:2ce46fd0b8a0c947ae047d222f7136fc4d55538741373107574271bc00e20e8f"}, + {file = "numpy-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd6acc766814ea6443628f4e6751d0da6593dae29c08c0b2606164db026970c"}, + {file = "numpy-2.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:354f373279768fa5a584bac997de6a6c9bc535c482592d7a813bb0c09be6c76f"}, + {file = "numpy-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4d2f62e55a4cd9c58c1d9a1c9edaedcd857a73cb6fda875bf79093f9d9086f85"}, + {file = "numpy-2.0.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:1e72728e7501a450288fc8e1f9ebc73d90cfd4671ebbd631f3e7857c39bd16f2"}, + {file = "numpy-2.0.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:84554fc53daa8f6abf8e8a66e076aff6ece62de68523d9f665f32d2fc50fd66e"}, + {file = "numpy-2.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c73aafd1afca80afecb22718f8700b40ac7cab927b8abab3c3e337d70e10e5a2"}, + {file = "numpy-2.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49d9f7d256fbc804391a7f72d4a617302b1afac1112fac19b6c6cec63fe7fe8a"}, + {file = "numpy-2.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0ec84b9ba0654f3b962802edc91424331f423dcf5d5f926676e0150789cb3d95"}, + {file = "numpy-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:feff59f27338135776f6d4e2ec7aeeac5d5f7a08a83e80869121ef8164b74af9"}, + {file = "numpy-2.0.0-cp312-cp312-win32.whl", hash = "sha256:c5a59996dc61835133b56a32ebe4ef3740ea5bc19b3983ac60cc32be5a665d54"}, + {file = "numpy-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a356364941fb0593bb899a1076b92dfa2029f6f5b8ba88a14fd0984aaf76d0df"}, + {file = "numpy-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e61155fae27570692ad1d327e81c6cf27d535a5d7ef97648a17d922224b216de"}, + {file = "numpy-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4554eb96f0fd263041baf16cf0881b3f5dafae7a59b1049acb9540c4d57bc8cb"}, + {file = "numpy-2.0.0-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:903703372d46bce88b6920a0cd86c3ad82dae2dbef157b5fc01b70ea1cfc430f"}, + {file = "numpy-2.0.0-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:3e8e01233d57639b2e30966c63d36fcea099d17c53bf424d77f088b0f4babd86"}, + {file = "numpy-2.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cde1753efe513705a0c6d28f5884e22bdc30438bf0085c5c486cdaff40cd67a"}, + {file = "numpy-2.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821eedb7165ead9eebdb569986968b541f9908979c2da8a4967ecac4439bae3d"}, + {file = "numpy-2.0.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9a1712c015831da583b21c5bfe15e8684137097969c6d22e8316ba66b5baabe4"}, + {file = "numpy-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9c27f0946a3536403efb0e1c28def1ae6730a72cd0d5878db38824855e3afc44"}, + {file = "numpy-2.0.0-cp39-cp39-win32.whl", hash = "sha256:63b92c512d9dbcc37f9d81b123dec99fdb318ba38c8059afc78086fe73820275"}, + {file = "numpy-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:3f6bed7f840d44c08ebdb73b1825282b801799e325bcbdfa6bc5c370e5aecc65"}, + {file = "numpy-2.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9416a5c2e92ace094e9f0082c5fd473502c91651fb896bc17690d6fc475128d6"}, + {file = "numpy-2.0.0-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:17067d097ed036636fa79f6a869ac26df7db1ba22039d962422506640314933a"}, + {file = "numpy-2.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ecb5b0582cd125f67a629072fed6f83562d9dd04d7e03256c9829bdec027ad"}, + {file = "numpy-2.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cef04d068f5fb0518a77857953193b6bb94809a806bd0a14983a8f12ada060c9"}, + {file = "numpy-2.0.0.tar.gz", hash = "sha256:cf5d1c9e6837f8af9f92b6bd3e86d513cdc11f60fd62185cc49ec7d1aba34864"}, +] + [[package]] name = "packaging" version = "24.1" @@ -691,6 +773,79 @@ files = [ {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] +[[package]] +name = "pandas" +version = "2.2.2" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"}, + {file = "pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238"}, + {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"}, + {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"}, + {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"}, + {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99"}, + {file = "pandas-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772"}, + {file = "pandas-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288"}, + {file = "pandas-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151"}, + {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b"}, + {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee"}, + {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db"}, + {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, + {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, + {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, + {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, + {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, + {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, + {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, + {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"}, + {file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"}, + {file = "pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2"}, + {file = "pandas-2.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd"}, + {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863"}, + {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921"}, + {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a"}, + {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92fd6b027924a7e178ac202cfbe25e53368db90d56872d20ffae94b96c7acc57"}, + {file = "pandas-2.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:640cef9aa381b60e296db324337a554aeeb883ead99dc8f6c18e81a93942f5f4"}, + {file = "pandas-2.2.2.tar.gz", hash = "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.7" + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] + [[package]] name = "pdoc3" version = "0.10.0" @@ -1062,6 +1217,31 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2024.1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, +] + [[package]] name = "pywin32-ctypes" version = "0.2.2" @@ -1153,6 +1333,26 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "riden" +version = "1.2.0" +description = "A python library for Riden RD power supplies" +optional = false +python-versions = ">=3.7,<4.0" +files = [] +develop = false + +[package.dependencies] +click = "^8.0.3" +modbus_tk = "^1.1.2" +pyserial = "^3.5" + +[package.source] +type = "git" +url = "https://github.com/geeksville/riden.git#ecbda543cf566346dd28558d957b4aa7fe116a83" +reference = "HEAD" +resolved_reference = "ecbda543cf566346dd28558d957b4aa7fe116a83" + [[package]] name = "setuptools" version = "70.1.0" @@ -1168,6 +1368,17 @@ files = [ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + [[package]] name = "sortedcontainers" version = "2.4.0" @@ -1294,6 +1505,17 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "tzdata" +version = "2024.1" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, +] + [[package]] name = "urllib3" version = "2.2.2" @@ -1560,5 +1782,5 @@ tunnel = [] [metadata] lock-version = "2.0" -python-versions = "^3.8,<3.13" -content-hash = "2ee78b53bdbdb0d560d2092d28d4379ec0ca18377e9d698f152d708318f9ca23" +python-versions = "^3.9,<3.13" +content-hash = "b893063a1dfe925f901dc45b01d68e255e2bd0ba68bda54de83c875c17b964d8" diff --git a/protobufs b/protobufs index a641c5c..bfde27a 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit a641c5ce4fca158d18ca3cffc92ac7a10f9b6a04 +Subproject commit bfde27a4ea5cfd0f60ffe961b5ce249aaf54c182 diff --git a/pyproject.toml b/pyproject.toml index 94cbcd2..b2dc9a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" # 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 pyserial = "^3.5" protobuf = ">=5.26.0" dotmap = "^1.3.30" @@ -29,6 +29,8 @@ types-requests = "^2.31.0.20240406" types-setuptools = "^69.5.0.20240423" types-pyyaml = "^6.0.12.20240311" packaging = "^24.0" +riden = {git = "https://github.com/geeksville/riden.git#ecbda543cf566346dd28558d957b4aa7fe116a83"} +pandas = "^2.2.2" [tool.poetry.group.dev.dependencies] hypothesis = "^6.103.2"