From 9eaf3ce1c48bd9416e8a94680cb973232de1a76e Mon Sep 17 00:00:00 2001 From: pdxlocations Date: Fri, 31 Jul 2026 23:16:45 -0700 Subject: [PATCH] init --- LICENSE | 5 + README.md | 101 ++++++ manifest.py | 2 + micromesh/__init__.py | 20 ++ .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 1322 bytes .../__pycache__/interface.cpython-311.pyc | Bin 0 -> 9417 bytes .../__pycache__/mesh_pb2.cpython-311.pyc | Bin 0 -> 311 bytes .../__pycache__/messages.cpython-311.pyc | Bin 0 -> 14234 bytes .../__pycache__/portnums.cpython-311.pyc | Bin 0 -> 1888 bytes .../__pycache__/portnums_pb2.cpython-311.pyc | Bin 0 -> 321 bytes .../__pycache__/protobuf.cpython-311.pyc | Bin 0 -> 17572 bytes micromesh/__pycache__/stream.cpython-311.pyc | Bin 0 -> 4116 bytes micromesh/interface.py | 149 +++++++++ micromesh/mesh_pb2.py | 3 + micromesh/messages.py | 178 ++++++++++ micromesh/portnums.py | 49 +++ micromesh/portnums_pb2.py | 3 + micromesh/protobuf.py | 310 ++++++++++++++++++ micromesh/stream.py | 67 ++++ package.json | 16 + pyproject.toml | 21 ++ .../test_meshmicro.cpython-311.pyc | Bin 0 -> 11038 bytes .../test_micromesh.cpython-311.pyc | Bin 0 -> 11038 bytes tests/test_micromesh.py | 112 +++++++ 24 files changed, 1036 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 manifest.py create mode 100644 micromesh/__init__.py create mode 100644 micromesh/__pycache__/__init__.cpython-311.pyc create mode 100644 micromesh/__pycache__/interface.cpython-311.pyc create mode 100644 micromesh/__pycache__/mesh_pb2.cpython-311.pyc create mode 100644 micromesh/__pycache__/messages.cpython-311.pyc create mode 100644 micromesh/__pycache__/portnums.cpython-311.pyc create mode 100644 micromesh/__pycache__/portnums_pb2.cpython-311.pyc create mode 100644 micromesh/__pycache__/protobuf.cpython-311.pyc create mode 100644 micromesh/__pycache__/stream.cpython-311.pyc create mode 100644 micromesh/interface.py create mode 100644 micromesh/mesh_pb2.py create mode 100644 micromesh/messages.py create mode 100644 micromesh/portnums.py create mode 100644 micromesh/portnums_pb2.py create mode 100644 micromesh/protobuf.py create mode 100644 micromesh/stream.py create mode 100644 package.json create mode 100644 pyproject.toml create mode 100644 tests/__pycache__/test_meshmicro.cpython-311.pyc create mode 100644 tests/__pycache__/test_micromesh.cpython-311.pyc create mode 100644 tests/test_micromesh.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..288e96b --- /dev/null +++ b/LICENSE @@ -0,0 +1,5 @@ +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +This project is licensed under the GNU General Public License version 3 or any later +version. The full license text is available at https://www.gnu.org/licenses/gpl-3.0.txt. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8b2977a --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +# MicroMesh + +MicroMesh is a small, dependency-free Meshtastic client for MicroPython. It talks to a +Meshtastic device over its serial API and implements the protobuf wire format directly, +so `google.protobuf`, threads, and `pyserial` are not required. + +The initial release supports: + +- serial `ToRadio` / `FromRadio` framing and resynchronization; +- the configuration handshake and a small in-memory node list; +- text, arbitrary data, and position packets; +- routing acknowledgements, node/user data, waypoints, and device metrics; +- familiar generated-protobuf methods: `SerializeToString`, `ParseFromString`, + `CopyFrom`, `HasField`, `ClearField`, and `WhichOneof`; +- preservation of unknown protobuf fields for forward compatibility. + +This is intentionally not a complete replacement for the desktop +[`meshtastic`](https://github.com/meshtastic/python) package. Configuration and admin +messages are currently returned as raw encoded bytes. + +## Install + +From a checkout of this repository, install with a current `mpremote`: + +```sh +mpremote mip install package.json +``` + +Alternatively, copy the `micromesh` directory to `/lib` on the board. After this +repository is published on GitHub it can also be installed as +`mpremote mip install github:ORG/micromesh`. + +It can also be installed on CPython for development: + +```sh +python -m pip install . +``` + +## Quick start + +```python +from machine import UART, Pin +from micromesh import PortNum, SerialInterface + +uart = UART(1, baudrate=115200, tx=Pin(17), rx=Pin(16), timeout=0) + +def received(packet): + if packet.WhichOneof("payload_variant") != "decoded": + return + if packet.decoded.portnum == PortNum.TEXT_MESSAGE_APP: + print("from !%08x:" % packet.from_, packet.decoded.payload.decode()) + +mesh = SerialInterface(uart, on_packet=received) +mesh.connect() +mesh.sendText("hello mesh") + +while True: + mesh.poll() +``` + +The UART baud rate and pins depend on the attached Meshtastic device. `poll()` is +non-blocking when the UART is configured with `timeout=0`. + +Direct messages accept either an integer or Meshtastic's hexadecimal node ID: + +```python +mesh.sendText("hello", destinationId="!a1b2c3d4", wantAck=True) +mesh.sendPosition(45.5152, -122.6784, altitude=15) +mesh.sendData(b"custom", portNum=PortNum.PRIVATE_APP) +``` + +## Protobufs + +Messages can be used without a radio: + +```python +from micromesh import Data, PortNum + +data = Data(portnum=PortNum.TEXT_MESSAGE_APP, payload=b"hello") +encoded = data.SerializeToString() +decoded = Data().ParseFromString(encoded) +``` + +Generated-module-style imports are available as `micromesh.mesh_pb2` and +`micromesh.portnums_pb2` for easier porting from the desktop library. + +Only the commonly useful schemas are modeled. Unknown fields survive a decode/encode +round trip, but their contents are not interpreted. A field named `from` is accessed as +`packet.from_` because `from` is a Python keyword. + +## Development + +```sh +python -m unittest discover -s tests +``` + +The API protocol is defined by the +[`meshtastic/protobufs`](https://github.com/meshtastic/protobufs) project. Update the +field declarations in `micromesh/messages.py` when adopting newer schemas. + +`manifest.py` is also included for freezing the package into custom MicroPython firmware. diff --git a/manifest.py b/manifest.py new file mode 100644 index 0000000..8ef9ee4 --- /dev/null +++ b/manifest.py @@ -0,0 +1,2 @@ +metadata(version="0.1.0", description="Small Meshtastic client for MicroPython") +package("micromesh") diff --git a/micromesh/__init__.py b/micromesh/__init__.py new file mode 100644 index 0000000..8c8c0c2 --- /dev/null +++ b/micromesh/__init__.py @@ -0,0 +1,20 @@ +"""MicroMesh: a dependency-free Meshtastic client for MicroPython.""" + +from .interface import BROADCAST_ADDR, DATA_PAYLOAD_LEN, SerialInterface, node_num +from .messages import ( + Constants, Data, DeviceMetrics, FromRadio, Heartbeat, LogRecord, MeshPacket, + MyNodeInfo, NodeInfo, Position, QueueStatus, RouteDiscovery, Routing, + ToRadio, User, Waypoint, +) +from .portnums import PortNum +from .protobuf import DecodeError, ProtoMessage, decode_varint, encode_varint + +__version__ = "0.1.0" + +__all__ = ( + "BROADCAST_ADDR", "Constants", "DATA_PAYLOAD_LEN", "Data", "DecodeError", "DeviceMetrics", + "FromRadio", "Heartbeat", "LogRecord", "MeshPacket", "MyNodeInfo", "NodeInfo", + "PortNum", "Position", "ProtoMessage", "QueueStatus", "RouteDiscovery", "Routing", + "SerialInterface", "ToRadio", "User", "Waypoint", "decode_varint", "encode_varint", + "node_num", +) diff --git a/micromesh/__pycache__/__init__.cpython-311.pyc b/micromesh/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d1612a5594a0f18e311a4dca1b46aeae52e5e1be GIT binary patch literal 1322 zcmdUtO>fgM7{{G9U6OX~y7zbJ0U=e0bzBhwm34?o+bG=-!WYX++$|8tmF-S+r+om# z5k3Oq+w3^`G>H?pNxSWYpA^{OGOjq*PtWrtvH#D@mvXs??E3JLzZH3ezJc>=<$f9$ zpRDm2MF^wFiE@d@FFhq5-#ykJj%#;OqX*jswm5kLOuBp;-SPPSwrMlJ?vX=eQ z9_Ry+u6%cFc6HZOo_1a|TWigoUD9l|I;Prc?lwug`DW9G$>#dD8QWnprpZRCne5V# z89x;fBWa)4t0q_zsnRsn%D646sVTSEVH~n8rez!|6FifGcW4v~6Fg^B>ODra2{y%k zhlN5$rU+8oG4?5&tyqQPkja7Z zz>m{?iODUuoZA+n{n2PQ*Q;xD% z7U}d#FFYVkQ=>c4T510c40GKaX0*nI6fC^v?Fbk*x<^kh? z3BV+v0GI+y17-jVfJMNF#df`BJVGK75@G_Ya@d-orc6$D27GFKKFmuE#b!GHL1L!v zFJXUbGJemN)6Rj)^06*daj)MUmLbdRw`@})#P&(qK3d6EJ53TozM=~&5nZ+$&@0v0 zV`&oi6dN?ICb&WNV|~!yYxJV`i3ll-f@*LWf1BCCHH(lqjWr=Fy}_mbh>NJ7u*dSQ zy#R(?-LpK;JC1XT)=$v-8S0$ew9j*{lRHQM7mCQab%K_!?^CpOa??JmOdprd-H}r6 L*n>yju|@j<5KUrD literal 0 HcmV?d00001 diff --git a/micromesh/__pycache__/interface.cpython-311.pyc b/micromesh/__pycache__/interface.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..68273237567d725954b47efad286e9bf9eb0838a GIT binary patch literal 9417 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 literal 0 HcmV?d00001 diff --git a/micromesh/__pycache__/messages.cpython-311.pyc b/micromesh/__pycache__/messages.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5aec2a86931e4a03b2edab9c346f4bc4f0988846 GIT binary patch literal 14234 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 PBsjsqMaijzh@bJlY28vd literal 0 HcmV?d00001 diff --git a/micromesh/__pycache__/portnums.cpython-311.pyc b/micromesh/__pycache__/portnums.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fc4ce5200005861a928dec830a64aeea92e01753 GIT binary patch literal 1888 zcmZXU&2JM&6u@`=5hw9SoY;;{4CEU~s$vNaTu{}CJ#m(JKeW3}$cT32WLDtHj$PZ4 zN+pC_MQTtva4J1=qOFknw*;w&)t)N#iHaW+PxY@Ebcl*Cz#Bv_nGumnl62{Or&WQt9a6q_PxmLeIJ zCetiKvTT~bQ1P}p?0>%Jv z)q^M?28aXZ0P}zaz#?D?unhPdfB$=JFEm5OVwczzlCm>a7*~mUs z@LFxqHH?bGVvJin79>1RUi1V7PMApJm9?=Y!jIdyf>msrBD4rG#6fXOQL7ciBk|8o z*d{7ssI0hvq=uF9#+q)PmmS$?6M`)(nwLFpj)@R7`ULijl^o12WOxPGF5*bHB4m-4Wmu? zro)0_(+jHT{%_Jd{>@77jY74{J4s>J*>BX_UFUJ(yzdm~!+P)0!ET|=zG<{X|LGQ* z^}SB3$(vIF;Li%slz30--fTbazz^#Ke%kJz9R0J$ol5mL-E8hSly+zG$Dyp7O7|-Q z{!((Xueb|~$DvuF^ym32$Dy2?oZ?GD%w7Tqz9K!{SKUmuuetNw>Z&`P>+1rOss24T zH**}yh;@HwBH1sy=QhI8RGJ6n`|z!ONgQ;(lFINnGdX_FR0ei~G#>94;puFhrmU?* zya=VNwMVI6124$x9WU&+0WVVT9<?YPvMjsO>anzXGIB#soJb$zZ_xd7zOW@riOD1DMfTZ)=MPS#fPC9W zAG?v*4^O^-^4-(JrzesezU}50e!lbS&ae1w{?;G)TSt+j$g{EMV=tE9j^_S737;DJ GQT`7cNfGY= literal 0 HcmV?d00001 diff --git a/micromesh/__pycache__/portnums_pb2.cpython-311.pyc b/micromesh/__pycache__/portnums_pb2.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b60fbfdc6307deb4333766e0ae57a99e5883a7fb GIT binary patch literal 321 zcmY+8y-Gtd6vvZWp;hSO;v{6fLK|=vS3yt<)}>3h`Orp^n~>y!SMUjZ03Shon~riP z!O2bN*2$B83?2?A=i^8I|M%hW072Zm+Q%1ozw@w*V%sbap?O0R&5*>hFnEe&?-rGN z&zBdfR-sBtj;xc7QKOhzqlj?US4neibZUvV-h~v&y$huB-e>_gSChOYbz`{-ar}cp zq{JOQxh{*Y&zN+Av2KtdVB!Z_W;?4Xy^Fw8u4bTZzX|etQ_>Pc&Je$rn0W{Ljz_;tzK7 BV$J{n literal 0 HcmV?d00001 diff --git a/micromesh/__pycache__/protobuf.cpython-311.pyc b/micromesh/__pycache__/protobuf.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d7fd1e291e26e67a54a14badd6f7747327489eb7 GIT binary patch literal 17572 zcmch9eQX<7w%-iDM2VzGij+is)3zjwvTVh+itkY$HBu{?HYt^Fdy1*_F6z`Axkw0dH0mKpzut2fs8Yl{- z-U31XdHtO`J1x~Ph$O&aZ>la3j`x1X@7%La7A97kRP)N75K8V{5Isbe_=pw z{N-{xSc(HFl_e>af)gd3g5ew&aL0Z#8Vb+*XT_Q5%t8O;j1Zjk&qacQ|Khy=OlVS^ zITMUr-d9m^GU^wC)1iw&aUvR=p7)2Y%}xie1;f#Zemf1dkIv5qBUiglx z-RC1gG17f87@iKzMuPL*G)U2jNHjFrJ$@+^y*ziZdscX7dS-GW8kz}5y06hlVh}DD zz2!TzZ+2cRL$wTe@DgzXENcHUChOv$XwT=E%y$bBoC$wKAP@g1z-=yClosc{z}J^r z%mNn&0bWHuG-bxSA%<~T;G!CVaoiV3`;z4#u)rgi@hY{!iS2Pdtq~W+RNk3U8UutG%~pm=k=+WM$3puNd0%3`+t02{paK!ypPWZiv*~6 zz?3zH!qIH`#rbG(LKG+FvzCcjBCxEPq9ULM^V<{CbHS`}W-c0`8vTCJfuF1cGzcmg ze|thi6=EIYYVjBO0AP{Zv^$gLrBlnN7Kbur_QYEs%rDMotPaV&OSbMxm^UjtOM900 zES|_zIFqe6x)NO(tL>K+H!6}tsll74(}S|7L$(GKYe2FFP%gP+$(As0nK_$F&Aw&f z%+{Zu`ruSzPBv95rfSJly@^E6Qr~hP-WgR)7EcRJCL@Udj2G}CegX8pV0+@#8D0D;Z~1^3d|T z2~);e$!6I?)7_f#-rSRJlij;z>+Xa(T+yRc^hl;2l^|%G@uH0F^9gw6x%eQf7kI5IaoJ0lX= z<@tDF3+(f^2{B)RFtr?0q2RO-D9c)2dH&^-BcmeGQnqaP0Rl!W$!-4yKkvH!6y!7oYe^jWw`#q`-|^u^qH_^${in7-)KrSZPvD? z&#ku0wfmLY{Y&KK9Lpv5WyDt= z4K8lNm$ty-*ht5<1rfL4D=!T~4m`)*<43@FEaACp5S+56t0C+;WjSFeYZZgDLE>Cl z%k0GDRs33KW=UR{meJO~`b9s<1t4iPv^;0Qp3dc-C_zg-)a^_aJ3|1^~oR)0i+V+cI7se#$eo&G@lre17~m@@eW?l!Ju?y2(y+vGX?U{;GZ0OKSXyj!J5Zwt!JZR$6KDsx$BRet z6QRDV(ty~B*I0!*va}3OQ~GlNi`?&R&gG+$?~r7pXVc?LoX)6GM`csb}ZSh~<>pzT1LDFd`K4aWifh8wG7`i)bl66{#$ zE&-a%m|Lg>tYZ3$M{wcYE4TrxnN07=+J8)H41{q~Hb!)kGSL)`*TZia!o|B_x|q6= zss_lbe2CMlU_AF!obb=8Oi!greVjg`#J;}jTYuYT^ z+ZB7e#GWl_@9J+a|=FpAedYDaf^P;}vR$ zg?_;0s*>FUVv=|O37CV*akk};s~bQhYZ*Q{bb9cdNQ{H=cH}WmP9s4KM&_oY;#nkQ z&8(1q2B4K*oX^!0tJP10mT(cJBj*9YkKA6xy$jp3&7By?RM#ekGc7ys#x`2I*IT;f zmi?8H4yipsQz<9K?{k~7E-Z2o^+D5HZO!+K9s7B3&yx{ z(HSMynhT{lJ|IDqyQ1ynXwgypU91(6j&p^^fd#Sh61QL?8KF=*&J&fyO)RIjsG(T1 zkrX3wW4cJt%~gaSJtZ!fo}|NF(R!0+cB(=x>YKhUu$eB>=Hh1btUYeV_pSvCq!<1w z8$q$582>b}&21rW5jee?z;ny3%X^=y@1|Ve4T8C3R4iIbSxJf&xEh4=lGq9@zb(uQ zcCbUoh`0yXKKMrjRcInLUmiRJ~ zvQ`)hBG+Nm$r>*OquI*m!AV0G=c20VNPH1_RJlwO)3TPcBSUA0&t)w+wrrf8nGKXN zR;#XXi@L%^TC1um3aOA}#7NdW1G|~HU9T9}23?l{mIp!k;Wtq{}d((gUO>esHb9* zBu77*NgAJ$NrH(jHW$m@X^kC%#I!)xg1DhnG9kGnZYr=vjoIytI<=x?OK=sw^vpzr z`5HB7Y)vqQkpDCOZ(z`RA|uq;-=o>qU%&|Mc^uP4mV|s982jTGA(Y4~T3K}yVDg$- zWl-V;)S)scVz7Zq@pXzImLtAF;GYpVMc^d@#9gu#p-3nkiB5zkgISXhnvALyyh#}* z2rzDR5pTuphH)5ilF}K&5$G)lu#L;GrnYexO?+-kgf2M^}4g z`+mi~U$XDd*sCDHuw|KJ8K1}2+J5JicMmAL2N(z0ksMk%BY8XVq=(dB@RHgFm|9~i zF{QR+qqcj!wp%r0fQ^(NVj%GX+1;)u-cP)rX=()Q?5U$5lr1utpZN!oLARJPVj?Ab0P(Kb)~;Gq`)f+>j>cvHjMSShNo z$g{b-XDI9vIzzPe#fb=0JWHhTeDtqSU+&<5`V(D=OU_-Ab=P)$G(nkmL%pE<7)=O5 zn0|HQC`-fJP@p;SDj`Ar&D$d?PE5n%uHvm1S_=vPiVFDwV4ASnm+c!?-@4V8@}{D) zwNcgIs@5sXqvkEZ@mM;#9_2mm0nMy5tPt$<*$UT)Ah<$hkUy@AM?1PZ+9EG(kg^ z#_#rkT?fm1d(iVuy@gaq4O!=pC&Wl_7{^`bn0+SeJ{J^2u*%1Rqcdt01TZ#ej0F{w zi}l3J>^v1MA@Y3m-vY5n2LL0L4Qu1NwJ~*mqp4@TsYh-)q%<9pt%nusVJZKp<51iq z+ELhQ958X*&&%~ni@ui&e6iI!TD1R_+K~$iVH&k=5Bhoq{zL97gMLK~a!LM;d!%aK zC|@uvl@|-(Z}TF*>?)2~DlaHRuoM*SONsI+9MXJfeImA+67n>P$6$#fqMv(Gb|TGj zLt&?m)Z$Cqg(EI}(f-DQV9F^NW*h;L^1(01i*t)mvBRWh^H5I=h22xUA2*Btv3TFt zk0_KEQeyTll*KKN*`>KOodRzQSomdv<*PEi1f%aG6q|im7e%97s3zJ{cn8LsJR?}? zSS-4;kh=YJw6v9{X(QTJs4;HQ*YKn-oWwq+2c@;}pv2`D*m#a(JRei)$^FJi&_Ol} ze|f*bqyA6k>`nCJORO5&c3A+0)Gk;b*Q2;qKYo~XUvy8tEabcr;{;UAa=nR>I2m>O zVIu&sSfS|jw6Nu)Aa)btK>=R10r6QAGs_X5QtfR&So^0Zu3Z!+24c_XRK0DybYDMt z8B9k8@S>LZ3bjOtSQoi(1nJ!9x0k*RFRBV~fX^DQ2IqB#Q9^`H86NrpKH^{Ec!v8M z)&+hEmYTP@SGe0e2BTct@^d?3*0ApxQ9vQK`QtBi!hGXBhpqg~F55 zb2!P5PF#Ax2ERrm%D^!KJH{)LzE4v*+T63oiSWF-;ZIN;ow*dL`g9JHwFKXZk|#ms zc+OKGg4uwR0WoOx^oI5;kw`){+MlF1GD!p~)Lqa=5d}klH54A7AxVJ=^kkM0=EZlA zCu=nX=8lF=vv{A&xnx8z_e}_Dgy`a-03dAve6CG`z9JdCEum?-*2#w2(AYJ$L&h=WaY1 zmk(!3E2q<5#oJDjcYX0B{jT+X^gng}wo~bQX`}Du^}d(ozEP!b6j|i@^9o??d8PJz z;>C=o0b-7$DLwSr2z$BT`&*b15c5s&+bh!R!S#`#G!ldfL2B%gJoIF|jhW_Mcf(S3 zU-EqN{95l?2XqCecR9YrRX9fZv_<{hto7e~cjeurDbom1ig%yqRhS$E-nU=6^-|ij z;Xkof&s$a`mmemU-xF2__b9L; zNLbb0K6UHVCkv~=ucq%$$or0e-TM!H^4>FY%ZSo4B74s&-m{YTY%X6ay3ulAz2(4K z@BP+qhNPAQa?8s~%geI&oZ>wvvFAH8tKY(5o{o_TsF8uhiHzO(%TqT_B_m5`m(M2f zZ`0{bo?D77$0TbVTTR20#gdH$ghY$z;a4CgusXiL-bf;VpAj)bEC3i~i={7$WT9mL zHW$k)x|0vHo-6CW;^r7?BU=NUR@4@&(PA;Cx%S|nWYm|1&~Fk#^0ep+R zrVNk8tR$zt9J~?on3?mK70YQ?G#N~=piSWZCBvW4W&J7pGo`GLELD#Fv^}Psil|!~ z4A@fi_zE_1+<62Z_9xWncw&u}kEzj#8uvc2q>IT;Prw#4=gy6fL8yY^*As(WAA_|8 z)}dlKy`-1edc7m6$)q1+@q>9TI<@x*)DdPPrYL3^{=v{c#jL}45s-R}5(cP%q5tGG z(LH&Z8Of0R93?%kCjCStk(NXt^)6x2Q8ukjsg(CrEO zZR#J8A@T-h9tss0W(*EWFz6m#w>3((M)24TPv^R)Q}%Qzp032;rpN!eCq4RU-Dh>O zXOH69vnt4*1CkxqQD^eVx~)O7HIOCu^sUqBTG<;=ya5<~ZBEJ4F4^eW^wy=?Zk}E_ zjqQQeb^g=(&+66cSLbAKb9)cfb@;~d#PLl}-O95t1D#jflI(kmnt5cRDXsuYn^gizw9}tc;HZVj3M;sjM9NYxeZ=mt9_~Jn`0|u z>HRmy!Pd{;tN*J0zVP+s^??y-VC25xzCjv&MGm|wdoQ4OPUxkwbC>cM8;r(32SBaR zK`{;a@MwPdfA=(6w?<0j+6Cy3SVbCtg5+D`RrP58TEfxckEP?QC9RO9h~tt55xOIe zwu*EsW>41fLv{dbX6}V1TIYE;!-X>Fea3VVrui`>tE`^CLGHIM_UO0{IWd;#eQ1L# zXtIw!N&|i{4iz6s>RW=S{9fG3a4uh{h?hl)x8y>OXZ??-=&y|#rEIi|EZF1rCypiC z??h*`U=)B0xgINaq-luTi}7A?#O-LM;)*u<;thOhI+qS};(DAI!}w?x{IsU+=DNpz z;^pvu(9PdMB`C@n*Lm5+9eVs!dJtcP&R4|u3-!h;;*Pkp7`rdf>n~Z)DHj`W7h0)& z%&fWe?Li-Y3VhaHgbo=5PE`d=3U7o{Nl)G3pT^6IvCP>!djJ1W9lbQ>|*y{GRuz{cX#QyhuzUY(HL^^^0-@XeZky374KMUjzd-ol#QqsXr@?^5GB4( zfTk?wFF0YUKE8YD62moU+k&4NYXj9qM=VSVc%2HrLV)&U=Ax;Zd-u{OYowaqm|91? zNA<_+q&KECR^Yfq2u@AFoda57&UrY(w8E?-|IIv&RbBE7N(QRbt^Fgar5S*n)pOn+ z@KTOWh|#POnyac&Qd?Ow?p}!VOdnLW$1aLBLKhW(Mzu_^OrdZ%C^Gd?_3EKfQ6+?8 zs7ebFq{SL_(n6^%7daw#F~{_U=)< zdlKcDO7BOJRL{-%mHG8bzg+3hcL1noL7;ddHn3a7^9* z>8r^>D16=~IzmpjYl4Tt~fSat?RB<95A)5SysE0 zw!^aPh~hdTxsH$$Ti>E-w;#>jSU@E$U8|#Ohvb$%rKRtt9lkPCaFk3%Z(d6paTNTC zCpCJf?rz;J97k`~w@K|U%JnCe`je9PWTvJ*btt_n)u+_7BX3-C*deP^b< zbEEy}di&A)ZC`u74t{g~+cCNQg3^BBjxA+M1v9?pUpqc_qy^cxNAc~Ee0y-Epuw-N z<2wsi-Gqa!4PWQFuXD9`ZI|pjr1%ac%}Mj`ak$=lw{IQi_@1`Srk&}Y)tWW0YG4OW zP#({0cstg;9jo5e;M(x&v=Vq$_Vz2@e#zVaU!aZqTMLTx*EQdGW&eocAGzs3>IA#>n@tzi?VCK;@ZFAIX_DOd$^N0&Ry9p^H|&CtgSQ90T=VEN`BH5P z8eEY^tkX@4@zNSA)lE^JJeW;Ix{8=UZn*1_w6AI~v|QoF$}T75kD-`qK+9iu{v_T( zOsrnFvUQBejGjl%A`{er;xksz>g{8-Ia(u&cVBK?I?V#*AtauJf_QNGU5;eE` zZuO-u$)0uv(motRoK=Ymbt;M!GLANEHWeR$i|D5UCVx}0>u^q3dQ~T^k{hH>xL_N< z1wLJ7=6vkHalC;EiS&zZ*;KXA67EJ0a*L=DOms{jkm27DtH8hjAId3O)mZk8>mdu4 za5agVIXs*?k+?*>V%}5m4kXpR*l9vjQY}zY%OHdr<5m*sbmClzlB!)w(8MwrFG`Hl z5a%!w`Py_sT{&~3+ziiTMbD&pn(Q50+i*%AG|hC1wjrVhuSspBQoQHk+x zeKZbnt5ERua$^;A8%9(cLs=744^;29BrfiY3d@dl=5!$+3%_6N^V z{7(tcnTyy8P<-2nIhpUh9ulILRZ&89DqEtwObYoCz1;wSnShK3%uEn7^oQ{GmiR9y zW|>lRwt#ku*hzr3NivcufiM~4U*a=snG2IKAa6Kki(Qi#RO>Qh4e6x9caV0LjJsN@3rKEyGPQLu#p0gR^Xb-A_op3q&cYBt z7TMjfA*6niZoTuG>}^jDeUCz^np@4u=1m{8>N~c(HdIu&ALaFZ^1`jN$+L;K5^t@# z$%5fd4lW)0tI>sQVIC8O-^QoLQOKT*8hi6Iy=9PZ?Wbw`urXxe;y7O8r#0~@^EIn~1 zhh4G?kbQ3X6}p9n12o+kmIW!KzC08FZgZDlvcO?5UwmMUi(s@WbD^YRFE;<8)O@Z| zAKWnff^Qm@_rYbyhCJAGFXKlX2FR90XU54P3yVEYk#u5H>Wb-Zk_<-9Q)zD()M|y?A)g~_mLcUiO=|&NMOC=yo>9! zN%N+=AvN@QkL>PH+;HyXSq|3uH-qS$?3+@2Q(yrN{={&yXX!L<+GuxZbYwyF$=1dg zUi6+Gr8cdCd-|_%mk~PjQ;@^241(bkxOL+O#t(eQ4}1Uw!L&y!nqwm@p1@nt1tPj+ zj!R#-h*9F;l8@d`quGH%P_PPKu=}J2de%(8vHv^!zpecDm23DXIgd-$@hV;-4d~9I_Aldb;%Y1d)pd&_TPBkkH$F6AhfY+ud_=dA z+#mi_ji_og_iS+*=y#fTZE+gd$(!M~rvVFZZlli}u$atVY9$Bky!i;F=77a#uHWJ` z(Bq}5^q?z1Wi{{$Z#H{C&v{T;M@2Mn$V%6#G~ngU&(SgCBcVEOG-Q+b(*g!fy{crv z3;7Y`Tm)67gQIL%7wDEB{m%kL#HBiTQcb-?MMQ)o&euq$njDp=&h0K-7aA94CP8yd zs<55BMmJg6e}s@No5QU-96PWZaOzz+B1d*LE?e;m-7;bs)yt_QG^#QSGmdoAC!+wu zqJ%Wy(xCpY3IpPAkV>7590A0g9-iMan0OOr3BYdTn=&Tc1y%ojpDC-t`0?bio&y HUv2!~g5ymO literal 0 HcmV?d00001 diff --git a/micromesh/__pycache__/stream.cpython-311.pyc b/micromesh/__pycache__/stream.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d911648ea0922d868c52d5c2c3bbc6d9dce7ceb GIT binary patch literal 4116 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 DATA_PAYLOAD_LEN: + raise ValueError("payload exceeds %d bytes" % DATA_PAYLOAD_LEN) + if not portNum: + raise ValueError("portNum must be non-zero") + packet = MeshPacket(to=node_num(destinationId), channel=channelIndex, + id=_packet_id(), want_ack=wantAck, priority=priority) + packet.decoded.portnum = portNum + packet.decoded.payload = data + packet.decoded.want_response = wantResponse + if hopLimit is not None: + packet.hop_limit = hopLimit + if replyId is not None: + packet.decoded.reply_id = replyId + self._send(ToRadio(packet=packet)) + return packet + + def sendText(self, text, destinationId=BROADCAST_ADDR, wantAck=False, + wantResponse=False, channelIndex=0, hopLimit=None): + return self.sendData(text.encode("utf-8"), destinationId, + PortNum.TEXT_MESSAGE_APP, wantAck, wantResponse, + channelIndex, hopLimit) + + def sendPosition(self, latitude, longitude, altitude=0, + destinationId=BROADCAST_ADDR, wantAck=False, + channelIndex=0, hopLimit=None): + position = Position(latitude_i=int(latitude * 10000000), + longitude_i=int(longitude * 10000000)) + if altitude: + position.altitude = int(altitude) + return self.sendData(position, destinationId, PortNum.POSITION_APP, + wantAck, False, channelIndex, hopLimit) + + def heartbeat(self): + from .messages import Heartbeat + return self._send(ToRadio(heartbeat=Heartbeat())) + + def disconnect(self): + return self._send(ToRadio(disconnect=True)) + + def poll(self): + """Read all currently available bytes and return decoded messages.""" + available = getattr(self.uart, "any", None) + if available: + count = available() + data = self.uart.read(count) if count else b"" + else: + data = self.uart.read() + messages = [] + for payload in self.parser.feed(data or b""): + message = FromRadio().ParseFromString(payload) + messages.append(message) + self._handle(message) + return messages + + def _handle(self, message): + variant = message.WhichOneof("payload_variant") + if variant == "my_info": + self.my_info = message.my_info + elif variant == "node_info": + self.nodes[message.node_info.num] = message.node_info + elif variant == "config_complete_id": + self.config_complete = message.config_complete_id == self.config_id + if self.on_receive: + self.on_receive(message) + if variant == "packet" and self.on_packet: + self.on_packet(message.packet) diff --git a/micromesh/mesh_pb2.py b/micromesh/mesh_pb2.py new file mode 100644 index 0000000..984e01d --- /dev/null +++ b/micromesh/mesh_pb2.py @@ -0,0 +1,3 @@ +"""Generated-module-style compatibility imports for core mesh messages.""" + +from .messages import * # noqa: F401,F403 diff --git a/micromesh/messages.py b/micromesh/messages.py new file mode 100644 index 0000000..cc78897 --- /dev/null +++ b/micromesh/messages.py @@ -0,0 +1,178 @@ +"""Small generated-style subset of Meshtastic protobuf messages.""" + +from .protobuf import Field, ProtoMessage, fields + + +class Constants: + DATA_PAYLOAD_LEN = 233 + + +class Position(ProtoMessage): + FIELDS = fields( + latitude_i=Field(1, "sfixed32", optional=True), + longitude_i=Field(2, "sfixed32", optional=True), + altitude=Field(3, "int", optional=True), time=Field(4, "fixed32"), + location_source=Field(5), altitude_source=Field(6), + timestamp=Field(7, "fixed32"), timestamp_millis_adjust=Field(8, "int"), + altitude_hae=Field(9, "sint", optional=True), + altitude_geoidal_separation=Field(10, "sint", optional=True), + PDOP=Field(11), HDOP=Field(12), VDOP=Field(13), gps_accuracy=Field(14), + ground_speed=Field(15, optional=True), ground_track=Field(16, optional=True), fix_quality=Field(17), + fix_type=Field(18), sats_in_view=Field(19), sensor_id=Field(20), + next_update=Field(21), seq_number=Field(22), precision_bits=Field(23), + ) + + +class User(ProtoMessage): + FIELDS = fields( + id=Field(1, "string"), long_name=Field(2, "string"), short_name=Field(3, "string"), + macaddr=Field(4, "bytes"), hw_model=Field(5), is_licensed=Field(6, "bool"), + role=Field(7), public_key=Field(8, "bytes"), + is_unmessagable=Field(9, "bool", optional=True), + ) + + +class RouteDiscovery(ProtoMessage): + FIELDS = fields( + route=Field(1, "fixed32", repeated=True, packed=True), + snr_towards=Field(2, "int", repeated=True, packed=True), + route_back=Field(3, "fixed32", repeated=True, packed=True), + snr_back=Field(4, "int", repeated=True, packed=True), + ) + + +class Routing(ProtoMessage): + class Error: + NONE = 0 + NO_ROUTE = 1 + GOT_NAK = 2 + TIMEOUT = 3 + NO_INTERFACE = 4 + MAX_RETRANSMIT = 5 + NO_CHANNEL = 6 + TOO_LARGE = 7 + NO_RESPONSE = 8 + DUTY_CYCLE_LIMIT = 9 + BAD_REQUEST = 32 + NOT_AUTHORIZED = 33 + + FIELDS = fields( + route_request=Field(1, "message", RouteDiscovery), + route_reply=Field(2, "message", RouteDiscovery), error_reason=Field(3), + ) + ONEOFS = {"variant": ("route_request", "route_reply", "error_reason")} + + +class Data(ProtoMessage): + FIELDS = fields( + portnum=Field(1), payload=Field(2, "bytes"), want_response=Field(3, "bool"), + dest=Field(4, "fixed32"), source=Field(5, "fixed32"), request_id=Field(6, "fixed32"), + reply_id=Field(7, "fixed32"), emoji=Field(8, "fixed32"), bitfield=Field(9, optional=True), + ) + + +class MeshPacket(ProtoMessage): + class Priority: + UNSET = 0 + MIN = 1 + BACKGROUND = 10 + DEFAULT = 64 + RELIABLE = 70 + RESPONSE = 80 + HIGH = 100 + ALERT = 110 + ACK = 120 + MAX = 127 + + FIELDS = fields( + **{ + "from": Field(1, "fixed32"), "to": Field(2, "fixed32"), + "channel": Field(3), "decoded": Field(4, "message", Data), + "encrypted": Field(5, "bytes"), "id": Field(6, "fixed32"), + "rx_time": Field(7, "fixed32", optional=True), "rx_snr": Field(8, "float"), + "hop_limit": Field(9), "want_ack": Field(10, "bool"), + "priority": Field(11), "rx_rssi": Field(12, "int", optional=True), "delayed": Field(13), + "via_mqtt": Field(14, "bool"), "hop_start": Field(15), + "public_key": Field(16, "bytes"), "pki_encrypted": Field(17, "bool"), + "next_hop": Field(18), "relay_node": Field(19), "tx_after": Field(20), + "transport_mechanism": Field(21), + } + ) + ONEOFS = {"payload_variant": ("decoded", "encrypted")} + + +class DeviceMetrics(ProtoMessage): + FIELDS = fields( + battery_level=Field(1), voltage=Field(2, "float"), channel_utilization=Field(3, "float"), + air_util_tx=Field(4, "float"), uptime_seconds=Field(5), + ) + + +class NodeInfo(ProtoMessage): + FIELDS = fields( + num=Field(1), user=Field(2, "message", User), position=Field(3, "message", Position), + snr=Field(4, "float"), last_heard=Field(5, "fixed32"), + device_metrics=Field(6, "message", DeviceMetrics), channel=Field(7), + via_mqtt=Field(8, "bool"), hops_away=Field(9, optional=True), is_favorite=Field(10, "bool"), + is_ignored=Field(11, "bool"), is_key_manually_verified=Field(12, "bool"), + is_muted=Field(13, "bool"), + ) + + +class MyNodeInfo(ProtoMessage): + FIELDS = fields( + my_node_num=Field(1), reboot_count=Field(8), min_app_version=Field(11), + device_id=Field(12, "bytes"), pio_env=Field(13, "string"), + firmware_edition=Field(14), nodedb_count=Field(15), + ) + + +class LogRecord(ProtoMessage): + FIELDS = fields( + message=Field(1, "string"), time=Field(2, "fixed32"), + source=Field(3, "string"), level=Field(4), + ) + + +class QueueStatus(ProtoMessage): + FIELDS = fields(res=Field(1, "int"), free=Field(2), maxlen=Field(3), mesh_packet_id=Field(4)) + + +class Waypoint(ProtoMessage): + FIELDS = fields( + id=Field(1), latitude_i=Field(2, "sfixed32", optional=True), + longitude_i=Field(3, "sfixed32", optional=True), + expire=Field(4), locked_to=Field(5), name=Field(6, "string"), + description=Field(7, "string"), icon=Field(8, "fixed32"), geofence_radius=Field(9), + notify_on_enter=Field(11, "bool"), notify_on_exit=Field(12, "bool"), + notify_favorites_only=Field(13, "bool"), + ) + + +class Heartbeat(ProtoMessage): + FIELDS = {} + + +class FromRadio(ProtoMessage): + # Large configuration messages are exposed as encoded bytes to keep the runtime small. + FIELDS = fields( + id=Field(1), packet=Field(2, "message", MeshPacket), + my_info=Field(3, "message", MyNodeInfo), node_info=Field(4, "message", NodeInfo), + config=Field(5, "bytes"), log_record=Field(6, "message", LogRecord), + config_complete_id=Field(7), rebooted=Field(8, "bool"), moduleConfig=Field(9, "bytes"), + channel=Field(10, "bytes"), queueStatus=Field(11, "message", QueueStatus), + xmodemPacket=Field(12, "bytes"), metadata=Field(13, "bytes"), + mqttClientProxyMessage=Field(14, "bytes"), fileInfo=Field(15, "bytes"), + clientNotification=Field(16, "bytes"), deviceuiConfig=Field(17, "bytes"), + lockdown_status=Field(18, "bytes"), region_presets=Field(19, "bytes"), + ) + ONEOFS = {"payload_variant": tuple(name for name in FIELDS if name != "id")} + + +class ToRadio(ProtoMessage): + FIELDS = fields( + packet=Field(1, "message", MeshPacket), want_config_id=Field(3), + disconnect=Field(4, "bool"), xmodemPacket=Field(5, "bytes"), + mqttClientProxyMessage=Field(6, "bytes"), heartbeat=Field(7, "message", Heartbeat), + ) + ONEOFS = {"payload_variant": tuple(FIELDS)} diff --git a/micromesh/portnums.py b/micromesh/portnums.py new file mode 100644 index 0000000..c0e3438 --- /dev/null +++ b/micromesh/portnums.py @@ -0,0 +1,49 @@ +"""Meshtastic application port numbers.""" + + +class PortNum: + UNKNOWN_APP = 0 + TEXT_MESSAGE_APP = 1 + REMOTE_HARDWARE_APP = 2 + POSITION_APP = 3 + NODEINFO_APP = 4 + ROUTING_APP = 5 + ADMIN_APP = 6 + TEXT_MESSAGE_COMPRESSED_APP = 7 + WAYPOINT_APP = 8 + AUDIO_APP = 9 + DETECTION_SENSOR_APP = 10 + ALERT_APP = 11 + KEY_VERIFICATION_APP = 12 + REMOTE_SHELL_APP = 13 + REPLY_APP = 32 + IP_TUNNEL_APP = 33 + PAXCOUNTER_APP = 34 + STORE_FORWARD_PLUSPLUS_APP = 35 + NODE_STATUS_APP = 36 + MESH_BEACON_APP = 37 + SERIAL_APP = 64 + STORE_FORWARD_APP = 65 + RANGE_TEST_APP = 66 + TELEMETRY_APP = 67 + ZPS_APP = 68 + SIMULATOR_APP = 69 + TRACEROUTE_APP = 70 + NEIGHBORINFO_APP = 71 + ATAK_PLUGIN = 72 + MAP_REPORT_APP = 73 + POWERSTRESS_APP = 74 + LORAWAN_BRIDGE = 75 + RETICULUM_TUNNEL_APP = 76 + CAYENNE_APP = 77 + ATAK_PLUGIN_V2 = 78 + LORA_OTA_APP = 79 + GROUPALARM_APP = 112 + PRIVATE_APP = 256 + ATAK_FORWARDER = 257 + MAX = 511 + + +for _name in dir(PortNum): + if _name.isupper(): + globals()[_name] = getattr(PortNum, _name) diff --git a/micromesh/portnums_pb2.py b/micromesh/portnums_pb2.py new file mode 100644 index 0000000..6770c6a --- /dev/null +++ b/micromesh/portnums_pb2.py @@ -0,0 +1,3 @@ +"""Generated-module-style compatibility imports for application port numbers.""" + +from .portnums import * # noqa: F401,F403 diff --git a/micromesh/protobuf.py b/micromesh/protobuf.py new file mode 100644 index 0000000..3f1b295 --- /dev/null +++ b/micromesh/protobuf.py @@ -0,0 +1,310 @@ +"""Tiny proto3 codec used by MicroMesh. + +It deliberately implements only wire types used by the public messages below. +There are no descriptors and no dependency on ``google.protobuf``. +""" + +try: + import struct +except ImportError: # pragma: no cover - all supported ports normally have it + import ustruct as struct + + +VARINT = 0 +FIXED64 = 1 +BYTES = 2 +FIXED32 = 5 + + +class DecodeError(ValueError): + pass + + +def encode_varint(value): + value = int(value) + if value < 0: + value &= 0xFFFFFFFFFFFFFFFF + out = bytearray() + while value > 0x7F: + out.append((value & 0x7F) | 0x80) + value >>= 7 + out.append(value) + return bytes(out) + + +def decode_varint(buf, offset=0): + value = 0 + shift = 0 + length = len(buf) + while offset < length and shift < 70: + byte = buf[offset] + offset += 1 + value |= (byte & 0x7F) << shift + if not byte & 0x80: + return value, offset + shift += 7 + raise DecodeError("truncated or invalid varint") + + +def _skip(buf, offset, wire): + if wire == VARINT: + _, offset = decode_varint(buf, offset) + return offset + if wire == FIXED64: + offset += 8 + elif wire == BYTES: + size, offset = decode_varint(buf, offset) + offset += size + elif wire == FIXED32: + offset += 4 + else: + raise DecodeError("unsupported protobuf wire type: %d" % wire) + if offset > len(buf): + raise DecodeError("truncated protobuf field") + return offset + + +class Field: + def __init__(self, number, kind="uint", message=None, repeated=False, packed=False, optional=False): + self.number = number + self.kind = kind + self.message = message + self.repeated = repeated + self.packed = packed + self.optional = optional + + @property + def wire(self): + if self.kind in ("bytes", "string", "message"): + return BYTES + if self.kind in ("fixed32", "sfixed32", "float"): + return FIXED32 + if self.kind in ("fixed64", "sfixed64", "double"): + return FIXED64 + return VARINT + + +class ProtoMessage: + FIELDS = {} + ONEOFS = {} + _field_numbers = None + + def __init__(self, **kwargs): + object.__setattr__(self, "_values", {}) + object.__setattr__(self, "_unknown", []) + for name, value in kwargs.items(): + setattr(self, name, value) + + @classmethod + def _by_number(cls): + result = cls._field_numbers + if result is None: + result = {} + for name, field in cls.FIELDS.items(): + result[field.number] = (name, field) + cls._field_numbers = result + return result + + def __getattr__(self, name): + fields = type(self).FIELDS + if name.endswith("_") and name[:-1] in fields: + name = name[:-1] + field = fields.get(name) + if field is None: + raise AttributeError(name) + if name in self._values: + return self._values[name] + if field.repeated: + value = [] + self._values[name] = value + return value + if field.kind == "message": + value = field.message() + for members in type(self).ONEOFS.values(): + if name in members: + for other in members: + if other != name: + self._values.pop(other, None) + self._values[name] = value + return value + if field.kind == "bytes": + return b"" + if field.kind == "string": + return "" + if field.kind in ("float", "double"): + return 0.0 + return False if field.kind == "bool" else 0 + + def __setattr__(self, name, value): + fields = type(self).FIELDS + if name.endswith("_") and name[:-1] in fields: + name = name[:-1] + field = fields.get(name) + if field is None: + object.__setattr__(self, name, value) + return + if field.kind == "message" and isinstance(value, dict): + value = field.message(**value) + for members in type(self).ONEOFS.values(): + if name in members: + for other in members: + if other != name: + self._values.pop(other, None) + self._values[name] = value + + def HasField(self, name): + return name in self._values + + def ClearField(self, name): + self._values.pop(name, None) + + def WhichOneof(self, name): + for field_name in self.ONEOFS.get(name, ()): + if field_name in self._values: + return field_name + return None + + def CopyFrom(self, other): + self.ParseFromString(other.SerializeToString()) + + def SerializeToString(self): + out = bytearray() + for name, field in sorted(type(self).FIELDS.items(), key=lambda item: item[1].number): + if name not in self._values: + continue + value = self._values[name] + in_oneof = any(name in members for members in type(self).ONEOFS.values()) + if not in_oneof and not field.optional and not field.repeated and field.kind != "message": + if value in (0, False, b"", "", 0.0): + continue + values = value if field.repeated else (value,) + if field.packed and values: + body = bytearray() + for item in values: + body.extend(self._encode_scalar(field, item, include_tag=False)) + out.extend(encode_varint((field.number << 3) | BYTES)) + out.extend(encode_varint(len(body))) + out.extend(body) + else: + for item in values: + out.extend(encode_varint((field.number << 3) | field.wire)) + out.extend(self._encode_scalar(field, item, include_tag=False)) + for raw in self._unknown: + out.extend(raw) + return bytes(out) + + def _encode_scalar(self, field, value, include_tag=False): + kind = field.kind + if kind == "message": + data = value.SerializeToString() + return encode_varint(len(data)) + data + if kind == "string": + value = value.encode("utf-8") + if kind in ("bytes", "string"): + value = bytes(value) + return encode_varint(len(value)) + value + if kind == "float": + return struct.pack("> (63 if kind == "sint64" else 31)) + return encode_varint(value) + + def ParseFromString(self, data): + object.__setattr__(self, "_values", {}) + object.__setattr__(self, "_unknown", []) + data = bytes(data) + offset = 0 + by_number = type(self)._by_number() + while offset < len(data): + start = offset + tag, offset = decode_varint(data, offset) + number, wire = tag >> 3, tag & 7 + entry = by_number.get(number) + if entry is None: + offset = _skip(data, offset, wire) + self._unknown.append(data[start:offset]) + continue + name, field = entry + if wire == BYTES: + size, offset = decode_varint(data, offset) + end = offset + size + if end > len(data): + raise DecodeError("truncated length-delimited field") + raw = data[offset:end] + offset = end + if field.packed and field.wire != BYTES: + values = self._values.setdefault(name, []) + inner = 0 + while inner < len(raw): + item, inner = self._decode_scalar(field, raw, inner, field.wire) + values.append(item) + continue + value = self._decode_bytes(field, raw) + else: + if wire != field.wire: + offset = _skip(data, offset, wire) + self._unknown.append(data[start:offset]) + continue + value, offset = self._decode_scalar(field, data, offset, wire) + if field.repeated: + self._values.setdefault(name, []).append(value) + else: + setattr(self, name, value) + return self + + def _decode_bytes(self, field, raw): + if field.kind == "message": + return field.message().ParseFromString(raw) + if field.kind == "string": + return raw.decode("utf-8") + return raw + + def _decode_scalar(self, field, data, offset, wire): + kind = field.kind + if wire == VARINT: + value, offset = decode_varint(data, offset) + if kind in ("sint", "sint64"): + value = (value >> 1) ^ -(value & 1) + elif kind in ("int", "int64"): + bits = 64 + if value & (1 << (bits - 1)): + value -= 1 << bits + elif kind == "bool": + value = bool(value) + return value, offset + size = 4 if wire == FIXED32 else 8 + if offset + size > len(data): + raise DecodeError("truncated fixed-width field") + raw = data[offset:offset + size] + if kind == "float": + value = struct.unpack(" MAX_FRAME: + raise ValueError("Meshtastic frame exceeds %d bytes" % MAX_FRAME) + return bytes((START1, START2, size >> 8, size & 0xFF)) + payload + + +class StreamParser: + def __init__(self, max_frame=MAX_FRAME, on_log=None): + self.max_frame = max_frame + self.on_log = on_log + self._buffer = bytearray() + self._log = bytearray() + + def feed(self, data): + packets = [] + for byte in data or b"": + if not self._buffer: + if byte == START1: + self._buffer.append(byte) + else: + self._log_byte(byte) + continue + if len(self._buffer) == 1: + if byte == START2: + self._buffer.append(byte) + elif byte == START1: + self._buffer[0] = byte + else: + self._buffer = bytearray() + self._log_byte(byte) + continue + self._buffer.append(byte) + if len(self._buffer) == HEADER_LEN: + size = (self._buffer[2] << 8) | self._buffer[3] + if size > self.max_frame: + self._buffer = bytearray() + elif size == 0: + packets.append(b"") + self._buffer = bytearray() + elif len(self._buffer) > HEADER_LEN: + size = (self._buffer[2] << 8) | self._buffer[3] + if len(self._buffer) == HEADER_LEN + size: + packets.append(bytes(self._buffer[HEADER_LEN:])) + self._buffer = bytearray() + return packets + + def _log_byte(self, byte): + if byte in (10, 13): + if byte == 10 and self._log and self.on_log: + try: + line = bytes(self._log).decode("utf-8") + except UnicodeError: + line = bytes(self._log).decode("utf-8", "replace") + self.on_log(line) + self._log = bytearray() + elif len(self._log) < 256: + self._log.append(byte) diff --git a/package.json b/package.json new file mode 100644 index 0000000..331f97c --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "micromesh", + "version": "0.1.0", + "description": "Small Meshtastic client and protobuf codec for MicroPython", + "license": "GPL-3.0-or-later", + "urls": [ + ["micromesh/__init__.py", "micromesh/__init__.py"], + ["micromesh/interface.py", "micromesh/interface.py"], + ["micromesh/mesh_pb2.py", "micromesh/mesh_pb2.py"], + ["micromesh/messages.py", "micromesh/messages.py"], + ["micromesh/portnums.py", "micromesh/portnums.py"], + ["micromesh/portnums_pb2.py", "micromesh/portnums_pb2.py"], + ["micromesh/protobuf.py", "micromesh/protobuf.py"], + ["micromesh/stream.py", "micromesh/stream.py"] + ] +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..aa9a4ac --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + +[project] +name = "micromesh" +version = "0.1.0" +description = "A small, dependency-free Meshtastic client for MicroPython" +readme = "README.md" +requires-python = ">=3.8" +license = {text = "GPL-3.0-or-later"} +authors = [{name = "MicroMesh contributors"}] +keywords = ["meshtastic", "micropython", "lora", "protobuf"] +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: Implementation :: MicroPython", + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", +] + +[tool.setuptools] +packages = ["micromesh"] diff --git a/tests/__pycache__/test_meshmicro.cpython-311.pyc b/tests/__pycache__/test_meshmicro.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aca38b2182e6459e07baa1b5ccb39f02b33dea6d GIT binary patch literal 11038 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 WkN)xL>!(-9-=>pt?SuzpEB_z+95#sn literal 0 HcmV?d00001 diff --git a/tests/test_micromesh.py b/tests/test_micromesh.py new file mode 100644 index 0000000..6a51a9a --- /dev/null +++ b/tests/test_micromesh.py @@ -0,0 +1,112 @@ +import unittest + +from micromesh import Data, FromRadio, MeshPacket, PortNum, Position, SerialInterface, ToRadio +from micromesh.protobuf import DecodeError, decode_varint, encode_varint +from micromesh.stream import StreamParser, frame + + +class FakeUART: + def __init__(self, incoming=b""): + self.incoming = bytearray(incoming) + self.written = bytearray() + + def any(self): + return len(self.incoming) + + def read(self, count=None): + count = len(self.incoming) if count is None else count + result = bytes(self.incoming[:count]) + del self.incoming[:count] + return result + + def write(self, data): + self.written.extend(data) + return len(data) + + +class MicroMeshTests(unittest.TestCase): + def test_varint_round_trip(self): + for value in (0, 1, 127, 128, 16384, 0xFFFFFFFF, 0xFFFFFFFFFFFFFFFF): + encoded = encode_varint(value) + self.assertEqual(decode_varint(encoded), (value, len(encoded))) + + def test_known_data_wire_bytes(self): + message = Data(portnum=PortNum.TEXT_MESSAGE_APP, payload=b"hello", want_response=True) + self.assertEqual(message.SerializeToString(), b"\x08\x01\x12\x05hello\x18\x01") + self.assertEqual(Data().ParseFromString(message.SerializeToString()).to_dict(), message.to_dict()) + + def test_generated_style_imports(self): + from micromesh import mesh_pb2, portnums_pb2 + self.assertEqual(mesh_pb2.Constants.DATA_PAYLOAD_LEN, 233) + self.assertEqual(portnums_pb2.PortNum.TEXT_MESSAGE_APP, 1) + + def test_proto3_scalar_defaults_are_omitted(self): + self.assertEqual(Data(portnum=0, payload=b"", want_response=False).SerializeToString(), b"") + self.assertEqual(ToRadio(disconnect=False).SerializeToString(), b"\x20\x00") + self.assertEqual(Position(latitude_i=0).SerializeToString(), b"\x0d\x00\x00\x00\x00") + + def test_mesh_packet_fixed_fields_and_nested_message(self): + packet = MeshPacket(**{"from": 0x12345678, "to": 0xFFFFFFFF, "id": 42}) + packet.decoded.portnum = PortNum.TEXT_MESSAGE_APP + packet.decoded.payload = b"hi" + decoded = MeshPacket().ParseFromString(packet.SerializeToString()) + self.assertEqual(decoded.from_, 0x12345678) + self.assertEqual(decoded.to, 0xFFFFFFFF) + self.assertEqual(decoded.id, 42) + self.assertEqual(decoded.decoded.payload, b"hi") + + def test_signed_position(self): + original = Position(latitude_i=-1220000000, longitude_i=455000000, altitude=-12) + decoded = Position().ParseFromString(original.SerializeToString()) + self.assertEqual(decoded.latitude_i, -1220000000) + self.assertEqual(decoded.altitude, -12) + + def test_unknown_fields_are_preserved(self): + raw = b"\x08\x01" + encode_varint(99 << 3) + b"\x07" + parsed = Data().ParseFromString(raw) + self.assertEqual(parsed.portnum, 1) + self.assertEqual(parsed.SerializeToString(), raw) + + def test_stream_parser_handles_chunks_logs_and_resync(self): + logs = [] + parser = StreamParser(on_log=logs.append) + wrapped = b"debug line\n" + frame(b"one") + frame(b"two") + packets = [] + for chunk in (wrapped[:3], wrapped[3:14], wrapped[14:19], wrapped[19:]): + packets.extend(parser.feed(chunk)) + self.assertEqual(packets, [b"one", b"two"]) + self.assertEqual(logs, ["debug line"]) + + def test_send_text_builds_to_radio_frame(self): + import micromesh.interface as interface_module + old_packet_id = interface_module._packet_id + interface_module._packet_id = lambda: 0x1234 + try: + uart = FakeUART() + interface = SerialInterface(uart) + packet = interface.sendText("hello", destinationId="!aabbccdd", wantAck=True) + finally: + interface_module._packet_id = old_packet_id + sent = ToRadio().ParseFromString(StreamParser().feed(uart.written)[0]) + self.assertEqual(sent.packet.to, 0xAABBCCDD) + self.assertEqual(sent.packet.id, 0x1234) + self.assertIs(sent.packet.want_ack, True) + self.assertEqual(sent.packet.decoded.portnum, PortNum.TEXT_MESSAGE_APP) + self.assertEqual(sent.packet.decoded.payload, b"hello") + self.assertEqual(packet.id, 0x1234) + + def test_poll_decodes_and_tracks_state(self): + config_id = 123 + uart = FakeUART(frame(FromRadio(config_complete_id=config_id).SerializeToString())) + interface = SerialInterface(uart) + interface.config_id = config_id + self.assertEqual(len(interface.poll()), 1) + self.assertIs(interface.config_complete, True) + + def test_truncated_message_raises(self): + with self.assertRaises(DecodeError): + Data().ParseFromString(b"\x12\x05no") + + +if __name__ == "__main__": + unittest.main()