mirror of
https://github.com/meshtastic/python.git
synced 2026-01-02 21:07:55 -05:00
add first cut at docs
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
# Meshtastic-python
|
||||
|
||||
A python client for using Meshtastic devices. This small library (and example application) provides an easy API for sending and receiving messages over mesh radios. It also provides access to any of the operations/data available in the device user interface or the Android application. Events are delivered using a publish-subscribe model, and you can subscribe to only the message types you are interested in.
|
||||
A python client for using Meshtastic devices. This small library (and example application) provides an easy API for sending and receiving messages over mesh radios. It also provides access to any of the operations/data available in the device user interface or the Android application. Events are delivered using a publish-subscribe model, and you can subscribe to only the message types you are interested in.
|
||||
|
||||
You probably don't want this yet because it is a pre-alpha WIP.
|
||||
|
||||
For the API documentation [click here](./doc/meshtastic/index.html).
|
||||
For the rough notes/implementation plan see [TODO](./TODO.md).
|
||||
|
||||
|
||||
3
TODO.md
3
TODO.md
@@ -5,7 +5,8 @@
|
||||
- add fromId and toId to received messages dictionaries
|
||||
- update nodedb as nodes change
|
||||
- make docs decent
|
||||
- keep everything in dicts
|
||||
- radioConfig - getter/setter syntax: https://www.python-course.eu/python3_properties.php
|
||||
- DONE keep everything in dicts
|
||||
- document properties/fields
|
||||
- include examples in readme. hello.py, textchat.py, replymessage.py all as one little demo
|
||||
- have python client turn off radio sleep (use 0 for X to mean restore defaults)
|
||||
|
||||
2
bin/regen-docs.sh
Executable file
2
bin/regen-docs.sh
Executable file
@@ -0,0 +1,2 @@
|
||||
rm -rf doc
|
||||
pdoc3 --html --output-dir doc meshtastic
|
||||
@@ -1,9 +1,8 @@
|
||||
rm dist/*
|
||||
set -e
|
||||
|
||||
pydoc3 -w meshtastic
|
||||
mv *.html doc
|
||||
|
||||
bin/regen-docs.sh
|
||||
pandoc --from=markdown --to=rst --output=README README.md
|
||||
python3 setup.py sdist bdist_wheel
|
||||
python3 -m twine check dist/*
|
||||
# test the upload
|
||||
@@ -1,5 +1,7 @@
|
||||
rm dist/*
|
||||
set -e
|
||||
|
||||
bin/regen-docs.sh
|
||||
pandoc --from=markdown --to=rst --output=README README.md
|
||||
python3 setup.py sdist bdist_wheel
|
||||
python3 -m twine upload dist/*
|
||||
725
doc/meshtastic/index.html
Normal file
725
doc/meshtastic/index.html
Normal file
@@ -0,0 +1,725 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
|
||||
<meta name="generator" content="pdoc 0.8.1" />
|
||||
<title>meshtastic API documentation</title>
|
||||
<meta name="description" content="an API for Meshtastic devices …" />
|
||||
<link href='https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.0/normalize.min.css' rel='stylesheet'>
|
||||
<link href='https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/8.0.0/sanitize.min.css' rel='stylesheet'>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/github.min.css" rel="stylesheet">
|
||||
<style>.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:30px;overflow:hidden}#sidebar > *:last-child{margin-bottom:2cm}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:1em 0 .50em 0}h3{font-size:1.4em;margin:25px 0 10px 0}h4{margin:0;font-size:105%}a{color:#058;text-decoration:none;transition:color .3s ease-in-out}a:hover{color:#e82}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900}pre code{background:#f8f8f8;font-size:.8em;line-height:1.4em}code{background:#f2f2f1;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{background:#f8f8f8;border:0;border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0;padding:1ex}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{margin-top:.6em;font-weight:bold}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-weight:bold;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}.admonition{padding:.1em .5em;margin-bottom:1em}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
|
||||
<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%;height:100vh;overflow:auto;position:sticky;top:0}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.item .name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul{padding-left:1.5em}.toc > ul > li{margin-top:.5em}}</style>
|
||||
<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<article id="content">
|
||||
<header>
|
||||
<h1 class="title">Package <code>meshtastic</code></h1>
|
||||
</header>
|
||||
<section id="section-intro">
|
||||
<h2 id="an-api-for-meshtastic-devices">an API for Meshtastic devices</h2>
|
||||
<p>Primary class: StreamInterface</p>
|
||||
<p>properties of StreamInterface:</p>
|
||||
<ul>
|
||||
<li>radioConfig - Current radio configuration and device settings, if you write to this the new settings will be applied to
|
||||
the device.</li>
|
||||
<li>nodes - The database of received nodes.
|
||||
Includes always up-to-date location and username information for each
|
||||
node in the mesh.
|
||||
This is a read-only datastructure.</li>
|
||||
<li>myNodeInfo - You probably don't want this.</li>
|
||||
</ul>
|
||||
<h2 id="published-pubsub-topics">Published PubSub topics</h2>
|
||||
<p>We use a publish-subscribe model to communicate asynchronous events [<a href="https://pypubsub.readthedocs.io/en/v4.0.3/">https://pypubsub.readthedocs.io/en/v4.0.3/</a> ].
|
||||
Available
|
||||
topics:</p>
|
||||
<ul>
|
||||
<li>meshtastic.connection.established - published once we've successfully connected to the radio and downloaded the node DB</li>
|
||||
<li>meshtastic.connection.lost - published once we've lost our link to the radio</li>
|
||||
<li>meshtastic.receive.position(packet) - delivers a received packet as a dictionary, if you only care about a particular
|
||||
type of packet, you should subscribe to the full topic name.
|
||||
If you want to see all packets, simply subscribe to "meshtastic.receive".</li>
|
||||
<li>meshtastic.receive.user(packet)</li>
|
||||
<li>meshtastic.receive.data(packet)</li>
|
||||
<li>meshtastic.node.updated(node = NodeInfo) - published when a node in the DB changes (appears, location changed, username changed, etc…)</li>
|
||||
</ul>
|
||||
<p>Example Usage:</p>
|
||||
<pre><code>import meshtastic
|
||||
from pubsub import pub
|
||||
|
||||
def onReceive(packet):
|
||||
print(f"Received: {packet}")
|
||||
|
||||
interface = StreamInterface() # By default will try to find a meshtastic device, otherwise provide a device path like /dev/ttyUSB0
|
||||
pub.subscribe(onReceive, "meshtastic.receive")
|
||||
interface.sendData("hello world") # defaults to broadcast, specify a destination ID if you wish
|
||||
</code></pre>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">"""
|
||||
## an API for Meshtastic devices
|
||||
|
||||
Primary class: StreamInterface
|
||||
|
||||
properties of StreamInterface:
|
||||
|
||||
- radioConfig - Current radio configuration and device settings, if you write to this the new settings will be applied to
|
||||
the device.
|
||||
- nodes - The database of received nodes. Includes always up-to-date location and username information for each
|
||||
node in the mesh. This is a read-only datastructure.
|
||||
- myNodeInfo - You probably don't want this.
|
||||
|
||||
## Published PubSub topics
|
||||
|
||||
We use a publish-subscribe model to communicate asynchronous events [https://pypubsub.readthedocs.io/en/v4.0.3/ ]. Available
|
||||
topics:
|
||||
|
||||
- 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(packet) - delivers a received packet as a dictionary, if you only care about a particular
|
||||
type of packet, you should subscribe to the full topic name. If you want to see all packets, simply subscribe to "meshtastic.receive".
|
||||
- meshtastic.receive.user(packet)
|
||||
- meshtastic.receive.data(packet)
|
||||
- meshtastic.node.updated(node = NodeInfo) - published when a node in the DB changes (appears, location changed, username changed, etc...)
|
||||
|
||||
Example Usage:
|
||||
|
||||
```
|
||||
import meshtastic
|
||||
from pubsub import pub
|
||||
|
||||
def onReceive(packet):
|
||||
print(f"Received: {packet}")
|
||||
|
||||
interface = StreamInterface() # By default will try to find a meshtastic device, otherwise provide a device path like /dev/ttyUSB0
|
||||
pub.subscribe(onReceive, "meshtastic.receive")
|
||||
interface.sendData("hello world") # defaults to broadcast, specify a destination ID if you wish
|
||||
```
|
||||
|
||||
"""
|
||||
|
||||
import google.protobuf.json_format
|
||||
import serial
|
||||
import serial.tools.list_ports
|
||||
import threading
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
from . import mesh_pb2
|
||||
from pubsub import pub
|
||||
|
||||
START1 = 0x94
|
||||
START2 = 0xc3
|
||||
HEADER_LEN = 4
|
||||
MAX_TO_FROM_RADIO_SIZE = 512
|
||||
|
||||
BROADCAST_ADDR = "all" # A special ID that means broadcast
|
||||
|
||||
|
||||
MY_CONFIG_ID = 42
|
||||
|
||||
|
||||
class MeshInterface:
|
||||
"""Interface class for meshtastic devices
|
||||
"""
|
||||
|
||||
def __init__(self, debugOut=None):
|
||||
"""Constructor"""
|
||||
self.debugOut = debugOut
|
||||
self.nodes = None # FIXME
|
||||
self._startConfig()
|
||||
|
||||
def sendText(self, text, destinationId=BROADCAST_ADDR):
|
||||
"""Send a utf8 string to some other node, if the node has a display it will also be shown on the device.
|
||||
|
||||
Arguments:
|
||||
text {string} -- The text to send
|
||||
|
||||
Keyword Arguments:
|
||||
destinationId {nodeId} -- where to send this message (default: {BROADCAST_ADDR})
|
||||
"""
|
||||
self.sendData(text.encode("utf-8"), destinationId,
|
||||
dataType=mesh_pb2.Data.CLEAR_TEXT)
|
||||
|
||||
def sendData(self, byteData, destinationId=BROADCAST_ADDR, dataType=mesh_pb2.Data.OPAQUE):
|
||||
"""Send a data packet to some other node"""
|
||||
meshPacket = mesh_pb2.MeshPacket()
|
||||
meshPacket.payload.data.payload = byteData
|
||||
meshPacket.payload.data.typ = dataType
|
||||
self.sendPacket(meshPacket, destinationId)
|
||||
|
||||
def sendPacket(self, meshPacket, destinationId=BROADCAST_ADDR):
|
||||
"""Send a MeshPacket to the specified node (or if unspecified, broadcast).
|
||||
You probably don't want this - use sendData instead."""
|
||||
toRadio = mesh_pb2.ToRadio()
|
||||
# FIXME add support for non broadcast addresses
|
||||
meshPacket.to = 255
|
||||
toRadio.packet.CopyFrom(meshPacket)
|
||||
self._sendToRadio(toRadio)
|
||||
|
||||
def _disconnected(self):
|
||||
"""Called by subclasses to tell clients this interface has disconnected"""
|
||||
pub.sendMessage("meshtastic.connection.lost")
|
||||
|
||||
def _startConfig(self):
|
||||
"""Start device packets flowing"""
|
||||
self.myInfo = None
|
||||
self.nodes = {} # nodes keyed by ID
|
||||
self._nodesByNum = {} # nodes keyed by nodenum
|
||||
self.radioConfig = None
|
||||
|
||||
startConfig = mesh_pb2.ToRadio()
|
||||
startConfig.want_config_id = MY_CONFIG_ID # we don't use this value
|
||||
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)
|
||||
json = google.protobuf.json_format.MessageToJson(fromRadio)
|
||||
logging.debug(f"Received: {json}")
|
||||
if fromRadio.HasField("my_info"):
|
||||
self.myInfo = fromRadio.my_info
|
||||
elif fromRadio.HasField("radio"):
|
||||
self.radioConfig = fromRadio.radio
|
||||
elif fromRadio.HasField("node_info"):
|
||||
node = fromRadio.node_info
|
||||
self._nodesByNum[node.num] = node
|
||||
self.nodes[node.user.id] = node
|
||||
elif fromRadio.config_complete_id == MY_CONFIG_ID:
|
||||
# we ignore the config_complete_id, it is unneeded for our stream API fromRadio.config_complete_id
|
||||
pub.sendMessage("meshtastic.connection.established")
|
||||
elif fromRadio.HasField("packet"):
|
||||
self._handlePacketFromRadio(fromRadio.packet)
|
||||
else:
|
||||
logging.warn("Unexpected FromRadio payload")
|
||||
|
||||
def _handlePacketFromRadio(self, meshPacket):
|
||||
"""Handle a MeshPacket that just arrived from the radio
|
||||
|
||||
Will publish one of the following events:
|
||||
- meshtastic.receive.position(packet = MeshPacket dictionary)
|
||||
- meshtastic.receive.user(packet = MeshPacket dictionary)
|
||||
- meshtastic.receive.data(packet = MeshPacket dictionary)
|
||||
"""
|
||||
# FIXME, update node DB as needed
|
||||
json = google.protobuf.json_format.MessageToDict(meshPacket)
|
||||
if meshPacket.payload.HasField("position"):
|
||||
pub.sendMessage("meshtastic.receive.position", packet=json)
|
||||
if meshPacket.payload.HasField("user"):
|
||||
pub.sendMessage("meshtastic.receive.user",
|
||||
packet=json)
|
||||
if meshPacket.payload.HasField("data"):
|
||||
pub.sendMessage("meshtastic.receive.data",
|
||||
packet=json)
|
||||
|
||||
|
||||
class StreamInterface(MeshInterface):
|
||||
"""Interface class for meshtastic devices over a stream link (serial, TCP, etc)"""
|
||||
|
||||
def __init__(self, devPath=None, debugOut=None):
|
||||
"""Constructor, opens a connection to a specified serial port, or if unspecified try to
|
||||
find one Meshtastic device by probing
|
||||
|
||||
Keyword Arguments:
|
||||
devPath {string} -- A filepath to a device, i.e. /dev/ttyUSB0 (default: {None})
|
||||
debugOut {stream} -- If a stream is provided, any debug serial output from the device will be emitted to that stream. (default: {None})
|
||||
|
||||
Raises:
|
||||
Exception: [description]
|
||||
Exception: [description]
|
||||
"""
|
||||
|
||||
if devPath is None:
|
||||
ports = list(filter(lambda port: port.vid != None,
|
||||
serial.tools.list_ports.comports()))
|
||||
if len(ports) == 0:
|
||||
raise Exception("No Meshtastic devices detected")
|
||||
elif len(ports) > 1:
|
||||
raise Exception(
|
||||
f"Multiple ports detected, you must specify a device, such as {ports[0].device}")
|
||||
else:
|
||||
devPath = ports[0].device
|
||||
|
||||
logging.debug(f"Connecting to {devPath}")
|
||||
self._rxBuf = bytes() # empty
|
||||
self._wantExit = False
|
||||
self.stream = serial.Serial(
|
||||
devPath, 921600, exclusive=True, timeout=0.5)
|
||||
self._rxThread = threading.Thread(target=self.__reader, args=())
|
||||
self._rxThread.start()
|
||||
MeshInterface.__init__(self, debugOut=debugOut)
|
||||
|
||||
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)
|
||||
self.stream.flush()
|
||||
|
||||
def close(self):
|
||||
"""Close a connection to the device"""
|
||||
logging.debug("Closing serial stream")
|
||||
# pyserial cancel_read doesn't seem to work, therefore we ask the reader thread to close things for us
|
||||
self._wantExit = True
|
||||
|
||||
def __reader(self):
|
||||
"""The reader thread that reads bytes from our stream"""
|
||||
empty = bytes()
|
||||
|
||||
while not self._wantExit:
|
||||
b = self.stream.read(1)
|
||||
if len(b) > 0:
|
||||
# logging.debug(f"read returned {b}")
|
||||
c = b[0]
|
||||
ptr = len(self._rxBuf)
|
||||
|
||||
# Assume we want to append this byte, fixme use bytearray instead
|
||||
self._rxBuf = self._rxBuf + b
|
||||
|
||||
if ptr == 0: # looking for START1
|
||||
if c != START1:
|
||||
self._rxBuf = empty # failed to find start
|
||||
if self.debugOut != None:
|
||||
try:
|
||||
self.debugOut.write(b.decode("utf-8"))
|
||||
except:
|
||||
self.debugOut.write('?')
|
||||
|
||||
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 + 1 == packetlen + HEADER_LEN:
|
||||
try:
|
||||
self._handleFromRadio(self._rxBuf[HEADER_LEN:])
|
||||
except Exception as ex:
|
||||
logging.warn(
|
||||
f"Error handling FromRadio, possibly corrupted? {ex}")
|
||||
traceback.print_exc()
|
||||
self._rxBuf = empty
|
||||
logging.debug("reader is exiting")
|
||||
self.stream.close()
|
||||
self._disconnected()</code></pre>
|
||||
</details>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="section-title" id="header-submodules">Sub-modules</h2>
|
||||
<dl>
|
||||
<dt><code class="name"><a title="meshtastic.mesh_pb2" href="mesh_pb2.html">meshtastic.mesh_pb2</a></code></dt>
|
||||
<dd>
|
||||
<div class="desc"></div>
|
||||
</dd>
|
||||
</dl>
|
||||
</section>
|
||||
<section>
|
||||
</section>
|
||||
<section>
|
||||
</section>
|
||||
<section>
|
||||
<h2 class="section-title" id="header-classes">Classes</h2>
|
||||
<dl>
|
||||
<dt id="meshtastic.MeshInterface"><code class="flex name class">
|
||||
<span>class <span class="ident">MeshInterface</span></span>
|
||||
<span>(</span><span>debugOut=None)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<div class="desc"><p>Interface class for meshtastic devices</p>
|
||||
<p>Constructor</p></div>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">class MeshInterface:
|
||||
"""Interface class for meshtastic devices
|
||||
"""
|
||||
|
||||
def __init__(self, debugOut=None):
|
||||
"""Constructor"""
|
||||
self.debugOut = debugOut
|
||||
self.nodes = None # FIXME
|
||||
self._startConfig()
|
||||
|
||||
def sendText(self, text, destinationId=BROADCAST_ADDR):
|
||||
"""Send a utf8 string to some other node, if the node has a display it will also be shown on the device.
|
||||
|
||||
Arguments:
|
||||
text {string} -- The text to send
|
||||
|
||||
Keyword Arguments:
|
||||
destinationId {nodeId} -- where to send this message (default: {BROADCAST_ADDR})
|
||||
"""
|
||||
self.sendData(text.encode("utf-8"), destinationId,
|
||||
dataType=mesh_pb2.Data.CLEAR_TEXT)
|
||||
|
||||
def sendData(self, byteData, destinationId=BROADCAST_ADDR, dataType=mesh_pb2.Data.OPAQUE):
|
||||
"""Send a data packet to some other node"""
|
||||
meshPacket = mesh_pb2.MeshPacket()
|
||||
meshPacket.payload.data.payload = byteData
|
||||
meshPacket.payload.data.typ = dataType
|
||||
self.sendPacket(meshPacket, destinationId)
|
||||
|
||||
def sendPacket(self, meshPacket, destinationId=BROADCAST_ADDR):
|
||||
"""Send a MeshPacket to the specified node (or if unspecified, broadcast).
|
||||
You probably don't want this - use sendData instead."""
|
||||
toRadio = mesh_pb2.ToRadio()
|
||||
# FIXME add support for non broadcast addresses
|
||||
meshPacket.to = 255
|
||||
toRadio.packet.CopyFrom(meshPacket)
|
||||
self._sendToRadio(toRadio)
|
||||
|
||||
def _disconnected(self):
|
||||
"""Called by subclasses to tell clients this interface has disconnected"""
|
||||
pub.sendMessage("meshtastic.connection.lost")
|
||||
|
||||
def _startConfig(self):
|
||||
"""Start device packets flowing"""
|
||||
self.myInfo = None
|
||||
self.nodes = {} # nodes keyed by ID
|
||||
self._nodesByNum = {} # nodes keyed by nodenum
|
||||
self.radioConfig = None
|
||||
|
||||
startConfig = mesh_pb2.ToRadio()
|
||||
startConfig.want_config_id = MY_CONFIG_ID # we don't use this value
|
||||
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)
|
||||
json = google.protobuf.json_format.MessageToJson(fromRadio)
|
||||
logging.debug(f"Received: {json}")
|
||||
if fromRadio.HasField("my_info"):
|
||||
self.myInfo = fromRadio.my_info
|
||||
elif fromRadio.HasField("radio"):
|
||||
self.radioConfig = fromRadio.radio
|
||||
elif fromRadio.HasField("node_info"):
|
||||
node = fromRadio.node_info
|
||||
self._nodesByNum[node.num] = node
|
||||
self.nodes[node.user.id] = node
|
||||
elif fromRadio.config_complete_id == MY_CONFIG_ID:
|
||||
# we ignore the config_complete_id, it is unneeded for our stream API fromRadio.config_complete_id
|
||||
pub.sendMessage("meshtastic.connection.established")
|
||||
elif fromRadio.HasField("packet"):
|
||||
self._handlePacketFromRadio(fromRadio.packet)
|
||||
else:
|
||||
logging.warn("Unexpected FromRadio payload")
|
||||
|
||||
def _handlePacketFromRadio(self, meshPacket):
|
||||
"""Handle a MeshPacket that just arrived from the radio
|
||||
|
||||
Will publish one of the following events:
|
||||
- meshtastic.receive.position(packet = MeshPacket dictionary)
|
||||
- meshtastic.receive.user(packet = MeshPacket dictionary)
|
||||
- meshtastic.receive.data(packet = MeshPacket dictionary)
|
||||
"""
|
||||
# FIXME, update node DB as needed
|
||||
json = google.protobuf.json_format.MessageToDict(meshPacket)
|
||||
if meshPacket.payload.HasField("position"):
|
||||
pub.sendMessage("meshtastic.receive.position", packet=json)
|
||||
if meshPacket.payload.HasField("user"):
|
||||
pub.sendMessage("meshtastic.receive.user",
|
||||
packet=json)
|
||||
if meshPacket.payload.HasField("data"):
|
||||
pub.sendMessage("meshtastic.receive.data",
|
||||
packet=json)</code></pre>
|
||||
</details>
|
||||
<h3>Subclasses</h3>
|
||||
<ul class="hlist">
|
||||
<li><a title="meshtastic.StreamInterface" href="#meshtastic.StreamInterface">StreamInterface</a></li>
|
||||
</ul>
|
||||
<h3>Methods</h3>
|
||||
<dl>
|
||||
<dt id="meshtastic.MeshInterface.sendData"><code class="name flex">
|
||||
<span>def <span class="ident">sendData</span></span>(<span>self, byteData, destinationId='all', dataType=0)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<div class="desc"><p>Send a data packet to some other node</p></div>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">def sendData(self, byteData, destinationId=BROADCAST_ADDR, dataType=mesh_pb2.Data.OPAQUE):
|
||||
"""Send a data packet to some other node"""
|
||||
meshPacket = mesh_pb2.MeshPacket()
|
||||
meshPacket.payload.data.payload = byteData
|
||||
meshPacket.payload.data.typ = dataType
|
||||
self.sendPacket(meshPacket, destinationId)</code></pre>
|
||||
</details>
|
||||
</dd>
|
||||
<dt id="meshtastic.MeshInterface.sendPacket"><code class="name flex">
|
||||
<span>def <span class="ident">sendPacket</span></span>(<span>self, meshPacket, destinationId='all')</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<div class="desc"><p>Send a MeshPacket to the specified node (or if unspecified, broadcast).
|
||||
You probably don't want this - use sendData instead.</p></div>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">def sendPacket(self, meshPacket, destinationId=BROADCAST_ADDR):
|
||||
"""Send a MeshPacket to the specified node (or if unspecified, broadcast).
|
||||
You probably don't want this - use sendData instead."""
|
||||
toRadio = mesh_pb2.ToRadio()
|
||||
# FIXME add support for non broadcast addresses
|
||||
meshPacket.to = 255
|
||||
toRadio.packet.CopyFrom(meshPacket)
|
||||
self._sendToRadio(toRadio)</code></pre>
|
||||
</details>
|
||||
</dd>
|
||||
<dt id="meshtastic.MeshInterface.sendText"><code class="name flex">
|
||||
<span>def <span class="ident">sendText</span></span>(<span>self, text, destinationId='all')</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<div class="desc"><p>Send a utf8 string to some other node, if the node has a display it will also be shown on the device.</p>
|
||||
<h2 id="arguments">Arguments</h2>
|
||||
<p>text {string} – The text to send</p>
|
||||
<p>Keyword Arguments:
|
||||
destinationId {nodeId} – where to send this message (default: {BROADCAST_ADDR})</p></div>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">def sendText(self, text, destinationId=BROADCAST_ADDR):
|
||||
"""Send a utf8 string to some other node, if the node has a display it will also be shown on the device.
|
||||
|
||||
Arguments:
|
||||
text {string} -- The text to send
|
||||
|
||||
Keyword Arguments:
|
||||
destinationId {nodeId} -- where to send this message (default: {BROADCAST_ADDR})
|
||||
"""
|
||||
self.sendData(text.encode("utf-8"), destinationId,
|
||||
dataType=mesh_pb2.Data.CLEAR_TEXT)</code></pre>
|
||||
</details>
|
||||
</dd>
|
||||
</dl>
|
||||
</dd>
|
||||
<dt id="meshtastic.StreamInterface"><code class="flex name class">
|
||||
<span>class <span class="ident">StreamInterface</span></span>
|
||||
<span>(</span><span>devPath=None, debugOut=None)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<div class="desc"><p>Interface class for meshtastic devices over a stream link (serial, TCP, etc)</p>
|
||||
<p>Constructor, opens a connection to a specified serial port, or if unspecified try to
|
||||
find one Meshtastic device by probing</p>
|
||||
<p>Keyword Arguments:
|
||||
devPath {string} – A filepath to a device, i.e. /dev/ttyUSB0 (default: {None})
|
||||
debugOut {stream} – If a stream is provided, any debug serial output from the device will be emitted to that stream. (default: {None})</p>
|
||||
<h2 id="raises">Raises</h2>
|
||||
<dl>
|
||||
<dt><code>Exception</code></dt>
|
||||
<dd>[description]</dd>
|
||||
<dt><code>Exception</code></dt>
|
||||
<dd>[description]</dd>
|
||||
</dl></div>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">class StreamInterface(MeshInterface):
|
||||
"""Interface class for meshtastic devices over a stream link (serial, TCP, etc)"""
|
||||
|
||||
def __init__(self, devPath=None, debugOut=None):
|
||||
"""Constructor, opens a connection to a specified serial port, or if unspecified try to
|
||||
find one Meshtastic device by probing
|
||||
|
||||
Keyword Arguments:
|
||||
devPath {string} -- A filepath to a device, i.e. /dev/ttyUSB0 (default: {None})
|
||||
debugOut {stream} -- If a stream is provided, any debug serial output from the device will be emitted to that stream. (default: {None})
|
||||
|
||||
Raises:
|
||||
Exception: [description]
|
||||
Exception: [description]
|
||||
"""
|
||||
|
||||
if devPath is None:
|
||||
ports = list(filter(lambda port: port.vid != None,
|
||||
serial.tools.list_ports.comports()))
|
||||
if len(ports) == 0:
|
||||
raise Exception("No Meshtastic devices detected")
|
||||
elif len(ports) > 1:
|
||||
raise Exception(
|
||||
f"Multiple ports detected, you must specify a device, such as {ports[0].device}")
|
||||
else:
|
||||
devPath = ports[0].device
|
||||
|
||||
logging.debug(f"Connecting to {devPath}")
|
||||
self._rxBuf = bytes() # empty
|
||||
self._wantExit = False
|
||||
self.stream = serial.Serial(
|
||||
devPath, 921600, exclusive=True, timeout=0.5)
|
||||
self._rxThread = threading.Thread(target=self.__reader, args=())
|
||||
self._rxThread.start()
|
||||
MeshInterface.__init__(self, debugOut=debugOut)
|
||||
|
||||
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)
|
||||
self.stream.flush()
|
||||
|
||||
def close(self):
|
||||
"""Close a connection to the device"""
|
||||
logging.debug("Closing serial stream")
|
||||
# pyserial cancel_read doesn't seem to work, therefore we ask the reader thread to close things for us
|
||||
self._wantExit = True
|
||||
|
||||
def __reader(self):
|
||||
"""The reader thread that reads bytes from our stream"""
|
||||
empty = bytes()
|
||||
|
||||
while not self._wantExit:
|
||||
b = self.stream.read(1)
|
||||
if len(b) > 0:
|
||||
# logging.debug(f"read returned {b}")
|
||||
c = b[0]
|
||||
ptr = len(self._rxBuf)
|
||||
|
||||
# Assume we want to append this byte, fixme use bytearray instead
|
||||
self._rxBuf = self._rxBuf + b
|
||||
|
||||
if ptr == 0: # looking for START1
|
||||
if c != START1:
|
||||
self._rxBuf = empty # failed to find start
|
||||
if self.debugOut != None:
|
||||
try:
|
||||
self.debugOut.write(b.decode("utf-8"))
|
||||
except:
|
||||
self.debugOut.write('?')
|
||||
|
||||
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 + 1 == packetlen + HEADER_LEN:
|
||||
try:
|
||||
self._handleFromRadio(self._rxBuf[HEADER_LEN:])
|
||||
except Exception as ex:
|
||||
logging.warn(
|
||||
f"Error handling FromRadio, possibly corrupted? {ex}")
|
||||
traceback.print_exc()
|
||||
self._rxBuf = empty
|
||||
logging.debug("reader is exiting")
|
||||
self.stream.close()
|
||||
self._disconnected()</code></pre>
|
||||
</details>
|
||||
<h3>Ancestors</h3>
|
||||
<ul class="hlist">
|
||||
<li><a title="meshtastic.MeshInterface" href="#meshtastic.MeshInterface">MeshInterface</a></li>
|
||||
</ul>
|
||||
<h3>Methods</h3>
|
||||
<dl>
|
||||
<dt id="meshtastic.StreamInterface.close"><code class="name flex">
|
||||
<span>def <span class="ident">close</span></span>(<span>self)</span>
|
||||
</code></dt>
|
||||
<dd>
|
||||
<div class="desc"><p>Close a connection to the device</p></div>
|
||||
<details class="source">
|
||||
<summary>
|
||||
<span>Expand source code</span>
|
||||
</summary>
|
||||
<pre><code class="python">def close(self):
|
||||
"""Close a connection to the device"""
|
||||
logging.debug("Closing serial stream")
|
||||
# pyserial cancel_read doesn't seem to work, therefore we ask the reader thread to close things for us
|
||||
self._wantExit = True</code></pre>
|
||||
</details>
|
||||
</dd>
|
||||
</dl>
|
||||
<h3>Inherited members</h3>
|
||||
<ul class="hlist">
|
||||
<li><code><b><a title="meshtastic.MeshInterface" href="#meshtastic.MeshInterface">MeshInterface</a></b></code>:
|
||||
<ul class="hlist">
|
||||
<li><code><a title="meshtastic.MeshInterface.sendData" href="#meshtastic.MeshInterface.sendData">sendData</a></code></li>
|
||||
<li><code><a title="meshtastic.MeshInterface.sendPacket" href="#meshtastic.MeshInterface.sendPacket">sendPacket</a></code></li>
|
||||
<li><code><a title="meshtastic.MeshInterface.sendText" href="#meshtastic.MeshInterface.sendText">sendText</a></code></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</section>
|
||||
</article>
|
||||
<nav id="sidebar">
|
||||
<h1>Index</h1>
|
||||
<div class="toc">
|
||||
<ul>
|
||||
<li><a href="#an-api-for-meshtastic-devices">an API for Meshtastic devices</a></li>
|
||||
<li><a href="#published-pubsub-topics">Published PubSub topics</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<ul id="index">
|
||||
<li><h3><a href="#header-submodules">Sub-modules</a></h3>
|
||||
<ul>
|
||||
<li><code><a title="meshtastic.mesh_pb2" href="mesh_pb2.html">meshtastic.mesh_pb2</a></code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><h3><a href="#header-classes">Classes</a></h3>
|
||||
<ul>
|
||||
<li>
|
||||
<h4><code><a title="meshtastic.MeshInterface" href="#meshtastic.MeshInterface">MeshInterface</a></code></h4>
|
||||
<ul class="">
|
||||
<li><code><a title="meshtastic.MeshInterface.sendData" href="#meshtastic.MeshInterface.sendData">sendData</a></code></li>
|
||||
<li><code><a title="meshtastic.MeshInterface.sendPacket" href="#meshtastic.MeshInterface.sendPacket">sendPacket</a></code></li>
|
||||
<li><code><a title="meshtastic.MeshInterface.sendText" href="#meshtastic.MeshInterface.sendText">sendText</a></code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<h4><code><a title="meshtastic.StreamInterface" href="#meshtastic.StreamInterface">StreamInterface</a></code></h4>
|
||||
<ul class="">
|
||||
<li><code><a title="meshtastic.StreamInterface.close" href="#meshtastic.StreamInterface.close">close</a></code></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</main>
|
||||
<footer id="footer">
|
||||
<p>Generated by <a href="https://pdoc3.github.io/pdoc"><cite>pdoc</cite> 0.8.1</a>.</p>
|
||||
</footer>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
|
||||
<script>hljs.initHighlightingOnLoad()</script>
|
||||
</body>
|
||||
</html>
|
||||
1941
doc/meshtastic/mesh_pb2.html
Normal file
1941
doc/meshtastic/mesh_pb2.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,262 @@
|
||||
"""
|
||||
## an API for Meshtastic devices
|
||||
|
||||
"""API for Meshtastic devices"""
|
||||
Primary class: StreamInterface
|
||||
|
||||
from .interface import StreamInterface
|
||||
properties of StreamInterface:
|
||||
|
||||
- radioConfig - Current radio configuration and device settings, if you write to this the new settings will be applied to
|
||||
the device.
|
||||
- nodes - The database of received nodes. Includes always up-to-date location and username information for each
|
||||
node in the mesh. This is a read-only datastructure.
|
||||
- myNodeInfo - You probably don't want this.
|
||||
|
||||
## Published PubSub topics
|
||||
|
||||
We use a publish-subscribe model to communicate asynchronous events [https://pypubsub.readthedocs.io/en/v4.0.3/ ]. Available
|
||||
topics:
|
||||
|
||||
- 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(packet) - delivers a received packet as a dictionary, if you only care about a particular
|
||||
type of packet, you should subscribe to the full topic name. If you want to see all packets, simply subscribe to "meshtastic.receive".
|
||||
- meshtastic.receive.user(packet)
|
||||
- meshtastic.receive.data(packet)
|
||||
- meshtastic.node.updated(node = NodeInfo) - published when a node in the DB changes (appears, location changed, username changed, etc...)
|
||||
|
||||
Example Usage:
|
||||
|
||||
```
|
||||
import meshtastic
|
||||
from pubsub import pub
|
||||
|
||||
def onReceive(packet):
|
||||
print(f"Received: {packet}")
|
||||
|
||||
interface = StreamInterface() # By default will try to find a meshtastic device, otherwise provide a device path like /dev/ttyUSB0
|
||||
pub.subscribe(onReceive, "meshtastic.receive")
|
||||
interface.sendData("hello world") # defaults to broadcast, specify a destination ID if you wish
|
||||
```
|
||||
|
||||
"""
|
||||
|
||||
import google.protobuf.json_format
|
||||
import serial
|
||||
import serial.tools.list_ports
|
||||
import threading
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
from . import mesh_pb2
|
||||
from pubsub import pub
|
||||
|
||||
START1 = 0x94
|
||||
START2 = 0xc3
|
||||
HEADER_LEN = 4
|
||||
MAX_TO_FROM_RADIO_SIZE = 512
|
||||
|
||||
BROADCAST_ADDR = "all" # A special ID that means broadcast
|
||||
|
||||
|
||||
MY_CONFIG_ID = 42
|
||||
|
||||
|
||||
class MeshInterface:
|
||||
"""Interface class for meshtastic devices
|
||||
"""
|
||||
|
||||
def __init__(self, debugOut=None):
|
||||
"""Constructor"""
|
||||
self.debugOut = debugOut
|
||||
self.nodes = None # FIXME
|
||||
self._startConfig()
|
||||
|
||||
def sendText(self, text, destinationId=BROADCAST_ADDR):
|
||||
"""Send a utf8 string to some other node, if the node has a display it will also be shown on the device.
|
||||
|
||||
Arguments:
|
||||
text {string} -- The text to send
|
||||
|
||||
Keyword Arguments:
|
||||
destinationId {nodeId} -- where to send this message (default: {BROADCAST_ADDR})
|
||||
"""
|
||||
self.sendData(text.encode("utf-8"), destinationId,
|
||||
dataType=mesh_pb2.Data.CLEAR_TEXT)
|
||||
|
||||
def sendData(self, byteData, destinationId=BROADCAST_ADDR, dataType=mesh_pb2.Data.OPAQUE):
|
||||
"""Send a data packet to some other node"""
|
||||
meshPacket = mesh_pb2.MeshPacket()
|
||||
meshPacket.payload.data.payload = byteData
|
||||
meshPacket.payload.data.typ = dataType
|
||||
self.sendPacket(meshPacket, destinationId)
|
||||
|
||||
def sendPacket(self, meshPacket, destinationId=BROADCAST_ADDR):
|
||||
"""Send a MeshPacket to the specified node (or if unspecified, broadcast).
|
||||
You probably don't want this - use sendData instead."""
|
||||
toRadio = mesh_pb2.ToRadio()
|
||||
# FIXME add support for non broadcast addresses
|
||||
meshPacket.to = 255
|
||||
toRadio.packet.CopyFrom(meshPacket)
|
||||
self._sendToRadio(toRadio)
|
||||
|
||||
def _disconnected(self):
|
||||
"""Called by subclasses to tell clients this interface has disconnected"""
|
||||
pub.sendMessage("meshtastic.connection.lost")
|
||||
|
||||
def _startConfig(self):
|
||||
"""Start device packets flowing"""
|
||||
self.myInfo = None
|
||||
self.nodes = {} # nodes keyed by ID
|
||||
self._nodesByNum = {} # nodes keyed by nodenum
|
||||
self.radioConfig = None
|
||||
|
||||
startConfig = mesh_pb2.ToRadio()
|
||||
startConfig.want_config_id = MY_CONFIG_ID # we don't use this value
|
||||
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)
|
||||
json = google.protobuf.json_format.MessageToJson(fromRadio)
|
||||
logging.debug(f"Received: {json}")
|
||||
if fromRadio.HasField("my_info"):
|
||||
self.myInfo = fromRadio.my_info
|
||||
elif fromRadio.HasField("radio"):
|
||||
self.radioConfig = fromRadio.radio
|
||||
elif fromRadio.HasField("node_info"):
|
||||
node = fromRadio.node_info
|
||||
self._nodesByNum[node.num] = node
|
||||
self.nodes[node.user.id] = node
|
||||
elif fromRadio.config_complete_id == MY_CONFIG_ID:
|
||||
# we ignore the config_complete_id, it is unneeded for our stream API fromRadio.config_complete_id
|
||||
pub.sendMessage("meshtastic.connection.established")
|
||||
elif fromRadio.HasField("packet"):
|
||||
self._handlePacketFromRadio(fromRadio.packet)
|
||||
else:
|
||||
logging.warn("Unexpected FromRadio payload")
|
||||
|
||||
def _handlePacketFromRadio(self, meshPacket):
|
||||
"""Handle a MeshPacket that just arrived from the radio
|
||||
|
||||
Will publish one of the following events:
|
||||
- meshtastic.receive.position(packet = MeshPacket dictionary)
|
||||
- meshtastic.receive.user(packet = MeshPacket dictionary)
|
||||
- meshtastic.receive.data(packet = MeshPacket dictionary)
|
||||
"""
|
||||
# FIXME, update node DB as needed
|
||||
json = google.protobuf.json_format.MessageToDict(meshPacket)
|
||||
if meshPacket.payload.HasField("position"):
|
||||
pub.sendMessage("meshtastic.receive.position", packet=json)
|
||||
if meshPacket.payload.HasField("user"):
|
||||
pub.sendMessage("meshtastic.receive.user",
|
||||
packet=json)
|
||||
if meshPacket.payload.HasField("data"):
|
||||
pub.sendMessage("meshtastic.receive.data",
|
||||
packet=json)
|
||||
|
||||
|
||||
class StreamInterface(MeshInterface):
|
||||
"""Interface class for meshtastic devices over a stream link (serial, TCP, etc)"""
|
||||
|
||||
def __init__(self, devPath=None, debugOut=None):
|
||||
"""Constructor, opens a connection to a specified serial port, or if unspecified try to
|
||||
find one Meshtastic device by probing
|
||||
|
||||
Keyword Arguments:
|
||||
devPath {string} -- A filepath to a device, i.e. /dev/ttyUSB0 (default: {None})
|
||||
debugOut {stream} -- If a stream is provided, any debug serial output from the device will be emitted to that stream. (default: {None})
|
||||
|
||||
Raises:
|
||||
Exception: [description]
|
||||
Exception: [description]
|
||||
"""
|
||||
|
||||
if devPath is None:
|
||||
ports = list(filter(lambda port: port.vid != None,
|
||||
serial.tools.list_ports.comports()))
|
||||
if len(ports) == 0:
|
||||
raise Exception("No Meshtastic devices detected")
|
||||
elif len(ports) > 1:
|
||||
raise Exception(
|
||||
f"Multiple ports detected, you must specify a device, such as {ports[0].device}")
|
||||
else:
|
||||
devPath = ports[0].device
|
||||
|
||||
logging.debug(f"Connecting to {devPath}")
|
||||
self._rxBuf = bytes() # empty
|
||||
self._wantExit = False
|
||||
self.stream = serial.Serial(
|
||||
devPath, 921600, exclusive=True, timeout=0.5)
|
||||
self._rxThread = threading.Thread(target=self.__reader, args=())
|
||||
self._rxThread.start()
|
||||
MeshInterface.__init__(self, debugOut=debugOut)
|
||||
|
||||
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)
|
||||
self.stream.flush()
|
||||
|
||||
def close(self):
|
||||
"""Close a connection to the device"""
|
||||
logging.debug("Closing serial stream")
|
||||
# pyserial cancel_read doesn't seem to work, therefore we ask the reader thread to close things for us
|
||||
self._wantExit = True
|
||||
|
||||
def __reader(self):
|
||||
"""The reader thread that reads bytes from our stream"""
|
||||
empty = bytes()
|
||||
|
||||
while not self._wantExit:
|
||||
b = self.stream.read(1)
|
||||
if len(b) > 0:
|
||||
# logging.debug(f"read returned {b}")
|
||||
c = b[0]
|
||||
ptr = len(self._rxBuf)
|
||||
|
||||
# Assume we want to append this byte, fixme use bytearray instead
|
||||
self._rxBuf = self._rxBuf + b
|
||||
|
||||
if ptr == 0: # looking for START1
|
||||
if c != START1:
|
||||
self._rxBuf = empty # failed to find start
|
||||
if self.debugOut != None:
|
||||
try:
|
||||
self.debugOut.write(b.decode("utf-8"))
|
||||
except:
|
||||
self.debugOut.write('?')
|
||||
|
||||
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 + 1 == packetlen + HEADER_LEN:
|
||||
try:
|
||||
self._handleFromRadio(self._rxBuf[HEADER_LEN:])
|
||||
except Exception as ex:
|
||||
logging.warn(
|
||||
f"Error handling FromRadio, possibly corrupted? {ex}")
|
||||
traceback.print_exc()
|
||||
self._rxBuf = empty
|
||||
logging.debug("reader is exiting")
|
||||
self.stream.close()
|
||||
self._disconnected()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!python3
|
||||
|
||||
import argparse
|
||||
from .interface import StreamInterface
|
||||
from . import StreamInterface
|
||||
import logging
|
||||
import sys
|
||||
from pubsub import pub
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
|
||||
import google.protobuf.json_format
|
||||
import serial
|
||||
import serial.tools.list_ports
|
||||
import threading
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
from . import mesh_pb2
|
||||
from pubsub import pub
|
||||
|
||||
START1 = 0x94
|
||||
START2 = 0xc3
|
||||
HEADER_LEN = 4
|
||||
MAX_TO_FROM_RADIO_SIZE = 512
|
||||
|
||||
BROADCAST_ADDR = "all" # A special ID that means broadcast
|
||||
|
||||
"""
|
||||
|
||||
properties:
|
||||
|
||||
- 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.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(packet = MeshPacket)
|
||||
- meshtastic.receive.user(packet = MeshPacket)
|
||||
- meshtastic.receive.data(packet = MeshPacket)
|
||||
- meshtastic.node.updated(node = NodeInfo) - published when a node in the DB changes (appears, location changed, username changed, etc...)
|
||||
- meshtastic.debug(message = string)
|
||||
- meshtastic.send(packet = MeshPacket) - Not yet implemented, instead call sendPacket(...) on MeshInterface
|
||||
|
||||
"""
|
||||
|
||||
MY_CONFIG_ID = 42
|
||||
|
||||
|
||||
class MeshInterface:
|
||||
"""Interface class for meshtastic devices"""
|
||||
|
||||
def __init__(self, debugOut=sys.stdout):
|
||||
"""Constructor"""
|
||||
self.debugOut = debugOut
|
||||
self.nodes = None # FIXME
|
||||
self._startConfig()
|
||||
|
||||
def sendText(self, text, destinationId=BROADCAST_ADDR):
|
||||
"""Send a utf8 string to some other node, if the node has a display it will also be shown on the device."""
|
||||
self.sendData(text.encode("utf-8"), destinationId,
|
||||
dataType=mesh_pb2.Data.CLEAR_TEXT)
|
||||
|
||||
def sendData(self, byteData, destinationId=BROADCAST_ADDR, dataType=mesh_pb2.Data.OPAQUE):
|
||||
"""Send a data packet to some other node"""
|
||||
meshPacket = mesh_pb2.MeshPacket()
|
||||
meshPacket.payload.data.payload = byteData
|
||||
meshPacket.payload.data.typ = dataType
|
||||
self.sendPacket(meshPacket, destinationId)
|
||||
|
||||
def sendPacket(self, meshPacket, destinationId=BROADCAST_ADDR):
|
||||
"""Send a MeshPacket to the specified node (or if unspecified, broadcast).
|
||||
You probably don't want this - use sendData instead."""
|
||||
toRadio = mesh_pb2.ToRadio()
|
||||
# FIXME add support for non broadcast addresses
|
||||
meshPacket.to = 255
|
||||
toRadio.packet.CopyFrom(meshPacket)
|
||||
self._sendToRadio(toRadio)
|
||||
|
||||
def _disconnected(self):
|
||||
"""Called by subclasses to tell clients this interface has disconnected"""
|
||||
pub.sendMessage("meshtastic.connection.lost")
|
||||
|
||||
def _startConfig(self):
|
||||
"""Start device packets flowing"""
|
||||
self.myInfo = None
|
||||
self.nodes = {} # nodes keyed by ID
|
||||
self._nodesByNum = {} # nodes keyed by nodenum
|
||||
self.radioConfig = None
|
||||
|
||||
startConfig = mesh_pb2.ToRadio()
|
||||
startConfig.want_config_id = MY_CONFIG_ID # we don't use this value
|
||||
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)
|
||||
json = google.protobuf.json_format.MessageToJson(fromRadio)
|
||||
logging.debug(f"Received: {json}")
|
||||
if fromRadio.HasField("my_info"):
|
||||
self.myInfo = fromRadio.my_info
|
||||
elif fromRadio.HasField("radio"):
|
||||
self.radioConfig = fromRadio.radio
|
||||
elif fromRadio.HasField("node_info"):
|
||||
node = fromRadio.node_info
|
||||
self._nodesByNum[node.num] = node
|
||||
self.nodes[node.user.id] = node
|
||||
elif fromRadio.config_complete_id == MY_CONFIG_ID:
|
||||
# we ignore the config_complete_id, it is unneeded for our stream API fromRadio.config_complete_id
|
||||
pub.sendMessage("meshtastic.connection.established")
|
||||
elif fromRadio.HasField("packet"):
|
||||
self._handlePacketFromRadio(fromRadio.packet)
|
||||
else:
|
||||
logging.warn("Unexpected FromRadio payload")
|
||||
|
||||
def _handlePacketFromRadio(self, meshPacket):
|
||||
"""Handle a MeshPacket that just arrived from the radio
|
||||
|
||||
Will publish one of the following events:
|
||||
- meshtastic.receive.position(packet = MeshPacket dictionary)
|
||||
- meshtastic.receive.user(packet = MeshPacket dictionary)
|
||||
- meshtastic.receive.data(packet = MeshPacket dictionary)
|
||||
"""
|
||||
# FIXME, update node DB as needed
|
||||
json = google.protobuf.json_format.MessageToDict(meshPacket)
|
||||
if meshPacket.payload.HasField("position"):
|
||||
pub.sendMessage("meshtastic.receive.position", packet=json)
|
||||
if meshPacket.payload.HasField("user"):
|
||||
pub.sendMessage("meshtastic.receive.user",
|
||||
packet=json)
|
||||
if meshPacket.payload.HasField("data"):
|
||||
pub.sendMessage("meshtastic.receive.data",
|
||||
packet=json)
|
||||
|
||||
|
||||
class StreamInterface(MeshInterface):
|
||||
"""Interface class for meshtastic devices over a stream link(serial, TCP, etc)"""
|
||||
|
||||
def __init__(self, devPath=None, debugOut=sys.stdout):
|
||||
"""Constructor, opens a connection to a specified serial port, or if unspecified try to find one Meshtastic device by probing"""
|
||||
|
||||
if devPath is None:
|
||||
ports = list(filter(lambda port: port.vid != None,
|
||||
serial.tools.list_ports.comports()))
|
||||
if len(ports) == 0:
|
||||
raise Exception("No Meshtastic devices detected")
|
||||
elif len(ports) > 1:
|
||||
raise Exception(
|
||||
f"Multiple ports detected, you must specify a device, such as {ports[0].device}")
|
||||
else:
|
||||
devPath = ports[0].device
|
||||
|
||||
logging.debug(f"Connecting to {devPath}")
|
||||
self._rxBuf = bytes() # empty
|
||||
self._wantExit = False
|
||||
self.stream = serial.Serial(
|
||||
devPath, 921600, exclusive=True, timeout=0.5)
|
||||
self._rxThread = threading.Thread(target=self.__reader, args=())
|
||||
self._rxThread.start()
|
||||
MeshInterface.__init__(self, debugOut=debugOut)
|
||||
|
||||
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)
|
||||
self.stream.flush()
|
||||
|
||||
def close(self):
|
||||
"""Close a connection to the device"""
|
||||
logging.debug("Closing serial stream")
|
||||
# pyserial cancel_read doesn't seem to work, therefore we ask the reader thread to close things for us
|
||||
self._wantExit = True
|
||||
|
||||
def __reader(self):
|
||||
"""The reader thread that reads bytes from our stream"""
|
||||
empty = bytes()
|
||||
|
||||
while not self._wantExit:
|
||||
b = self.stream.read(1)
|
||||
if len(b) > 0:
|
||||
# logging.debug(f"read returned {b}")
|
||||
c = b[0]
|
||||
ptr = len(self._rxBuf)
|
||||
|
||||
# Assume we want to append this byte, fixme use bytearray instead
|
||||
self._rxBuf = self._rxBuf + b
|
||||
|
||||
if ptr == 0: # looking for START1
|
||||
if c != START1:
|
||||
self._rxBuf = empty # failed to find start
|
||||
if self.debugOut != None:
|
||||
try:
|
||||
self.debugOut.write(b.decode("utf-8"))
|
||||
except:
|
||||
self.debugOut.write('?')
|
||||
|
||||
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 + 1 == packetlen + HEADER_LEN:
|
||||
try:
|
||||
self._handleFromRadio(self._rxBuf[HEADER_LEN:])
|
||||
except Exception as ex:
|
||||
logging.warn(
|
||||
f"Error handling FromRadio, possibly corrupted? {ex}")
|
||||
traceback.print_exc()
|
||||
self._rxBuf = empty
|
||||
logging.debug("reader is exiting")
|
||||
self.stream.close()
|
||||
self._disconnected()
|
||||
Reference in New Issue
Block a user