Files
python/meshtastic/interface.py
2020-04-27 17:49:16 -07:00

131 lines
4.5 KiB
Python

import serial
import threading
import logging
import sys
from . import mesh_pb2
START1 = 0x94
START2 = 0xc3
HEADER_LEN = 4
MAX_TO_FROM_RADIO_SIZE = 512
"""
TODO:
* use port enumeration to find ports https://pyserial.readthedocs.io/en/latest/shortintro.html
Contains a reader thread that is always trying to read on the serial port.
methods:
- constructor(serialPort)
- sendData(destinationId, bytes, variant)
- sendPacket(destinationId, meshPacket) - throws errors if we have errors talking to the device
- close() - shuts down the interface
- init() - starts the enumeration process to download NodeDB etc... - we will not publish to topics until this enumeration completes
- radioConfig - getter/setter syntax: https://www.python-course.eu/python3_properties.php
- nodes - the database of received nodes
- myNodeInfo
- myNodeId
## PubSub topics
Use a pubsub model to communicate events [https://pypubsub.readthedocs.io/en/v4.0.3/ ]
- meshtastic.send(MeshPacket) - Not implemented, instead call send(packet) on MeshInterface
- meshtastic.connection.established - published once we've successfully connected to the radio and downloaded the node DB
- meshtastic.connection.lost - published once we've lost our link to the radio
- meshtastic.receive.position(MeshPacket)
- meshtastic.receive.user(MeshPacket)
- meshtastic.receive.data(MeshPacket)
- meshtastic.node.updated(NodeInfo) - published when a node in the DB changes (appears, location changed, username changed, etc...)
- meshtastic.debug(string)
"""
class MeshInterface:
"""Interface class for meshtastic devices"""
def __init__(self):
self.debugOut = sys.stdout
self.nodes = None # FIXME
self.configId = 17
self._startConfig()
def _startConfig(self):
"""Start device packets flowing"""
startConfig = mesh_pb2.ToRadio()
startConfig.want_config_id = self.configId
self._sendToRadio(startConfig)
def _sendToRadio(self, toRadio):
"""Send a ToRadio protobuf to the device"""
logging.error(f"Subclass must provide toradio: {toRadio}")
def _handleFromRadio(self, fromRadioBytes):
"""
Handle a packet that arrived from the radio (update model and publish events)
Called by subclasses."""
fromRadio = mesh_pb2.FromRadio()
fromRadio.ParseFromString(fromRadioBytes)
logging.debug(f"Received: {fromRadio}")
class StreamInterface(MeshInterface):
"""Interface class for meshtastic devices over a stream link (serial, TCP, etc)"""
def __init__(self, devPath):
"""Constructor, opens a connection to a specified serial port"""
logging.debug(f"Connecting to {devPath}")
self.rxBuf = bytes() # empty
self.stream = serial.Serial(devPath, 921600)
self.rxThread = threading.Thread(target=self.__reader, args=())
self.rxThread.start()
MeshInterface.__init__(self)
def _sendToRadio(self, toRadio):
"""Send a ToRadio protobuf to the device"""
logging.debug(f"Sending: {toRadio}")
b = toRadio.SerializeToString()
bufLen = len(b)
header = bytes([START1, START2, (bufLen >> 8) & 0xff, bufLen & 0xff])
self.stream.write(header)
self.stream.write(b)
def close(self):
self.stream.close() # This will cause our reader thread to exit
def __reader(self):
"""The reader thread that reads bytes from our stream"""
empty = bytes()
while True:
b = self.stream.read(1)
c = b[0]
ptr = len(self.rxBuf)
self.rxBuf = self.rxBuf + b # Assume we want to append this byte
if ptr == 0: # looking for START1
if c != START1:
self.rxBuf = empty # failed to find start
self.debugOut.write(b.decode("utf-8"))
elif ptr == 1: # looking for START2
if c != START2:
self.rfBuf = empty # failed to find start2
elif ptr >= HEADER_LEN: # we've at least got a header
# big endian length follos header
packetlen = (self.rxBuf[2] << 8) + self.rxBuf[3]
if ptr == HEADER_LEN: # we _just_ finished reading the header, validate length
if packetlen > MAX_TO_FROM_RADIO_SIZE:
self.rfBuf = empty # length ws out out bounds, restart
if len(self.rxBuf) != 0 and ptr == packetlen + HEADER_LEN:
self._handleFromRadio(self.rxBuf[HEADER_LEN:])