diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7ec9bbc..abde86e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: which meshtastic meshtastic --version - name: Run pylint - run: pylint meshtastic examples/ + run: pylint meshtastic examples/ --ignore-patterns ".*_pb2.py$" - name: Run tests with pytest run: pytest --cov=meshtastic - name: Generate coverage report diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 48fe55f..e0bf0f5 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -633,6 +633,10 @@ def onConnected(interface): interface.getNode(args.dest).showInfo() closeNow = True print("") + pypi_version = meshtastic.util.check_if_newer_version() + if pypi_version: + print(f'*** A newer version v{pypi_version} is available!' + ' Consider running "pip install --upgrade meshtastic" ***\n') else: print("Showing info of remote node is not supported.") print("Use the '--get' command for a specific configuration (e.g. 'lora') instead.") diff --git a/meshtastic/mesh_interface.py b/meshtastic/mesh_interface.py index 8879878..06036cc 100644 --- a/meshtastic/mesh_interface.py +++ b/meshtastic/mesh_interface.py @@ -5,6 +5,7 @@ import random import time import json import logging +import collections from typing import AnyStr import threading from datetime import datetime @@ -56,6 +57,8 @@ class MeshInterface: self.configId = None self.gotResponse = False # used in gpio read self.mask = None # used in gpio read and gpio watch + self.queueStatus = None + self.queue = collections.OrderedDict() def close(self): """Shutdown this interface""" @@ -509,13 +512,61 @@ class MeshInterface: m.disconnect = True self._sendToRadio(m) + def _queueHasFreeSpace(self): + # We never got queueStatus, maybe the firmware is old + if self.queueStatus is None: + return True + return self.queueStatus.free > 0 + + def _queueClaim(self): + if self.queueStatus is None: + return + self.queueStatus.free -= 1 + def _sendToRadio(self, toRadio): """Send a ToRadio protobuf to the device""" if self.noProto: logging.warning(f"Not sending packet because protocol use is disabled by noProto") else: #logging.debug(f"Sending toRadio: {stripnl(toRadio)}") - self._sendToRadioImpl(toRadio) + + if not toRadio.HasField('packet'): + # not a meshpacket -- send immediately, give queue a chance, + # this makes heartbeat trigger queue + self._sendToRadioImpl(toRadio) + else: + # meshpacket -- queue + self.queue[toRadio.packet.id] = toRadio + + resentQueue = collections.OrderedDict() + + while self.queue: + #logging.warn("queue: " + " ".join(f'{k:08x}' for k in self.queue)) + while not self._queueHasFreeSpace(): + logging.debug("Waiting for free space in TX Queue") + time.sleep(0.5) + try: + toResend = self.queue.popitem(last=False) + except KeyError: + break + packetId, packet = toResend + #logging.warn(f"packet: {packetId:08x} {packet}") + resentQueue[packetId] = packet + if packet is False: + continue + self._queueClaim() + if packet != toRadio: + logging.debug(f"Resending packet ID {packetId:08x} {packet}") + self._sendToRadioImpl(packet) + + #logging.warn("resentQueue: " + " ".join(f'{k:08x}' for k in resentQueue)) + for packetId, packet in resentQueue.items(): + if self.queue.pop(packetId, False) is False: # Packet got acked under us + logging.debug(f"packet {packetId:08x} got acked under us") + continue + if packet: + self.queue[packetId] = packet + #logging.warn("queue + resentQueue: " + " ".join(f'{k:08x}' for k in self.queue)) def _sendToRadioImpl(self, toRadio): """Send a ToRadio protobuf to the device""" @@ -528,6 +579,21 @@ class MeshInterface: """ self.localNode.requestChannels() + def _handleQueueStatusFromRadio(self, queueStatus): + self.queueStatus = queueStatus + logging.debug(f"TX QUEUE free {queueStatus.free} of {queueStatus.maxlen}, res = {queueStatus.res}, id = {queueStatus.mesh_packet_id:08x} ") + + if queueStatus.res: + return + + #logging.warn("queue: " + " ".join(f'{k:08x}' for k in self.queue)) + justQueued = self.queue.pop(queueStatus.mesh_packet_id, None) + + if justQueued is None and queueStatus.mesh_packet_id != 0: + self.queue[queueStatus.mesh_packet_id] = False + logging.debug(f"Reply for unexpected packet ID {queueStatus.mesh_packet_id:08x}") + #logging.warn("queue: " + " ".join(f'{k:08x}' for k in self.queue)) + def _handleFromRadio(self, fromRadioBytes): """ Handle a packet that arrived from the radio(update model and publish events) @@ -584,6 +650,9 @@ class MeshInterface: elif fromRadio.HasField("packet"): self._handlePacketFromRadio(fromRadio.packet) + elif fromRadio.HasField('queueStatus'): + self._handleQueueStatusFromRadio(fromRadio.queueStatus) + elif fromRadio.rebooted: # Tell clients the device went away. Careful not to call the overridden # subclass version that closes the serial port diff --git a/meshtastic/util.py b/meshtastic/util.py index 257d4d7..9d4bcc5 100644 --- a/meshtastic/util.py +++ b/meshtastic/util.py @@ -14,6 +14,8 @@ import subprocess import serial import serial.tools.list_ports import pkg_resources +import requests + from meshtastic.supported_device import supported_devices @@ -241,7 +243,11 @@ def support_info(): print(f' Encoding (stdin): {sys.stdin.encoding}') print(f' Encoding (stdout): {sys.stdout.encoding}') the_version = pkg_resources.get_distribution("meshtastic").version - print(f' meshtastic: v{the_version}') + pypi_version = check_if_newer_version() + if pypi_version: + print(f' meshtastic: v{the_version} (*** newer version v{pypi_version} available ***)') + else: + print(f' meshtastic: v{the_version}') print(f' Executable: {sys.argv[0]}') print(f' Python: {platform.python_version()} {platform.python_implementation()} {platform.python_compiler()}') print('') @@ -545,3 +551,19 @@ def detect_windows_port(sd): #print(f'x:{x}') ports.add(f'COM{x}') return ports + + +def check_if_newer_version(): + """Check pip to see if we are running the latest version.""" + pypi_version = None + try: + url = "https://pypi.org/pypi/meshtastic/json" + data = requests.get(url).json() + pypi_version = data["info"]["version"] + except Exception as e: + #print(f"could not get version from pypi e:{e}") + pass + act_version = pkg_resources.get_distribution("meshtastic").version + if pypi_version and pkg_resources.parse_version(pypi_version) <= pkg_resources.parse_version(act_version): + return None + return pypi_version diff --git a/requirements.txt b/requirements.txt index 8c351a1..a52ecd9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,7 @@ pyqrcode tabulate timeago webencodings +requests pyparsing twine autopep8 diff --git a/setup.py b/setup.py index eaa448a..c24aa8a 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ setup( ], packages=["meshtastic"], include_package_data=True, - install_requires=["pyserial>=3.4", "protobuf>=3.13.0", + install_requires=["pyserial>=3.4", "protobuf>=3.13.0", "requests>=2.25.0", "pypubsub>=4.0.3", "dotmap>=1.3.14", "pexpect>=4.6.0", "pyqrcode>=1.2.1", "tabulate>=0.8.9", "timeago>=1.0.15", "pyyaml", "pygatt>=4.0.5 ; platform_system=='Linux'"],