mirror of
https://github.com/meshtastic/python.git
synced 2026-04-17 21:42:20 -04:00
Merge remote-tracking branch 'root/ble-logging' into pr-fixbluetooth
This commit is contained in:
52
.github/workflows/release.yml
vendored
52
.github/workflows/release.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
- name: Get version
|
||||
id: get_version
|
||||
run: >-
|
||||
poetry version
|
||||
poetry version --short | sed 's/^/::set-output name=version::/'
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: actions/create-release@v1
|
||||
@@ -152,31 +152,31 @@ jobs:
|
||||
asset_name: readme.txt
|
||||
asset_content_type: text/plain
|
||||
|
||||
build-and-publish-windows:
|
||||
runs-on: windows-latest
|
||||
needs: release_create
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.release_create.outputs.new_sha }}
|
||||
# build-and-publish-windows:
|
||||
# runs-on: windows-latest
|
||||
# needs: release_create
|
||||
# steps:
|
||||
# - name: Checkout
|
||||
# uses: actions/checkout@v4
|
||||
# with:
|
||||
# ref: ${{ needs.release_create.outputs.new_sha }}
|
||||
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.9"
|
||||
# - name: Set up Python 3.9
|
||||
# uses: actions/setup-python@v5
|
||||
# with:
|
||||
# python-version: "3.9"
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
pip install poetry
|
||||
bin/build-bin.sh
|
||||
# - name: Build
|
||||
# run: |
|
||||
# pip install poetry
|
||||
# bin/build-bin.sh
|
||||
|
||||
- name: Add windows to release
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.release_create.outputs.upload_url }}
|
||||
asset_path: dist/meshtastic.exe
|
||||
asset_name: meshtastic_windows
|
||||
asset_content_type: application/zip
|
||||
# - name: Add windows to release
|
||||
# uses: actions/upload-release-asset@v1
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# with:
|
||||
# upload_url: ${{ needs.release_create.outputs.upload_url }}
|
||||
# asset_path: dist/meshtastic.exe
|
||||
# asset_name: meshtastic_windows
|
||||
# asset_content_type: application/zip
|
||||
|
||||
@@ -76,7 +76,7 @@ from typing import *
|
||||
|
||||
import google.protobuf.json_format
|
||||
import serial # type: ignore[import-untyped]
|
||||
import timeago # type: ignore[import-untyped]
|
||||
from dotmap import DotMap # type: ignore[import-untyped]
|
||||
from google.protobuf.json_format import MessageToJson
|
||||
from pubsub import pub # type: ignore[import-untyped]
|
||||
from tabulate import tabulate
|
||||
|
||||
@@ -6,6 +6,7 @@ import struct
|
||||
import time
|
||||
from threading import Event, Thread
|
||||
from typing import Optional
|
||||
from print_color import print
|
||||
|
||||
from bleak import BleakClient, BleakScanner, BLEDevice
|
||||
|
||||
@@ -16,6 +17,8 @@ SERVICE_UUID = "6ba1b218-15a8-461f-9fa8-5dcae273eafd"
|
||||
TORADIO_UUID = "f75c76d2-129e-4dad-a1dd-7866124401e7"
|
||||
FROMRADIO_UUID = "2c55e69e-4993-11ed-b878-0242ac120002"
|
||||
FROMNUM_UUID = "ed9da18c-a800-4f66-a670-aa7547e34453"
|
||||
LOGRADIO_UUID = "6c6fd238-78fa-436b-aacf-15c5be1ef2e2"
|
||||
|
||||
|
||||
|
||||
class BLEInterface(MeshInterface):
|
||||
@@ -75,12 +78,28 @@ class BLEInterface(MeshInterface):
|
||||
|
||||
logging.debug("Register FROMNUM notify callback")
|
||||
self.client.start_notify(FROMNUM_UUID, self.from_num_handler)
|
||||
self.client.start_notify(LOGRADIO_UUID, self.log_radio_handler)
|
||||
|
||||
async def from_num_handler(self, _, b): # pylint: disable=C0116
|
||||
from_num = struct.unpack("<I", bytes(b))[0]
|
||||
logging.debug(f"FROMNUM notify: {from_num}")
|
||||
self.should_read = True
|
||||
|
||||
async def log_radio_handler(self, _, b): # pylint: disable=C0116
|
||||
log_radio = b.decode('utf-8').replace('\n', '')
|
||||
if log_radio.startswith("DEBUG"):
|
||||
print(log_radio, color="cyan", end=None)
|
||||
elif log_radio.startswith("INFO"):
|
||||
print(log_radio, color="white", end=None)
|
||||
elif log_radio.startswith("WARN"):
|
||||
print(log_radio, color="yellow", end=None)
|
||||
elif log_radio.startswith("ERROR"):
|
||||
print(log_radio, color="red", end=None)
|
||||
else:
|
||||
print(log_radio, end=None)
|
||||
|
||||
self.should_read = False
|
||||
|
||||
@staticmethod
|
||||
def scan() -> list[BLEDevice]:
|
||||
"""Scan for available BLE devices."""
|
||||
|
||||
@@ -14,7 +14,6 @@ from decimal import Decimal
|
||||
from typing import Any, Callable, Dict, List, Optional, Union
|
||||
|
||||
import google.protobuf.json_format
|
||||
import timeago # type: ignore[import-untyped]
|
||||
from pubsub import pub # type: ignore[import-untyped]
|
||||
from tabulate import tabulate
|
||||
|
||||
@@ -42,6 +41,29 @@ from meshtastic.util import (
|
||||
)
|
||||
|
||||
|
||||
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"
|
||||
Zero or negative intervals simply return "now"
|
||||
"""
|
||||
intervals = (
|
||||
("year", 60 * 60 * 24 * 365),
|
||||
("month", 60 * 60 * 24 * 30),
|
||||
("day", 60 * 60 * 24),
|
||||
("hour", 60 * 60),
|
||||
("min", 60),
|
||||
("sec", 1),
|
||||
)
|
||||
for name, interval_duration in intervals:
|
||||
if delta_secs < interval_duration:
|
||||
continue
|
||||
x = delta_secs // interval_duration
|
||||
plur = "s" if x > 1 else ""
|
||||
return f"{x} {name}{plur} ago"
|
||||
|
||||
return "now"
|
||||
|
||||
|
||||
class MeshInterface: # pylint: disable=R0902
|
||||
"""Interface class for meshtastic devices
|
||||
|
||||
@@ -158,11 +180,13 @@ class MeshInterface: # pylint: disable=R0902
|
||||
|
||||
def getTimeAgo(ts) -> Optional[str]:
|
||||
"""Format how long ago have we heard from this node (aka timeago)."""
|
||||
return (
|
||||
timeago.format(datetime.fromtimestamp(ts), datetime.now())
|
||||
if ts
|
||||
else None
|
||||
)
|
||||
if ts is None:
|
||||
return None
|
||||
delta = datetime.now() - datetime.fromtimestamp(ts)
|
||||
delta_secs = int(delta.total_seconds())
|
||||
if delta_secs < 0:
|
||||
return None # not handling a timestamp from the future
|
||||
return _timeago(delta_secs)
|
||||
|
||||
rows: List[Dict[str, Any]] = []
|
||||
if self.nodesByNum:
|
||||
|
||||
@@ -5,9 +5,10 @@ import re
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from hypothesis import given, strategies as st
|
||||
|
||||
from .. import mesh_pb2, config_pb2, BROADCAST_ADDR, LOCAL_ADDR
|
||||
from ..mesh_interface import MeshInterface
|
||||
from ..mesh_interface import MeshInterface, _timeago
|
||||
from ..node import Node
|
||||
|
||||
# TODO
|
||||
@@ -684,3 +685,21 @@ def test_waitConnected_isConnected_timeout(capsys):
|
||||
out, err = capsys.readouterr()
|
||||
assert re.search(r"warn about something", err, re.MULTILINE)
|
||||
assert out == ""
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_timeago():
|
||||
"""Test that the _timeago function returns sane values"""
|
||||
assert _timeago(0) == "now"
|
||||
assert _timeago(1) == "1 sec ago"
|
||||
assert _timeago(15) == "15 secs ago"
|
||||
assert _timeago(333) == "5 mins ago"
|
||||
assert _timeago(99999) == "1 day ago"
|
||||
assert _timeago(9999999) == "3 months ago"
|
||||
assert _timeago(-999) == "now"
|
||||
|
||||
@given(seconds=st.integers())
|
||||
def test_timeago_fuzz(seconds):
|
||||
"""Fuzz _timeago to ensure it works with any integer"""
|
||||
val = _timeago(seconds)
|
||||
assert re.match(r"(now|\d+ (secs?|mins?|hours?|days?|months?|years?))", val)
|
||||
|
||||
@@ -24,8 +24,16 @@ import serial.tools.list_ports # type: ignore[import-untyped]
|
||||
from meshtastic.supported_device import supported_devices
|
||||
from meshtastic.version import get_active_version
|
||||
|
||||
"""Some devices such as a seger jlink we never want to accidentally open"""
|
||||
blacklistVids = dict.fromkeys([0x1366])
|
||||
"""Some devices such as a seger jlink or st-link we never want to accidentally open
|
||||
0x1915 NordicSemi (PPK2)
|
||||
"""
|
||||
blacklistVids = dict.fromkeys([0x1366, 0x0483, 0x1915])
|
||||
|
||||
"""Some devices are highly likely to be meshtastic.
|
||||
0x239a RAK4631
|
||||
0x303a Heltec tracker"""
|
||||
whitelistVids = dict.fromkeys([0x239a, 0x303a])
|
||||
|
||||
|
||||
def quoteBooleans(a_string):
|
||||
"""Quote booleans
|
||||
@@ -130,19 +138,35 @@ def findPorts(eliminate_duplicates: bool=False) -> List[str]:
|
||||
Returns:
|
||||
list -- a list of device paths
|
||||
"""
|
||||
l = list(
|
||||
all_ports = serial.tools.list_ports.comports()
|
||||
|
||||
# look for 'likely' meshtastic devices
|
||||
ports = list(
|
||||
map(
|
||||
lambda port: port.device,
|
||||
filter(
|
||||
lambda port: port.vid is not None and port.vid not in blacklistVids,
|
||||
serial.tools.list_ports.comports(),
|
||||
lambda port: port.vid is not None and port.vid in whitelistVids,
|
||||
all_ports,
|
||||
),
|
||||
)
|
||||
)
|
||||
l.sort()
|
||||
|
||||
# if no likely devices, just list everything not blacklisted
|
||||
if len(ports) == 0:
|
||||
ports = list(
|
||||
map(
|
||||
lambda port: port.device,
|
||||
filter(
|
||||
lambda port: port.vid is not None and port.vid not in blacklistVids,
|
||||
all_ports,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
ports.sort()
|
||||
if eliminate_duplicates:
|
||||
l = eliminate_duplicate_port(l)
|
||||
return l
|
||||
ports = eliminate_duplicate_port(ports)
|
||||
return ports
|
||||
|
||||
|
||||
class dotdict(dict):
|
||||
|
||||
24
poetry.lock
generated
24
poetry.lock
generated
@@ -762,6 +762,17 @@ files = [
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "print-color"
|
||||
version = "0.4.6"
|
||||
description = "A simple package to print in color to the terminal"
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
files = [
|
||||
{file = "print_color-0.4.6-py3-none-any.whl", hash = "sha256:494bd1cdb84daf481f0e63bd22b3c32f7d52827d8f5d9138a96bb01ca8ba9299"},
|
||||
{file = "print_color-0.4.6.tar.gz", hash = "sha256:d3aafc1666c8d31a85fffa6ee8e4f269f5d5e338d685b4e6179915c71867c585"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "5.27.1"
|
||||
@@ -1098,6 +1109,7 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||
@@ -1193,16 +1205,6 @@ files = [
|
||||
[package.extras]
|
||||
widechars = ["wcwidth"]
|
||||
|
||||
[[package]]
|
||||
name = "timeago"
|
||||
version = "1.0.16"
|
||||
description = "A very simple python library, used to format datetime with `*** time ago` statement. eg: \"3 hours ago\"."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "timeago-1.0.16-py3-none-any.whl", hash = "sha256:9b8cb2e3102b329f35a04aa4531982d867b093b19481cfbb1dac7845fa2f79b0"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.0.1"
|
||||
@@ -1561,4 +1563,4 @@ tunnel = []
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.8,<3.13"
|
||||
content-hash = "ad7784653845f37d34feab43aa3947b8d6e51d7f4e51679a8730e26b98dbe453"
|
||||
content-hash = "8e82c70af84ffd1525ece9c446bf06c9a1a1235cdf3bb6c563413daf389de353"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "meshtastic"
|
||||
version = "2.3.11"
|
||||
version = "2.3.12"
|
||||
description = "Python API & client shell for talking to Meshtastic devices"
|
||||
authors = ["Meshtastic Developers <contact@meshtastic.org>"]
|
||||
license = "GPL-3.0-only"
|
||||
@@ -14,7 +14,6 @@ dotmap = "^1.3.30"
|
||||
pexpect = "^4.9.0"
|
||||
pyqrcode = "^1.2.1"
|
||||
tabulate = "^0.9.0"
|
||||
timeago = "^1.0.16"
|
||||
webencodings = "^0.5.1"
|
||||
requests = "^2.31.0"
|
||||
pyparsing = "^3.1.2"
|
||||
@@ -22,6 +21,7 @@ pyyaml = "^6.0.1"
|
||||
pypubsub = "^4.0.3"
|
||||
bleak = "^0.21.1"
|
||||
packaging = "^24.0"
|
||||
print-color = "^0.4.6"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
hypothesis = "^6.103.2"
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import time
|
||||
|
||||
import meshtastic
|
||||
import meshtastic.serial_interface
|
||||
|
||||
interface = (
|
||||
meshtastic.SerialInterface()
|
||||
meshtastic.serial_interface.SerialInterface()
|
||||
) # By default will try to find a meshtastic device, otherwise provide a device path like /dev/ttyUSB0
|
||||
interface.sendText("hello mesh")
|
||||
interface.close()
|
||||
|
||||
Reference in New Issue
Block a user