├── .screens ├── heltec.png ├── htpdeck.png ├── solder_pads_lol.png ├── tbeam.png ├── tdeck.png └── wisblock.png ├── LICENSE ├── README.md ├── assets ├── icon.xbm └── schematic.pdf ├── docs ├── FIRMWARE.md ├── HARDWARE.md ├── MQTT.md ├── SETUP.md └── T-DECK.md ├── meshapi.py ├── meshirc.py └── meshmqtt.py /.screens/heltec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidvegas/meshtastic/54cd7543eb7f3c2d9e403d49de3ad0c130753678/.screens/heltec.png -------------------------------------------------------------------------------- /.screens/htpdeck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidvegas/meshtastic/54cd7543eb7f3c2d9e403d49de3ad0c130753678/.screens/htpdeck.png -------------------------------------------------------------------------------- /.screens/solder_pads_lol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidvegas/meshtastic/54cd7543eb7f3c2d9e403d49de3ad0c130753678/.screens/solder_pads_lol.png -------------------------------------------------------------------------------- /.screens/tbeam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidvegas/meshtastic/54cd7543eb7f3c2d9e403d49de3ad0c130753678/.screens/tbeam.png -------------------------------------------------------------------------------- /.screens/tdeck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidvegas/meshtastic/54cd7543eb7f3c2d9e403d49de3ad0c130753678/.screens/tdeck.png -------------------------------------------------------------------------------- /.screens/wisblock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidvegas/meshtastic/54cd7543eb7f3c2d9e403d49de3ad0c130753678/.screens/wisblock.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2024, acidvegas 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Meshtastic Utilities 2 | 3 | This repository serves as a collection of resources created in my journey to learn & utilize [LoRa](https://en.wikipedia.org/wiki/LoRa) based communications with [Meshtastic](https://meshtastic.org). 4 | 5 | The goal here is to create simple & clean modules to interface with the hardware in a way that can be used to expand the possibilities of the devices capabilities. 6 | 7 | ![](.screens/htpdeck.png) 8 | 9 | ## Documentation 10 | - [Hardware Options](./docs/HARDWARE.md) 11 | - [Setup Hardware](./Sdocs/ETUP.md) 12 | - [Setup a T-Deck](./docs/T-DECK.md) 13 | - [Firmware Hacks & Customization](./docs/FIRMWARE.md) 14 | - [MQTT Notes](./docs/MQTT.md) 15 | 16 | ## Code 17 | - [Meshtastic Serial/TCP Interface](./meshapi.py) 18 | - [Meshtastic MQTT Interface](./meshmqtt.py) 19 | - [Meshtastic IRC Relay / Bridge](./meshirc.py) 20 | 21 | ## Bugs & Issues 22 | - Devices must have Wifi turned off when going mobile. Upon leaving my house with WiFi still enabled, the UI & connection was EXTREMELY laggy & poor. Couldn't even type well... 23 | - Devices using a MQTT with TLS will reboot loop. 24 | - A fix for the reboot loop is simply disabling MQTT over serial with `meshtastic --set mqtt.tls_enabled false` 25 | - Enabling JSON with MQTT causes messages to not be encrypted in the MQTT server.. 26 | 27 | ## Roadmap 28 | - Asyncronous meshtastic interface 29 | - Documentation on MQTT bridging for high availability 30 | - Bridge for IRC to allow channel messages to relay over Meshtastic & all Meshtastic events to relay into IRC. *(IRC to Meshtastic will require a command like `!mesh ` to avoid overloading the traffic over LoRa)* 31 | 32 | ## Notes 33 | - [Meshtastic PortNum List](https://buf.build/meshtastic/protobufs/docs/main:meshtastic#meshtastic.PortNum) 34 | ___ 35 | 36 | ###### Mirrors for this repository: [acid.vegas](https://git.acid.vegas/meshtastic) • [SuperNETs](https://git.supernets.org/acidvegas/meshtastic) • [GitHub](https://github.com/acidvegas/meshtastic) • [GitLab](https://gitlab.com/acidvegas/meshtastic) • [Codeberg](https://codeberg.org/acidvegas/meshtastic) -------------------------------------------------------------------------------- /assets/icon.xbm: -------------------------------------------------------------------------------- 1 | #define icon_width 175 2 | #define icon_height 175 3 | static uint8_t icon_bits[] = { 4 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 5 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 6 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 7 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 8 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 9 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 10 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 11 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 12 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 13 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 14 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 15 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 16 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 17 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 18 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 19 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 20 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 21 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 22 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 23 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 24 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 25 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 26 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 27 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 28 | 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 29 | 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 30 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 31 | 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 32 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 33 | 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 34 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 35 | 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 36 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 0x00, 37 | 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 38 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0x00, 0x00, 0x00, 0xC0, 39 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 40 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 41 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 42 | 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 43 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 44 | 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 45 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 46 | 0x01, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 47 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 48 | 0x00, 0xFC, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 49 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xFE, 50 | 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 51 | 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x07, 0x00, 0x00, 0xFE, 0xFF, 0x01, 52 | 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 53 | 0x00, 0x00, 0xF0, 0xFF, 0x0F, 0x00, 0x00, 0xFF, 0x7F, 0x00, 0x00, 0x00, 54 | 0x00, 0xFE, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 55 | 0xE0, 0xFF, 0x1F, 0x00, 0x80, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x80, 0xFF, 56 | 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 57 | 0x1F, 0x00, 0x80, 0xFF, 0x1F, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 58 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x1F, 0x00, 59 | 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 60 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0xC0, 0xFF, 61 | 0x07, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0x1F, 0x00, 0x00, 0x00, 62 | 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0x00, 0xC0, 0xFF, 0x07, 0x00, 63 | 0x00, 0x00, 0xFE, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 64 | 0x00, 0x00, 0x00, 0xFC, 0x7F, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0x00, 0x00, 65 | 0xFF, 0x7F, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 66 | 0x00, 0xFC, 0x7F, 0x00, 0xE0, 0xFF, 0x01, 0x00, 0x00, 0x00, 0xFF, 0x0F, 67 | 0x00, 0xE0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 68 | 0x7F, 0x00, 0xE0, 0xFF, 0x01, 0x00, 0x00, 0x00, 0xFF, 0x03, 0x00, 0x80, 69 | 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 70 | 0xE0, 0xFF, 0x01, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFE, 0x1F, 71 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0xE0, 0xFF, 72 | 0x00, 0x00, 0x00, 0x80, 0x7F, 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0x00, 73 | 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0xE0, 0xFF, 0x00, 0x00, 74 | 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x01, 0x00, 0x00, 0x00, 75 | 0x00, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x80, 76 | 0x1F, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 77 | 0x00, 0xF0, 0x7F, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0x80, 0x1F, 0x00, 78 | 0x00, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 79 | 0x7F, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 80 | 0x00, 0xFC, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x7F, 0x00, 81 | 0xE0, 0xFF, 0x00, 0x00, 0x00, 0xC0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xF0, 82 | 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0xE0, 0xFF, 83 | 0x00, 0x00, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x00, 84 | 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0xE0, 0xFF, 0x00, 0x00, 85 | 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x03, 0x00, 0x00, 86 | 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0xE0, 87 | 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0x00, 0x00, 88 | 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 89 | 0x00, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 90 | 0x7F, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0x00, 91 | 0x00, 0x00, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 92 | 0xE0, 0xFF, 0x00, 0x80, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 93 | 0xE0, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 94 | 0x00, 0xE0, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 95 | 0x01, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 0x00, 0xF0, 96 | 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x03, 0x00, 97 | 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 0x00, 0xFC, 0xFF, 0xFF, 98 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0x0F, 0xFC, 0x07, 0x00, 0x00, 0x00, 99 | 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 0x00, 0xFE, 0xFF, 0xFF, 0x00, 0x00, 100 | 0x00, 0x00, 0x00, 0x80, 0x7F, 0xF8, 0x1F, 0x00, 0x00, 0x00, 0x00, 0xF0, 101 | 0x7F, 0x00, 0xE0, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 102 | 0x00, 0x00, 0xFF, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x7F, 0x00, 103 | 0xE0, 0xFF, 0x80, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 104 | 0xFE, 0xE1, 0x7F, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0xE0, 0xFF, 105 | 0xC0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x87, 106 | 0xFF, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0xE0, 0xFF, 0xE0, 0xFF, 107 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x07, 0xFF, 0x03, 108 | 0x00, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0xE0, 0xFF, 0xE0, 0x7F, 0x00, 0x00, 109 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0xFE, 0x07, 0x00, 0x00, 110 | 0x00, 0xF0, 0x7F, 0x00, 0xE0, 0xFF, 0xF0, 0x3F, 0x00, 0x00, 0x00, 0x00, 111 | 0x00, 0x00, 0x00, 0x00, 0xC0, 0x1F, 0xFC, 0x0F, 0x00, 0x00, 0x00, 0xF0, 112 | 0x7F, 0x00, 0xE0, 0xFF, 0xF0, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 113 | 0x00, 0x00, 0x80, 0x3F, 0xF0, 0x1F, 0x00, 0x00, 0x00, 0xF0, 0x7F, 0x00, 114 | 0xE0, 0xFF, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 115 | 0x00, 0x7F, 0xE0, 0x3F, 0x00, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0xE0, 0xFF, 116 | 0xF8, 0x07, 0x00, 0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 117 | 0xC0, 0x7F, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 0xF8, 0x07, 118 | 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x80, 0xFF, 119 | 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 0xF8, 0x03, 0x00, 0x00, 120 | 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0xFF, 0x03, 0x00, 121 | 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 0xF8, 0x03, 0x00, 0x00, 0xF0, 0x00, 122 | 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0xFC, 0x07, 0x00, 0x00, 0xE0, 123 | 0x7F, 0x00, 0xE0, 0xFF, 0xF8, 0x03, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 124 | 0x00, 0xE0, 0x01, 0xF8, 0x01, 0xF8, 0x0F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 125 | 0xE0, 0xFF, 0xF8, 0x01, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x00, 0xF0, 126 | 0x0F, 0xF8, 0x01, 0xF0, 0x1F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 127 | 0xF8, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFD, 128 | 0x03, 0xE0, 0x3F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 0xF8, 0x01, 129 | 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0xC0, 130 | 0x7F, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 0xF8, 0x01, 0x00, 0x00, 131 | 0xE0, 0x03, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0x80, 0xFF, 0x00, 132 | 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 0xF8, 0x01, 0x00, 0x00, 0xC0, 0x03, 133 | 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x0F, 0x00, 0xFF, 0x01, 0x00, 0xF0, 134 | 0x7F, 0x00, 0xE0, 0xFF, 0xF8, 0x01, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 135 | 0x00, 0x80, 0xFF, 0xFF, 0x0F, 0x00, 0xFE, 0x03, 0x00, 0xF0, 0x7F, 0x00, 136 | 0xE0, 0xFF, 0xF8, 0x03, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x00, 0x00, 137 | 0xF8, 0xFF, 0x1F, 0x00, 0xFC, 0x0F, 0x00, 0xF0, 0x7F, 0x00, 0xE0, 0xFF, 138 | 0xF8, 0x03, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 139 | 0x3F, 0x00, 0xF0, 0x1F, 0x00, 0xF0, 0x7F, 0x00, 0xE0, 0xFF, 0xF0, 0x03, 140 | 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x7F, 0x00, 141 | 0xE0, 0x3F, 0x00, 0xF0, 0x7F, 0x00, 0xE0, 0xFF, 0xF0, 0x03, 0x00, 0x00, 142 | 0x80, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0xC0, 0x7F, 143 | 0x00, 0xF0, 0x7F, 0x00, 0xE0, 0xFF, 0xF0, 0x07, 0x00, 0x00, 0x80, 0x0F, 144 | 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x80, 0xFF, 0x00, 0xF0, 145 | 0x7F, 0x00, 0xE0, 0xFF, 0xF0, 0x07, 0x00, 0x00, 0x80, 0x0F, 0x00, 0x00, 146 | 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x03, 0x00, 0xFF, 0x01, 0xF0, 0x7F, 0x00, 147 | 0xE0, 0xFF, 0xF8, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 148 | 0x00, 0xF8, 0xFF, 0x07, 0x00, 0xFE, 0x03, 0xF0, 0x7F, 0x00, 0xE0, 0xFF, 149 | 0xFC, 0x07, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 150 | 0xFF, 0x0F, 0x00, 0xFC, 0x0F, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 0xFE, 0x07, 151 | 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0xF0, 0x0F, 152 | 0x00, 0xF8, 0x1F, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0x00, 0x00, 153 | 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0xF0, 154 | 0x3F, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x3E, 155 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x3F, 0x00, 0xE0, 0x7F, 0xE0, 156 | 0x7F, 0x00, 0xE0, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 157 | 0x00, 0x00, 0x00, 0x00, 0x80, 0x7F, 0x00, 0xC0, 0xFF, 0xE0, 0x7F, 0x00, 158 | 0xE0, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 159 | 0x00, 0x00, 0x00, 0xFF, 0x00, 0x80, 0xFF, 0xE1, 0x7F, 0x00, 0xE0, 0xFF, 160 | 0x7F, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 161 | 0x00, 0xFE, 0x01, 0x00, 0xFF, 0xE3, 0x7F, 0x00, 0xE0, 0xFF, 0x1F, 0x00, 162 | 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 163 | 0x01, 0x00, 0xFE, 0xE7, 0x7F, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0x00, 0x00, 164 | 0x00, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x03, 0x00, 165 | 0xFC, 0xFF, 0x7F, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0xF8, 166 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x07, 0x00, 0xF8, 0xFF, 167 | 0x7F, 0x00, 0xE0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x01, 0x00, 168 | 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0xF0, 0xFF, 0x7F, 0x00, 169 | 0xE0, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0xE0, 170 | 0x3F, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0xE0, 0xFF, 0x7F, 0x00, 0xE0, 0xFF, 171 | 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0xFF, 0x01, 172 | 0x00, 0xF8, 0x1F, 0x00, 0xE0, 0xFF, 0x7F, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 173 | 0x01, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xFE, 0xFF, 0x07, 0x00, 0xF8, 174 | 0x3F, 0x00, 0xC0, 0xFF, 0x7F, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0x01, 0x00, 175 | 0x00, 0xE0, 0x07, 0x00, 0x00, 0xFF, 0xFF, 0x3F, 0x00, 0xFC, 0x3F, 0x00, 176 | 0x80, 0xFF, 0x7F, 0x00, 0xE0, 0x3F, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xC0, 177 | 0x0F, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x03, 0xFE, 0x7F, 0x00, 0x00, 0xFF, 178 | 0x7F, 0x00, 0xE0, 0x3F, 0x00, 0xE0, 0x03, 0x00, 0x00, 0xC0, 0x3F, 0x00, 179 | 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0xFE, 0x7F, 0x00, 180 | 0xE0, 0x3F, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x80, 0x7F, 0x00, 0xC0, 0xFF, 181 | 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0xFC, 0x7F, 0x00, 0xE0, 0x3F, 182 | 0x00, 0xE0, 0x07, 0x00, 0x00, 0x80, 0xFF, 0x03, 0xE0, 0xFF, 0xFF, 0xFF, 183 | 0xFF, 0xFF, 0x1F, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0xE0, 0x3F, 0x00, 0xC0, 184 | 0x0F, 0x00, 0x00, 0x00, 0xFF, 0x7F, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 185 | 0x0F, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0xE0, 0x7F, 0x00, 0xC0, 0x0F, 0x00, 186 | 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 187 | 0x00, 0xF0, 0x7F, 0x00, 0xE0, 0x7F, 0x00, 0x80, 0x0F, 0x00, 0x00, 0x00, 188 | 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xF0, 189 | 0x7F, 0x00, 0xE0, 0x7F, 0x00, 0x80, 0x1F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 190 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0x7F, 0x00, 191 | 0xE0, 0x7F, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 192 | 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 193 | 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 194 | 0xFF, 0x3F, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 0x00, 0x00, 195 | 0x3E, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 196 | 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x7E, 0x00, 197 | 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 198 | 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 199 | 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0xE0, 200 | 0x7F, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x80, 0xFF, 201 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 202 | 0xE0, 0xFF, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 203 | 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 204 | 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 205 | 0x7F, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 0x00, 0x00, 206 | 0xF0, 0x01, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 207 | 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x00, 0xE0, 0xFF, 0x00, 0x00, 0xF0, 0x01, 208 | 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 209 | 0x00, 0xF0, 0x7F, 0x00, 0xE0, 0x7F, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 210 | 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xF0, 211 | 0x7F, 0x00, 0xE0, 0x3F, 0x00, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x80, 0xFF, 212 | 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x7F, 0x00, 213 | 0xE0, 0x1F, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 214 | 0xFF, 0x8F, 0x1F, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x7F, 0x00, 0xE0, 0x0F, 215 | 0x00, 0x00, 0xC0, 0x07, 0x00, 0x80, 0xC7, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 216 | 0x1E, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0xE0, 0x07, 0x00, 0x00, 217 | 0x80, 0x07, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0x00, 0x1C, 0x00, 218 | 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x00, 0xE0, 0x07, 0x00, 0x00, 0x80, 0x07, 219 | 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x18, 0x00, 0x00, 0x00, 220 | 0x00, 0xFC, 0x7F, 0x00, 0xE0, 0x07, 0xC0, 0x00, 0x00, 0x0F, 0x00, 0xF8, 221 | 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 222 | 0x7F, 0x00, 0xE0, 0x07, 0xE0, 0x01, 0x00, 0x0F, 0x00, 0xFC, 0xFF, 0xFF, 223 | 0xFF, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x7F, 0x00, 224 | 0xE0, 0x07, 0xE0, 0x03, 0x00, 0x1F, 0x00, 0xFE, 0xFF, 0xFF, 0xFF, 0x07, 225 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x7F, 0x00, 0xE0, 0x0F, 226 | 0xE0, 0x07, 0x00, 0x1E, 0x00, 0xFE, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 227 | 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x7F, 0x00, 0xE0, 0x0F, 0xC0, 0x07, 228 | 0x00, 0x3E, 0x00, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 229 | 0x00, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0xE0, 0x1F, 0xC0, 0x0F, 0x00, 0x7E, 230 | 0x80, 0xFF, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 231 | 0xC0, 0xFF, 0x7F, 0x00, 0xE0, 0x1F, 0x80, 0x0F, 0x00, 0xFC, 0xC0, 0xFF, 232 | 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0xFF, 233 | 0x7F, 0x00, 0xE0, 0x3F, 0x80, 0x1F, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 234 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x7F, 0x00, 235 | 0xE0, 0x3F, 0x80, 0x1F, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 236 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x7F, 0x00, 0xE0, 0x7F, 237 | 0x00, 0x1F, 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0x1F, 0x00, 0x00, 0x00, 0x00, 238 | 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x7F, 0x00, 0xE0, 0x7F, 0x00, 0x3F, 239 | 0x00, 0xF8, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 240 | 0x00, 0x00, 0xFC, 0xFF, 0x7F, 0x00, 0xE0, 0xFF, 0x00, 0x3E, 0x00, 0xF8, 241 | 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 242 | 0xFE, 0xFF, 0x7F, 0x00, 0xE0, 0xFF, 0x00, 0x7E, 0x00, 0xF0, 0xFF, 0xFF, 243 | 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 244 | 0x7F, 0x00, 0xE0, 0xFF, 0x00, 0x7C, 0x00, 0xF0, 0xFF, 0xFF, 0x7F, 0x00, 245 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x7F, 0x00, 246 | 0xE0, 0xFF, 0x01, 0xFC, 0x00, 0xE0, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 247 | 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0x7F, 0x00, 0xE0, 0xFF, 248 | 0x01, 0xF8, 0x00, 0x00, 0x00, 0xFF, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 249 | 0x00, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x7F, 0x00, 0xE0, 0xFF, 0x03, 0xF8, 250 | 0x01, 0x00, 0x00, 0xFE, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 251 | 0x00, 0xF0, 0xFF, 0xFF, 0x7F, 0x00, 0xE0, 0xFF, 0x03, 0xF8, 0x01, 0x00, 252 | 0x00, 0xFE, 0x07, 0x00, 0xF0, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0xF8, 253 | 0xFF, 0xFF, 0x7F, 0x00, 0xE0, 0xFF, 0x07, 0xF0, 0x03, 0x00, 0x00, 0xFE, 254 | 0x07, 0xE0, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 255 | 0x7F, 0x00, 0xE0, 0xFF, 0x07, 0xF0, 0x03, 0x00, 0x00, 0xFE, 0x03, 0xFE, 256 | 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0xFF, 0x7F, 0x00, 257 | 0xE0, 0xFF, 0x0F, 0xE0, 0x07, 0x00, 0x00, 0xFE, 0x83, 0xFF, 0xFF, 0xFF, 258 | 0xFF, 0x01, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0xE0, 0xFF, 259 | 0x0F, 0xC0, 0x0F, 0x00, 0x00, 0xFF, 0xC1, 0xFF, 0xFF, 0x3F, 0xFE, 0x01, 260 | 0x00, 0x00, 0x80, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0xE0, 0xFF, 0x0F, 0xC0, 261 | 0x0F, 0x00, 0x80, 0xFF, 0xE1, 0xFF, 0x07, 0x00, 0xFF, 0x01, 0x00, 0x00, 262 | 0xC0, 0xFF, 0xFF, 0xFF, 0x7F, 0x00, 0xE0, 0xFF, 0x1F, 0x80, 0x1F, 0x00, 263 | 0xC0, 0xFF, 0xE1, 0x1F, 0x00, 0xF8, 0xFF, 0x03, 0x00, 0x00, 0xE0, 0xFF, 264 | 0xFF, 0xFF, 0x7F, 0x00, 0xC0, 0xFF, 0x1F, 0x80, 0x3F, 0x00, 0xE0, 0xFF, 265 | 0xE0, 0x03, 0xFE, 0xFF, 0xFF, 0x1F, 0x00, 0x00, 0xFE, 0xFF, 0xFF, 0xFF, 266 | 0x7F, 0x00, 0xC0, 0xFF, 0x3F, 0x00, 0x7F, 0x00, 0xF0, 0xFF, 0xE0, 0xF8, 267 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 268 | 0xC0, 0xFF, 0x3F, 0x00, 0xFE, 0x00, 0xFC, 0xFF, 0x70, 0xFC, 0xFF, 0xFF, 269 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 270 | 0x7F, 0x00, 0xFE, 0xE7, 0xFF, 0xFF, 0x70, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 271 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 272 | 0xFC, 0xFF, 0xFF, 0xFF, 0x39, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 273 | 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0xF8, 0xFF, 274 | 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 275 | 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0xFE, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 276 | 0x3F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 277 | 0x0F, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0xE0, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 278 | 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 279 | 0x00, 0xFC, 0xFF, 0x01, 0xC0, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 280 | 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 0x00, 0xF8, 281 | 0xFF, 0x03, 0x80, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 282 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0xF8, 0xFF, 0x03, 283 | 0x00, 0xFE, 0xFF, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 284 | 0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0xF0, 0xFF, 0x07, 0x00, 0xF8, 285 | 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 286 | 0xFF, 0xFF, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x0F, 0x00, 0xE0, 0xFF, 0xFF, 287 | 0xFF, 0x01, 0x00, 0x00, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 288 | 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x1F, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0x07, 289 | 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 290 | 0x00, 0x00, 0xFF, 0x3F, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 291 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1F, 0x00, 0x00, 0x00, 0x00, 292 | 0xFE, 0x7F, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 293 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 294 | 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 295 | 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x03, 0xF0, 296 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 297 | 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 298 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 299 | 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 300 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x00, 301 | 0x00, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0x00, 302 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 303 | 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 304 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 305 | 0xFF, 0xFF, 0xFF, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 306 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFF, 307 | 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 308 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0xFF, 0x00, 309 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 310 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x07, 0x00, 0x00, 0x00, 311 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 312 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 313 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 314 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 315 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 316 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 317 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 318 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 319 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 320 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 321 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 322 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 323 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 324 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }; -------------------------------------------------------------------------------- /assets/schematic.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidvegas/meshtastic/54cd7543eb7f3c2d9e403d49de3ad0c130753678/assets/schematic.pdf -------------------------------------------------------------------------------- /docs/FIRMWARE.md: -------------------------------------------------------------------------------- 1 | # Meshtastic Firmware Hacks 2 | 3 | ## Prerequisite 4 | - Download & install [PlatformIO](https://platformio.org/platformio-ide) 5 | 6 | - `git clone https://github.com/meshtastic/firmware.git` 7 | 8 | - `cd firmware && git submodule update --init` 9 | 10 | ## Customization 11 | #### Custom Boot Logo 12 | - Use [XMB Viewer](https://windows87.github.io/xbm-viewer-converter/) to convert an image to XMB 13 | 14 | - The data from this goes in `firmware/src/graphics/img/icon.xbm` 15 | 16 | You can use the provided [icon.xbm](../assets/icon.xbm) for a rad GTA:SA fist to show up on boot. 17 | 18 | #### Custom boot message 19 | - Navigate to `firmware/src/graphics/Screen.cpp` 20 | 21 | - Find & replace `const char *title = "meshtastic.org";` with your custom message. 22 | 23 | #### Custom screen color 24 | - Navigate to `src/graphics/TFTDisplay.cpp` 25 | 26 | - Find & replace `#define TFT_MESH COLOR565(0xFF, 0x00, 0x00)` with your custom color. *(The one shown here is for RED mode >:))* 27 | 28 | ### Custom alert sound (for devices with a buzzer) 29 | - From the mobile app, click the 3 dots on the top right, and select `Radio configuration` 30 | - Under `Module configuration`, select `External Notification` 31 | - Scroll down & you will see a `Ringtone` option that takes [RTTTL](https://en.wikipedia.org/wiki/Ring_Tone_Text_Transfer_Language) formatted tones. 32 | 33 | As far as I know, at the time of writing this, the only way to change the Ringtone is from the App. While this is not a "firmware" related thing, I included it in this file because it was difficult to find this setting... 34 | 35 | #### Display RAM/PSRAM Usage 36 | Look for these lines: 37 | ```cpp 38 | display->drawString(x, y + FONT_HEIGHT_SMALL * 2, "SSID: " + String(wifiName)); 39 | display->drawString(x, y + FONT_HEIGHT_SMALL * 3, "http://meshtastic.local"); 40 | ``` 41 | 42 | And place the follow code AFTER the above lines: 43 | 44 | ```cpp 45 | // Display memory usage using the MemGet class 46 | uint32_t freeHeap = ESP.getFreeHeap(); 47 | uint32_t totalHeap = ESP.getHeapSize(); 48 | uint32_t usedHeap = totalHeap - freeHeap; 49 | display->drawString(x, y + FONT_HEIGHT_SMALL * 4, "Heap: " + String(usedHeap / 1024) + "/" + String(totalHeap / 1024) + " KB"); 50 | 51 | // Display PSRAM usage using the MemGet class 52 | uint32_t freePsram = ESP.getFreePsram(); 53 | uint32_t totalPsram = ESP.getPsramSize(); 54 | uint32_t usedPsram = totalPsram - freePsram; 55 | display->drawString(x, y + FONT_HEIGHT_SMALL * 5, "PSRAM: " + String(usedPsram / 1024) + "/" + String(totalPsram / 1024) + " KB"); 56 | ``` 57 | 58 | #### Heartbeat for redraw 59 | - Uncomment the line that says: `#define SHOW_REDRAWS` 60 | 61 | This will show a little 1x1 pixel on the top left corner anytime the screen is redraw. 62 | 63 | 64 | ## Compile & flash firmware 65 | - Select `PlatformIO: Pick Project Environment` & select your board. 66 | - Run `PLatformIO: Build` to compile the firmware. 67 | - Place device in DFU mode & plug in to the computer 68 | - Do `PlatformIO: Upload` to send the firmware to the device 69 | - Press the RESET button or reboot your device. 70 | 71 | See [here](https://meshtastic.org/docs/development/firmware/build/) for more information building & flashing custom firmware. -------------------------------------------------------------------------------- /docs/HARDWARE.md: -------------------------------------------------------------------------------- 1 | # Hardware 2 | > A simple overview of popular hardware options for Meshtastic usage 3 | 4 | ## LilyGo T-Deck 5 | ![](../.screens/tdeck.png) 6 | 7 | ###### Information 8 | This is the only FULL stand-alone device that I know of for Meshtastic, that once provisioned, you do not need a phone or computer to interface with it. The device comes with a touchscreen, full keyboard, built in speaker, and more. 9 | 10 | | Item | Cost | Description | 11 | | ---- | ---- | ----------- | 12 | | [T-Deck](https://www.lilygo.cc/products/t-deck) | $50 | The main LoRa microcontroller device | 13 | | [Case](https://www.printables.com/model/741124-lilygo-t-deck-case) | Free or $25 | You can 3D print it yourself, or follow the Etsy link and buy one | 14 | | [Antenna](https://www.amazon.com/Connector-868-915MHz-Lora32u4-Internet-WIshiOT/dp/B07LCKNN4H) | 14$ | External antenna for better range | 15 | | [GPS](https://www.amazon.com/dp/B09LQDG1HY) | 18$ | There may be a better 15mm option for the case above.. | 16 | | [Battery](https://www.amazon.com/dp/B0BG82T39Y) | Varies | The battery you get depends on the size of the case you order, contact me if you need help | 17 | 18 | ___ 19 | 20 | ## LilyGo T-Beam 21 | ![](../.screens/tbeam.png) 22 | 23 | ###### Information 24 | Very popular Meshtastic device. I personally use this for my home station 25 | 26 | | Item | Cost | Description | 27 | | ---- | ---- | ----------- | 28 | | [T-Beam](https://www.lilygo.cc/en-ca/products/t-beam-v1-1-esp32-lora-module?variant=43059202719925) | 32$ | The main LoRa board | 29 | | [Case](https://www.printables.com/model/127253-t-beam-case-for-meshtastic-v5) | Free | You can maybe find this case on Etsy or 3D print it yourself | 30 | 31 | ___ 32 | 33 | ## Heltec Lora 32 V3 34 | Great as a keychain for on the go meshtastic! Can power it directly from your phone if you want. 35 | 36 | ![](../.screens/heltec.png) 37 | 38 | - [Heltec Lora 32 V3](https://heltec.org/project/wifi-lora-32-v3/) 39 | 40 | ___ 41 | 42 | ## WisBlock 43 | These devices use an nrf52 chip instead of an esp32 chip. They are EXTREMELY low powered and are the best for solar nodes 44 | 45 | ![](../.screens/wisblock.png) 46 | 47 | - [WisBlock Starter Kit](https://store.rakwireless.com/products/wisblock-starter-kit?variant=41786685063366) 48 | **Note:** Make sure you get the nrf52 Arduino core and ensure you get the right frequency for your location. -------------------------------------------------------------------------------- /docs/MQTT.md: -------------------------------------------------------------------------------- 1 | # Meshtastic MQTT 2 | > Work in progress still, come back later... 3 | 4 | ###### default.conf 5 | ``` 6 | # Insecure 7 | listener 1883 8 | 9 | # TLS/SSL 10 | listener 8883 11 | acl_file /etc/mosquitto/conf.d/aclfile 12 | protocol mqtt 13 | require_certificate false 14 | certfile /etc/mosquitto/certs/cert.pem 15 | cafile /etc/mosquitto/certs/fullchain.pem 16 | keyfile /etc/mosquitto/certs/privkey.pem 17 | 18 | listener 8083 19 | protocol websockets 20 | certfile /etc/mosquitto/certs/cert.pem 21 | cafile /etc/mosquitto/certs/fullchain.pem 22 | keyfile /etc/mosquitto/certs/privkey.pem 23 | ``` 24 | 25 | ###### /etc/mosquitto/conf.d/aclfile 26 | ``` 27 | user acidvegas 28 | topic readwrite msh/# 29 | 30 | user mate 31 | topic readwrite msh/# 32 | 33 | pattern write $SYS/broker/connection/%c/state 34 | ``` 35 | 36 | ###### mosquito.conf 37 | ``` 38 | pid_file /run/mosquitto/mosquitto.pid 39 | 40 | per_listener_settings true 41 | allow_anonymous false 42 | persistence true 43 | persistence_location /var/lib/mosquitto 44 | password_file /etc/mosquitto/passwd 45 | 46 | log_dest file /var/log/mosquitto/mosquitto.log 47 | 48 | include_dir /etc/mosquitto/conf.d 49 | ``` -------------------------------------------------------------------------------- /docs/SETUP.md: -------------------------------------------------------------------------------- 1 | # Setup Hardware 2 | 3 | It is recommended that you provision your hardware using the serial interface over USB by using the [Meshtastic CLI Tool](https://pypi.org/project/meshtastic/). This is very specific because currently, at the time of writing this repository, changes made via the [web interface](https://client.meshtastic.org) do not work sometimes. When you "save" your settings, the device will reboot with the old settings still. I have had zero issues making changes over serial with the CLI interface. 4 | 5 | - `pip install meshtastic` to install the CLI tool 6 | - Plug in your device *(Make sure the USB cable you are using allows data transfer & not just power)* 7 | - Run the commands below *(Each command will make the device reboot after setting it)* 8 | 9 | ###### NAME 10 | ``` 11 | meshtastic --set-owner 'CHANGEME' --set-owner-short 'CHNG' 12 | ``` 13 | 14 | **Note:** Short name can only be 4 alphanumeric characters. 15 | 16 | ###### LORA 17 | ``` 18 | meshtastic --set lora.region US 19 | ``` 20 | 21 | ###### GPS 22 | ``` 23 | meshtastic --set position.gps_enabled true 24 | ``` 25 | 26 | or for a fixed position... 27 | 28 | ``` 29 | meshtastic --set position.fixed_position true --setlat 37.8651 --setlon -119.5383 30 | ``` 31 | 32 | ###### POWER 33 | ``` 34 | meshtastic --set power.is_power_saving false 35 | ``` 36 | 37 | ###### CHANNEL 38 | ``` 39 | meshtastic --ch-set name "SUPERNETS" --ch-set psk "CHANGEME" --ch-set uplink_enabled true --ch-set downlink_enabled true --ch-index 0 40 | ``` 41 | 42 | ###### WIFI 43 | ``` 44 | meshtastic --set network.wifi_enabled true --set network.wifi_ssid "CHANGEME" --set network.wifi_psk "CHANGEME" 45 | ``` 46 | 47 | ###### MQTT 48 | ``` 49 | meshtastic --set mqtt.enabled true --set mqtt.address changeme --set mqtt.username changeme --set mqtt.password changeme --set mqtt.encryption_enabled true --set mqtt.tls_enabled false --set mqtt.root msh --set mqtt.map_reporting_enabled true --set mqtt.json_enabled true 50 | ``` 51 | 52 | ###### BLUETOOTH 53 | ``` 54 | meshtastic --set bluetooth.enabled true 55 | ``` 56 | 57 | **Note:** Only enable this on devices are not using Wifi *(mobile devices)* because with ESP32 chips, I don't think Wifi & Bluetooth can function side-by-side together. 58 | -------------------------------------------------------------------------------- /docs/T-DECK.md: -------------------------------------------------------------------------------- 1 | # Meshtastic on a T-Deck 2 | 3 | ![](.screens/htpdeck.png) 4 | 5 | ## Parts 6 | - [T-Deck](https://www.lilygo.cc/products/t-deck) 7 | - [Case](https://www.printables.com/model/741124-lilygo-t-deck-case) 8 | - You can 3D print it yourself, or you can buy one on Etsy from the link above. 9 | - [Antenna](https://www.amazon.com/Connector-868-915MHz-Lora32u4-Internet-WIshiOT/dp/B07LCKNN4H) 10 | - The T-Deck has an I-PEX connection point for antennas. I used an I-PEX-to-SMA adapter so I could screw on a little 2dbi antenna. 11 | - [GPS]https://www.amazon.com/Teyleten-Robot-Dual-Mode-Positioning-Replacement/dp/B09LQDG1HY) 12 | - There may be a better 15mm option for the case above...this is just what I used. 13 | - [Battery](https://www.amazon.com/AKZYTUE-5000mAh-Battery-Rechargeable-Connector/dp/B07TXJ5XXZ/) 14 | - The battery you get depends on the size of the case you order. Do your measurements, contact me if you need help. 15 | 16 | ## GPS Installation 17 | The T-Deck has a grove connector for the GPS, but for using this inside of a case, I decided to remove the connector and solder the GPS directly to the board. 18 | 19 | ###### Soldering 20 | You will see VCC, GND, RX & TX points on both the T-Deck & the GPS. Solder wires to match these points, but switch RX & TX. So do VCC to VCC, GND to GND, and then ensure that RX is soldered to TX, and TX is soldered to RX. 21 | 22 | ###### WARNING 23 | Be careful taking off the grove! Snip the front connnection points and then use a soldering iron to loosen the metal on the 4 back side connection points. You can VERY easily pull the solder pads right off if you just try to rip the grove connector off without loosening the solder points. If you pull off a solder pad, you're pretty much boned on having a GPS module. This has happened to ALOT of people with these things. 24 | 25 | ![](.screens/solder_pads_lol.png) 26 | 27 | *Above: Picture of solder pads accidentally pulled off lol...* 28 | 29 | ## Flashing 30 | ###### **WARNING:** Do not power on the device until the antenna is plugged in! Even to flash the firmware, or for testing, make sure your antenna is plugged in or you can fry the radio! 31 | 32 | Simply plug in the T-Deck via USB and connect to a computer, then visit the [Meshtastic Web Flasher](https://flasher.meshtastic.org) and select your hardware & firmware version. Your device should show up as a serial device on /dev/ttyUSB0 or /dev/ttyAMC0. If you do not see your device, try adding your user to the dialout group. See [SETUP.md](./SETUP.md) for information on how to setup the device once it is flashed with Meshtastic. -------------------------------------------------------------------------------- /meshapi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Meshtastic Serial Interface - Developed by Acidvegas in Python (https://git.acid.vegas) 3 | 4 | import argparse 5 | import logging 6 | import os 7 | import time 8 | 9 | try: 10 | import meshtastic 11 | except ImportError: 12 | raise ImportError('meshtastic library not found (pip install meshtastic)') 13 | 14 | try: 15 | from pubsub import pub 16 | except ImportError: 17 | raise ImportError('pubsub library not found (pip install pypubsub)') 18 | 19 | 20 | # Initialize logging 21 | logging.basicConfig(level=logging.INFO, format='%(asctime)s | %(levelname)9s | %(funcName)s | %(message)s', datefmt='%Y-%m-%d %I:%M:%S') 22 | 23 | 24 | def now(): 25 | '''Returns the current date and time in a formatted string''' 26 | 27 | return time.strftime('%Y-%m-%d %H:%M:%S') 28 | 29 | 30 | class MeshtasticClient(object): 31 | def __init__(self, option: str, value: str): 32 | self.interface = None 33 | self.me = {} 34 | self.nodes = {} 35 | 36 | self.interface_option = option 37 | self.interface_value = value 38 | 39 | if self.interface_option == 'serial': 40 | from meshtastic.serial_interface import SerialInterface as MeshInterface 41 | from meshtastic.util import findPorts 42 | elif self.interface_option == 'tcp': 43 | from meshtastic.tcp_interface import TCPInterface as MeshInterface 44 | 45 | 46 | def connect(self, option: str, value: str): 47 | ''' 48 | Connect to the Meshtastic interface 49 | 50 | :param option: The interface option to connect to 51 | :param value: The value of the interface option 52 | ''' 53 | 54 | while True: 55 | try: 56 | if option == 'serial': 57 | if devices := findPorts(): 58 | if not os.path.exists(args.serial) or not args.serial in devices: 59 | raise Exception(f'Invalid serial port specified: {args.serial} (Available: {devices})') 60 | else: 61 | raise Exception('No serial devices found') 62 | self.interface = SerialInterface(value) 63 | 64 | elif option == 'tcp': 65 | self.interface = TCPInterface(value) 66 | 67 | else: 68 | raise SystemExit('Invalid interface option') 69 | 70 | except Exception as e: 71 | logging.error(f'Failed to connect to the radio: {e}') 72 | logging.error('Retrying in 15 seconds...') 73 | time.sleep(15) 74 | 75 | else: 76 | self.me = self.interface.getMyNodeInfo() 77 | break 78 | 79 | 80 | def send(self, message: str): 81 | ''' 82 | Send a message to the Meshtastic interface 83 | 84 | :param message: The message to send 85 | ''' 86 | 87 | if len(message) > 255: 88 | logging.warning('Message exceeds 255 characters') 89 | message = message[:255] 90 | 91 | self.interface.sendText(message) 92 | 93 | logging.info(f'Sent broadcast message: {message}') 94 | 95 | 96 | def listen(self): 97 | '''Create the Meshtastic callback subscriptions''' 98 | 99 | pub.subscribe(self.event_connect, 'meshtastic.connection.established') 100 | pub.subscribe(self.event_data, 'meshtastic.receive.data.portnum') 101 | pub.subscribe(self.event_disconnect, 'meshtastic.connection.lost') 102 | pub.subscribe(self.event_node, 'meshtastic.node') 103 | pub.subscribe(self.event_position, 'meshtastic.receive.position') 104 | pub.subscribe(self.event_text, 'meshtastic.receive.text') 105 | pub.subscribe(self.event_user, 'meshtastic.receive.user') 106 | 107 | logging.debug('Listening for Meshtastic events...') 108 | 109 | 110 | def event_connect(self, interface, topic=pub.AUTO_TOPIC): 111 | ''' 112 | Callback function for connection established 113 | 114 | :param interface: Meshtastic interface 115 | :param topic: PubSub topic 116 | ''' 117 | 118 | logging.info(f'Connected to the {self.me["user"]["longName"]} radio on {self.me["user"]["hwModel"]} hardware') 119 | logging.info(f'Found a total of {len(self.nodes):,} nodes') 120 | 121 | 122 | def event_data(self, packet: dict, interface): 123 | ''' 124 | Callback function for data updates 125 | 126 | :param packet: Data information 127 | :param interface: Meshtastic interface 128 | ''' 129 | 130 | logging.info(f'Data update: {data}') 131 | 132 | 133 | def event_disconnect(self, interface, topic=pub.AUTO_TOPIC): 134 | ''' 135 | Callback function for connection lost 136 | 137 | :param interface: Meshtastic interface 138 | :param topic: PubSub topic 139 | ''' 140 | 141 | logging.warning('Lost connection to radio!') 142 | 143 | time.sleep(10) 144 | 145 | # TODO: Consider storing the interface option and value in a class variable since we don't want to reference the args object inside the class 146 | self.connect('serial' if args.serial else 'tcp', args.serial if args.serial else args.tcp) 147 | 148 | 149 | def event_node(self, node): 150 | ''' 151 | Callback function for node updates 152 | 153 | :param node: Node information 154 | ''' 155 | 156 | # Node ID Formula = f'!{hex(node_num)[2:]}' 157 | 158 | self.nodes[node['num']] = node 159 | 160 | logging.info(f'Node found: {node["user"]["id"]} - {node["user"]["shortName"].ljust(4)} - {node["user"]["longName"]}') 161 | print(node) 162 | 163 | 164 | def event_position(self, packet: dict, interface): 165 | ''' 166 | Callback function for position updates 167 | 168 | :param packet: Position information 169 | :param interface: Meshtastic interface 170 | ''' 171 | 172 | sender = packet['from'] 173 | msg = packet['decoded']['payload'].hex() 174 | id = self.nodes[sender]['user']['id'] if sender in self.nodes else '!unk ' 175 | name = self.nodes[sender]['user']['longName'] if sender in self.nodes else 'UNK' 176 | longitude = packet['decoded']['position']['longitudeI'] / 1e7 177 | latitude = packet['decoded']['position']['latitudeI'] / 1e7 178 | altitude = packet['decoded']['position']['altitude'] 179 | snr = packet['rxSnr'] 180 | rssi = packet['rxRssi'] 181 | 182 | logging.info(f'{id} - {name}: {longitude}, {latitude}, {altitude}m (SNR: {snr}, RSSI: {rssi}) - {msg}') 183 | 184 | 185 | def event_text(self, packet: dict, interface): 186 | ''' 187 | Callback function for received packets 188 | 189 | :param packet: Packet received 190 | ''' 191 | 192 | sender = packet['from'] 193 | to = packet['to'] 194 | msg = packet['decoded']['payload'].decode('utf-8') 195 | id = self.nodes[sender]['user']['id'] if sender in self.nodes else '!unk ' 196 | name = self.nodes[sender]['user']['longName'] if sender in self.nodes else 'UNK' 197 | target = self.nodes[to]['user']['longName'] if to in self.nodes else 'UNK' 198 | 199 | logging.info(f'{id} {name} -> {target}: {msg}') 200 | print(packet) 201 | 202 | 203 | def event_user(self, packet: dict, interface): 204 | ''' 205 | Callback function for user updates 206 | 207 | :param user: User information 208 | ''' 209 | 210 | ''' 211 | { 212 | 'from' : 862341900, 213 | 'to' : 4294967295, 214 | 'decoded' : { 215 | 'portnum' : 'NODEINFO_APP', 216 | 'payload' : b'\n\t!33664b0c\x12\x08HELLDIVE\x1a\x04H3LL"\x06d\xe83fK\x0c(+8\x03', 217 | 'wantResponse' : True, 218 | 'user' : { 219 | 'id' : '!33664b0c', 220 | 'longName' : 'HELLDIVE', 221 | 'shortName' : 'H3LL', 222 | 'macaddr' : 'ZOgzZksM', 223 | 'hwModel' : 'HELTEC_V3', 224 | 'role' : 'ROUTER_CLIENT', 225 | 'raw' : 'rm this' 226 | } 227 | }, 228 | 'id' : 1612906268, 229 | 'rxTime' : 1714279638, 230 | 'rxSnr' : 6.25, 231 | 'hopLimit' : 3, 232 | 'rxRssi' : -38, 233 | 'hopStart' : 3, 234 | 'raw' : 'rm this' 235 | } 236 | ''' 237 | 238 | # Not sure what to do with this yet... 239 | pass 240 | 241 | 242 | 243 | if __name__ == '__main__': 244 | parser = argparse.ArgumentParser(description='Meshtastic Interfacing Tool') 245 | parser.add_argument('--serial', help='Use serial interface') # Typically /dev/ttyUSB0 or /dev/ttyACM0 246 | parser.add_argument('--tcp', help='Use TCP interface') # Can be an IP address or hostname (meshtastic.local) 247 | args = parser.parse_args() 248 | 249 | # Ensure one interface is specified 250 | if (not args.serial and not args.tcp) or (args.serial and args.tcp): 251 | raise SystemExit('Must specify either --serial or --tcp interface') 252 | 253 | # Initialize the Meshtastic client 254 | mesh = MeshtasticClient() 255 | 256 | # Listen for Meshtastic events 257 | mesh.listen() 258 | 259 | # Connect to the Meshtastic interface 260 | mesh.connect('serial' if args.serial else 'tcp', args.serial if args.serial else args.tcp) 261 | 262 | # Keep-alive loop 263 | try: 264 | while True: 265 | time.sleep(60) 266 | except KeyboardInterrupt: 267 | try: 268 | mesh.interface.close() 269 | except: 270 | pass 271 | finally: 272 | logging.info('Connection to radio lost') 273 | 274 | ''' 275 | Notes: 276 | conf = self.interface.localNode.localConfig 277 | ok = interface.getNode('^local') 278 | print(ok.channels) 279 | ''' -------------------------------------------------------------------------------- /meshirc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # meshtastic irc relay - developed by acidvegas in python (https://git.acid.vegas/meshtastic) 3 | 4 | import argparse 5 | import asyncio 6 | import logging 7 | import ssl 8 | import time 9 | 10 | # 0xEF576MkXA3aEURbCfNn6p0FfZdua4I 11 | 12 | # Formatting Control Characters / Color Codes 13 | bold = '\x02' 14 | italic = '\x1D' 15 | underline = '\x1F' 16 | reverse = '\x16' 17 | reset = '\x0f' 18 | white = '00' 19 | black = '01' 20 | blue = '02' 21 | green = '03' 22 | red = '04' 23 | brown = '05' 24 | purple = '06' 25 | orange = '07' 26 | yellow = '08' 27 | light_green = '09' 28 | cyan = '10' 29 | light_cyan = '11' 30 | light_blue = '12' 31 | pink = '13' 32 | grey = '14' 33 | light_grey = '15' 34 | 35 | 36 | # Logging Configuration 37 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(funcName)s - %(message)s') 38 | 39 | 40 | def color(msg: str, foreground: str, background: str = None) -> str: 41 | ''' 42 | Color a string with the specified foreground and background colors. 43 | 44 | :param msg: The string to color. 45 | :param foreground: The foreground color to use. 46 | :param background: The background color to use. 47 | ''' 48 | 49 | return f'\x03{foreground},{background}{msg}{reset}' if background else f'\x03{foreground}{msg}{reset}' 50 | 51 | 52 | class Bot(): 53 | def __init__(self): 54 | self.nickname = 'MESHTASTIC' 55 | self.reader = None 56 | self.writer = None 57 | self.last = time.time() 58 | 59 | 60 | async def action(self, chan: str, msg: str): 61 | ''' 62 | Send an ACTION to the IRC server. 63 | 64 | :param chan: The channel to send the ACTION to. 65 | :param msg: The message to send to the channel. 66 | ''' 67 | 68 | await self.sendmsg(chan, f'\x01ACTION {msg}\x01') 69 | 70 | 71 | async def raw(self, data: str): 72 | ''' 73 | Send raw data to the IRC server. 74 | 75 | :param data: The raw data to send to the IRC server. (512 bytes max including crlf) 76 | ''' 77 | 78 | await self.writer.write(data[:510].encode('utf-8') + b'\r\n') 79 | 80 | 81 | async def sendmsg(self, target: str, msg: str): 82 | ''' 83 | Send a PRIVMSG to the IRC server. 84 | 85 | :param target: The target to send the PRIVMSG to. (channel or user) 86 | :param msg: The message to send to the target. 87 | ''' 88 | 89 | await self.raw(f'PRIVMSG {target} :{msg}') 90 | 91 | 92 | async def connect(self): 93 | '''Connect to the IRC server.''' 94 | 95 | while True: 96 | try: 97 | options = { 98 | 'host' : args.server, 99 | 'port' : args.port, 100 | 'limit' : 1024, 101 | 'ssl' : ssl._create_unverified_context() if args.ssl else None, # TODO: Do not use the args variable here 102 | 'family' : 2, # AF_INET = 2, AF_INET6 = 10 103 | 'local_addr' : None 104 | } 105 | 106 | self.reader, self.writer = await asyncio.wait_for(asyncio.open_connection(**options), 15) 107 | 108 | await self.raw(f'USER MESHT 0 * :git.acid.vegas/meshtastic') # Static for now 109 | await self.raw('NICK ' + self.nickname) 110 | 111 | while not self.reader.at_eof(): 112 | data = await asyncio.wait_for(self.reader.readuntil(b'\r\n'), 300) 113 | await self.handle(data.decode('utf-8').strip()) 114 | 115 | except Exception as ex: 116 | logging.error(f'failed to connect to {args.server} ({str(ex)})') 117 | 118 | finally: 119 | await asyncio.sleep(15) 120 | 121 | 122 | async def eventPRIVMSG(self, data: str): 123 | ''' 124 | Handle the PRIVMSG event. 125 | 126 | :param data: The data received from the IRC server. 127 | ''' 128 | 129 | parts = data.split() 130 | 131 | ident = parts[0][1:] 132 | nick = parts[0].split('!')[0][1:] 133 | target = parts[2] 134 | msg = ' '.join(parts[3:])[1:] 135 | 136 | if target == args.channel: # TODO: Don't use the args variable here 137 | if msg.startswith('!'): 138 | if time.time() - self.last < 3: 139 | if not self.slow: 140 | self.slow = True 141 | await self.sendmsg(target, color('Slow down nerd!', red)) 142 | else: 143 | self.slow = False 144 | parts = msg.split() 145 | if parts[0] == '!meshage' and len(parts) > 1: 146 | message = ' '.join(parts[1:]) 147 | if len(message) > 255: 148 | await self.sendmsg(target, color('Message exceeds 255 bytes nerd!', red)) 149 | # TODO: Send a meshtastic message (We have to ensure our outbounds from IRC don't loop back into IRC) 150 | 151 | self.last = time.time() # Update the last command time if it starts with ! character to prevent command flooding 152 | 153 | 154 | async def handle(self, data: str): 155 | ''' 156 | Handle the data received from the IRC server. 157 | 158 | :param data: The data received from the IRC server. 159 | ''' 160 | 161 | logging.info(data) 162 | 163 | try: 164 | parts = data.split() 165 | 166 | if parts[0] == 'PING': 167 | await self.raw('PONG ' + parts[1]) 168 | 169 | elif parts[1] == '001': # RPL_WELCOME 170 | await self.raw(f'MODE {self.nickname} +B') 171 | await self.sendmsg('NickServ', f'IDENTIFY {self.nickname} simps0nsfan420') 172 | await asyncio.sleep(10) # Wait for NickServ to identify or any channel join delays 173 | await self.raw(f'JOIN {args.channel} {args.key if args.key else ""}') 174 | 175 | elif parts[1] == '433': # ERR_NICKNAMEINUSE 176 | self.nickname += '_' # revamp this to be more unique 177 | await self.raw('NICK ' + self.nickname) 178 | 179 | elif parts[1] == 'INVITE': 180 | target = parts[2] 181 | chan = parts[3][1:] 182 | if target == self.nickname and chan == args.channel: 183 | await self.raw(f'JOIN {chan}') 184 | 185 | elif parts[1] == 'KICK': 186 | chan = parts[2] 187 | kicked = parts[3] 188 | if kicked == self.nickname and chan == args.channel: 189 | await asyncio.sleep(3) 190 | await self.raw(f'JOIN {args.channel} {args.key if args.key else ""}') 191 | 192 | elif parts[1] == 'PRIVMSG': 193 | await self.eventPRIVMSG(data) # We put this in a separate function since it will likely be the most used/handled event 194 | 195 | except (UnicodeDecodeError, UnicodeEncodeError): 196 | pass 197 | 198 | except Exception as ex: 199 | logging.exception(f'Unknown error has occured! ({ex})') 200 | 201 | 202 | 203 | if __name__ == '__main__': 204 | parser = argparse.ArgumentParser(description='Connect to an IRC server.') 205 | parser.add_argument('server', help='The IRC server address.') 206 | parser.add_argument('channel', help='The IRC channel to join.') 207 | parser.add_argument('--port', type=int, help='The port number for the IRC server.') 208 | parser.add_argument('--ssl', action='store_true', help='Use SSL for the connection.') 209 | parser.add_argument('--key', default='', help='The key (password) for the IRC channel, if required.') 210 | args = parser.parse_args() 211 | 212 | if not args.channel.startswith('#'): 213 | channel = '#' + args.channel 214 | 215 | if not args.port: 216 | args.port = 6697 if args.ssl else 6667 217 | elif args.port < 1 or args.port > 65535: 218 | raise ValueError('Port must be between 1 and 65535.') 219 | 220 | print(f'Connecting to {args.server}:{args.port} (SSL: {args.ssl}) and joining {args.channel} (Key: {args.key or 'None'})') 221 | 222 | bot = Bot() 223 | 224 | asyncio.run(bot.connect()) 225 | -------------------------------------------------------------------------------- /meshmqtt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Meshtastic MQTT Interface - Developed by acidvegas in Python (https://acid.vegas/meshtastic) 3 | 4 | import argparse 5 | import base64 6 | import logging 7 | 8 | try: 9 | from cryptography.hazmat.backends import default_backend 10 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes 11 | except ImportError: 12 | raise SystemExit('missing the cryptography module (pip install cryptography)') 13 | 14 | try: 15 | from meshtastic import mesh_pb2, mqtt_pb2, portnums_pb2, telemetry_pb2 16 | except ImportError: 17 | raise SystemExit('missing the meshtastic module (pip install meshtastic)') 18 | 19 | try: 20 | import paho.mqtt.client as mqtt 21 | except ImportError: 22 | raise SystemExit('missing the paho-mqtt module (pip install paho-mqtt)') 23 | 24 | 25 | # Initialize the logging module 26 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %I:%M:%S') 27 | 28 | 29 | def clean_dict(dictionary: dict) -> dict: 30 | ''' 31 | Remove empty fields from a dictionary. 32 | 33 | :param dictionary: The dictionary to remove empty fields from 34 | ''' 35 | 36 | return {key: value for key, value in dictionary.items() if value} 37 | 38 | 39 | class MeshtasticMQTT(object): 40 | def __init__(self): 41 | '''Initialize the Meshtastic MQTT client''' 42 | 43 | self.broadcast_id = 4294967295 # Our channel ID 44 | self.key = None 45 | 46 | 47 | def connect(self, broker: str, port: int, root: str, tls: bool, username: str, password: str, key: str): 48 | ''' 49 | Connect to the MQTT broker 50 | 51 | :param broker: The MQTT broker address 52 | :param port: The MQTT broker port 53 | :param root: The root topic to subscribe to 54 | :param tls: Enable TLS/SSL 55 | :param username: The MQTT username 56 | :param password: The MQTT password 57 | :param key: The encryption key 58 | ''' 59 | 60 | # Initialize the MQTT client (these arguments were the only way to get it to work properly..) 61 | client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id='', clean_session=True, userdata=None) 62 | 63 | # Set the username and password for the MQTT broker 64 | client.username_pw_set(username=username, password=password) 65 | 66 | # Set the encryption key global in the client class (the default key is padded to ensure it's the correct length for AES) 67 | self.key = '1PG7OiApB1nwvP+rz05pAQ==' if key == 'AQ==' else key 68 | 69 | # Enable TLS/SSL if the user specified it 70 | if tls: 71 | client.tls_set() 72 | #client.tls_insecure_set(False) 73 | 74 | # Set the MQTT callbacks 75 | client.on_connect = self.on_connect 76 | client.on_message = self.on_message 77 | client.on_subscribe = self.on_subscribe 78 | client.on_unsubscribe = self.on_unsubscribe 79 | 80 | # Connect to the MQTT broker and subscribe to the root topic 81 | client.connect(broker, port, 60) 82 | client.subscribe(root, 0) 83 | 84 | # Keep-alive loop 85 | client.loop_forever() 86 | 87 | 88 | def decrypt_message_packet(self, message_packet): 89 | ''' 90 | Decrypt an encrypted message packet. 91 | 92 | :param message_packet: The message packet to decrypt 93 | ''' 94 | 95 | # Ensure the key is formatted and padded correctly before turning it into bytes 96 | padded_key = self.key.ljust(len(self.key) + ((4 - (len(self.key) % 4)) % 4), '=') 97 | key = padded_key.replace('-', '+').replace('_', '/') 98 | key_bytes = base64.b64decode(key.encode('ascii')) 99 | 100 | # Extract the nonce from the packet 101 | nonce_packet_id = getattr(message_packet, 'id').to_bytes(8, 'little') 102 | nonce_from_node = getattr(message_packet, 'from').to_bytes(8, 'little') 103 | nonce = nonce_packet_id + nonce_from_node 104 | 105 | # Decrypt the message 106 | cipher = Cipher(algorithms.AES(key_bytes), modes.CTR(nonce), backend=default_backend()) 107 | decryptor = cipher.decryptor() 108 | decrypted_bytes = decryptor.update(getattr(message_packet, 'encrypted')) + decryptor.finalize() 109 | 110 | # Parse the decrypted message 111 | data = mesh_pb2.Data() 112 | data.ParseFromString(decrypted_bytes) 113 | message_packet.decoded.CopyFrom(data) 114 | 115 | return message_packet 116 | 117 | 118 | def on_connect(self, client, userdata, flags, rc, properties): 119 | ''' 120 | Callback for when the client receives a CONNACK response from the server. 121 | 122 | :param client: The client instance for this callback 123 | :param userdata: The private user data as set in Client() or user_data_set() 124 | :param flags: Response flags sent by the broker 125 | :param rc: The connection result 126 | :param properties: The properties returned by the broker 127 | ''' 128 | 129 | if rc == 0: 130 | logging.info('Connected to MQTT broker') 131 | else: 132 | logging.error(f'Failed to connect to MQTT broker: {rc}') 133 | 134 | 135 | def on_message(self, client, userdata, msg): 136 | ''' 137 | Callback for when a message is received from the server. 138 | 139 | :param client: The client instance for this callback 140 | :param userdata: The private user data as set in Client() or user_data_set() 141 | :param msg: An instance of MQTTMessage. This is a 142 | ''' 143 | 144 | # Define the service envelope 145 | service_envelope = mqtt_pb2.ServiceEnvelope() 146 | 147 | try: 148 | # Parse the message payload 149 | service_envelope.ParseFromString(msg.payload) 150 | 151 | # Extract the message packet from the service envelope 152 | message_packet = service_envelope.packet 153 | except Exception as e: 154 | #logging.error(f'Failed to parse message: {str(e)}') 155 | return 156 | 157 | # Check if the message is encrypted before decrypting it 158 | if message_packet.HasField('encrypted') and not message_packet.HasField('decoded'): 159 | message_packet = self.decrypt_message_packet(message_packet) 160 | 161 | text = { 162 | 'from' : getattr(message_packet, 'from'), 163 | 'to' : getattr(message_packet, 'to'), 164 | 'channel' : getattr(message_packet, 'channel'), 165 | 'id' : getattr(message_packet, 'id'), 166 | 'rx_time' : getattr(message_packet, 'rx_time'), 167 | 'hop_limit' : getattr(message_packet, 'hop_limit'), 168 | 'priority' : getattr(message_packet, 'priority'), 169 | 'hop_start' : getattr(message_packet, 'hop_start') 170 | } 171 | logging.info(text) 172 | 173 | if message_packet.decoded.portnum == portnums_pb2.UNKNOWN_APP: 174 | logging.warning('Received an unknown app message:') 175 | logging.info(message_packet) 176 | 177 | elif message_packet.decoded.portnum == portnums_pb2.TEXT_MESSAGE_APP: 178 | text_payload = message_packet.decoded.payload.decode('utf-8') 179 | text = { 180 | 'message' : text_payload, 181 | 'from' : getattr(message_packet, 'from'), 182 | 'id' : getattr(message_packet, 'id'), 183 | 'to' : getattr(message_packet, 'to') 184 | } 185 | logging.info('Received text message:') 186 | logging.info(text) 187 | 188 | elif message_packet.decoded.portnum == portnums_pb2.REMOTE_HARDWARE_APP: 189 | data = mesh_pb2.RemoteHardware() 190 | data.ParseFromString(message_packet.decoded.payload) 191 | logging.info('Received remote hardware:') 192 | logging.info(data) 193 | 194 | elif message_packet.decoded.portnum == portnums_pb2.POSITION_APP: 195 | data = mesh_pb2.Position() 196 | data.ParseFromString(message_packet.decoded.payload) 197 | 198 | data_dict = {key: value for key, value in data} 199 | print(data_dict) 200 | 201 | logging.info('Received position:') 202 | loc = { 203 | 'lattitude' : getattr(data, 'latitude_i') / 1e7, 204 | 'longitude' : getattr(data, 'longitude_i') / 1e7, 205 | 'altitude' : getattr(data, 'altitude') / 1000, 206 | 'location_source' : getattr(data, 'location_source'), 207 | 'altitude_source' : getattr(data, 'altitude_source'), 208 | 'pdop' : getattr(data, 'PDOP'), 209 | 'hdop' : getattr(data, 'HDOP'), 210 | 'vdop' : getattr(data, 'VDOP'), 211 | 'gps_accuracy' : getattr(data, 'gps_accuracy'), 212 | 'ground_speed' : getattr(data, 'ground_speed'), 213 | 'ground_track' : getattr(data, 'ground_track'), 214 | 'fix_quality' : getattr(data, 'fix_quality'), 215 | 'fix_type' : getattr(data, 'fix_type'), 216 | 'sats_in_view' : getattr(data, 'sats_in_view'), 217 | 'sensor_id' : getattr(data, 'sensor_id'), 218 | 'next_update' : getattr(data, 'next_update'), 219 | 'seq_number' : getattr(data, 'seq_number'), 220 | 'precision_bits' : getattr(data, 'precision_bits') 221 | } 222 | 223 | if (loc := clean_dict(loc)): 224 | logging.info(loc) 225 | 226 | elif message_packet.decoded.portnum == portnums_pb2.NODEINFO_APP: 227 | data = mesh_pb2.NodeInfo() 228 | #data.ParseFromString(message_packet.decoded.payload) 229 | logging.info('Received node info:') 230 | logging.info(message_packet) 231 | 232 | elif message_packet.decoded.portnum == portnums_pb2.ROUTING_APP: 233 | data = mesh_pb2.Routing() 234 | data.ParseFromString(message_packet.decoded.payload) 235 | logging.info('Received routing:') 236 | logging.info(data) 237 | 238 | elif message_packet.decoded.portnum == portnums_pb2.ADMIN_APP: 239 | data = mesh_pb2.Admin() 240 | data.ParseFromString(message_packet.decoded.payload) 241 | logging.info('Received admin:') 242 | logging.info(data) 243 | 244 | elif message_packet.decoded.portnum == portnums_pb2.TEXT_MESSAGE_COMPRESSED_APP: 245 | data = mesh_pb2.TextMessageCompressed() 246 | data.ParseFromString(message_packet.decoded.payload) 247 | logging.info('Received compressed text message:') 248 | logging.info(data) 249 | 250 | elif message_packet.decoded.portnum == portnums_pb2.WAYPOINT_APP: 251 | data = mesh_pb2.Waypoint() 252 | data.ParseFromString(message_packet.decoded.payload) 253 | logging.info('Received waypoint:') 254 | logging.info(data) 255 | 256 | elif message_packet.decoded.portnum == portnums_pb2.AUDIO_APP: 257 | data = mesh_pb2.Audio() 258 | data.ParseFromString(message_packet.decoded.payload) 259 | logging.info('Received audio:') 260 | logging.info(data) 261 | 262 | elif message_packet.decoded.portnum == portnums_pb2.DETECTION_SENSOR_APP: 263 | data = mesh_pb2.DetectionSensor() 264 | data.ParseFromString(message_packet.decoded.payload) 265 | logging.info('Received detection sensor:') 266 | logging.info(data) 267 | 268 | elif message_packet.decoded.portnum == portnums_pb2.REPLY_APP: 269 | data = mesh_pb2.Reply() 270 | data.ParseFromString(message_packet.decoded.payload) 271 | logging.info('Received reply:') 272 | logging.info(data) 273 | 274 | elif message_packet.decoded.portnum == portnums_pb2.IP_TUNNEL_APP: 275 | data = mesh_pb2.IPTunnel() 276 | data.ParseFromString(message_packet.decoded.payload) 277 | logging.info('Received IP tunnel:') 278 | logging.info(data) 279 | 280 | elif message_packet.decoded.portnum == portnums_pb2.PAXCOUNTER_APP: 281 | data = mesh_pb2.Paxcounter() 282 | data.ParseFromString(message_packet.decoded.payload) 283 | logging.info('Received paxcounter:') 284 | logging.info(data) 285 | 286 | elif message_packet.decoded.portnum == portnums_pb2.SERIAL_APP: 287 | data = mesh_pb2.Serial() 288 | data.ParseFromString(message_packet.decoded.payload) 289 | logging.info('Received serial:') 290 | logging.info(data) 291 | 292 | elif message_packet.decoded.portnum == portnums_pb2.STORE_FORWARD_APP: 293 | logging.info('Received store and forward:') 294 | logging.info(message_packet) 295 | logging.info(message_packet.decoded.payload) 296 | 297 | elif message_packet.decoded.portnum == portnums_pb2.RANGE_TEST_APP: 298 | data = mesh_pb2.RangeTest() 299 | data.ParseFromString(message_packet.decoded.payload) 300 | logging.info('Received range test:') 301 | logging.info(data) 302 | 303 | elif message_packet.decoded.portnum == portnums_pb2.TELEMETRY_APP: 304 | data = telemetry_pb2.Telemetry() 305 | data.ParseFromString(message_packet.decoded.payload) 306 | logging.info('Received telemetry:') 307 | 308 | data_dict = {} 309 | for field, value in data.ListFields(): 310 | if field.name == 'device_metrics': 311 | text = clean_dict({item.name: getattr(value, item.name) for item in value.DESCRIPTOR.fields if hasattr(value, item.name)}) 312 | if text: 313 | logging.info(text) 314 | else: 315 | data_dict[field.name] = value 316 | 317 | logging.info(data_dict) 318 | 319 | if getattr(data, 'device_metrics'): 320 | text = { 321 | 'battery_level' : getattr(data.device_metrics, 'battery_level'), 322 | 'voltage' : getattr(data.device_metrics, 'voltage'), 323 | 'channel_utilization' : getattr(data.device_metrics, 'channel_utilization'), 324 | 'air_util_tx' : getattr(data.device_metrics, 'air_util_tx'), 325 | 'uptime_seconds' : getattr(data.device_metrics, 'uptime_seconds') 326 | } 327 | if (text := clean_dict(text)): 328 | logging.info(text) 329 | 330 | if getattr(data, 'environment_metrics'): 331 | env_metrics = { 332 | 'barometric_pressure' : getattr(data.environment_metrics, 'barometric_pressure'), 333 | 'current' : getattr(data.environment_metrics, 'current'), 334 | 'distance' : getattr(data.environment_metrics, 'distance'), 335 | 'gas_resistance' : getattr(data.environment_metrics, 'gas_resistance'), 336 | 'iaq' : getattr(data.environment_metrics, 'iaq'), 337 | 'relative_humidity' : getattr(data.environment_metrics, 'relative_humidity'), 338 | 'temperature' : getattr(data.environment_metrics, 'temperature'), 339 | 'voltage' : getattr(data.environment_metrics, 'voltage') 340 | } 341 | if (env_metrics := clean_dict(env_metrics)): 342 | logging.info(env_metrics) 343 | 344 | elif message_packet.decoded.portnum == portnums_pb2.ZPS_APP: 345 | data = mesh_pb2.Zps() 346 | data.ParseFromString(message_packet.decoded.payload) 347 | logging.info('Received ZPS:') 348 | logging.info(data) 349 | 350 | elif message_packet.decoded.portnum == portnums_pb2.SIMULATOR_APP: 351 | data = mesh_pb2.Simulator() 352 | data.ParseFromString(message_packet.decoded.payload) 353 | logging.info('Received simulator:') 354 | logging.info(data) 355 | 356 | elif message_packet.decoded.portnum == portnums_pb2.TRACEROUTE_APP: 357 | routeDiscovery = mesh_pb2.RouteDiscovery() 358 | routeDiscovery.ParseFromString(message_packet.decoded.payload) 359 | logging.info('Received traceroute:') 360 | logging.info(routeDiscovery) 361 | 362 | elif message_packet.decoded.portnum == portnums_pb2.NEIGHBORINFO_APP: 363 | neighborInfo = mesh_pb2.NeighborInfo() 364 | neighborInfo.ParseFromString(message_packet.decoded.payload) 365 | logging.info('Received neighbor info:') 366 | info = { 367 | 'node_id' : getattr(neighborInfo, 'node_id'), 368 | 'last_sent_by_id' : getattr(neighborInfo, 'last_sent_by_id'), 369 | 'node_broadcast_interval_secs' : getattr(neighborInfo, 'node_broadcast_interval_secs'), 370 | 'neighbors' : getattr(neighborInfo, 'neighbors') 371 | } 372 | logging.info(info) 373 | 374 | elif message_packet.decoded.portnum == portnums_pb2.ATAK_PLUGIN: 375 | data = mesh_pb2.AtakPlugin() 376 | data.ParseFromString(message_packet.decoded.payload) 377 | logging.info('Received ATAK plugin:') 378 | logging.info(data) 379 | 380 | elif message_packet.decoded.portnum == portnums_pb2.PRIVATE_APP: 381 | data = mesh_pb2.Private() 382 | data.ParseFromString(message_packet.decoded.payload) 383 | logging.info('Received private:') 384 | logging.info(data) 385 | 386 | elif message_packet.decoded.portnum == portnums_pb2.ATAK_FORWARDER: 387 | data = mesh_pb2.AtakForwarder() 388 | data.ParseFromString(message_packet.decoded.payload) 389 | logging.info('Received ATAK forwarder:') 390 | logging.info(data) 391 | 392 | else: 393 | logging.warning('Received an unknown message:') 394 | logging.info(message_packet) 395 | 396 | # Unencrypted messages 397 | else: 398 | if message_packet.decoded.portnum == portnums_pb2.MAP_REPORT_APP: 399 | pos = mesh_pb2.Position() 400 | pos.ParseFromString(message_packet.decoded.payload) 401 | logging.info('Received map report:') 402 | logging.info(pos) 403 | 404 | else: 405 | logging.warning('Received an unencrypted message') 406 | logging.info(f'Payload: {message_packet}') 407 | 408 | 409 | def on_subscribe(self, client, userdata, mid, reason_code_list, properties): 410 | ''' 411 | Callback for when the client receives a SUBACK response from the server. 412 | 413 | :param client: The client instance for this callback 414 | :param userdata: The private user data as set in Client() or user_data_set() 415 | :param mid: The message ID of the subscribe request 416 | :param reason_code_list: A list of SUBACK reason codes 417 | :param properties: The properties returned by the broker 418 | ''' 419 | 420 | # Since we subscribed only for a single channel, reason_code_list contains a single entry 421 | if reason_code_list[0].is_failure: 422 | logging.error(f'Broker rejected you subscription: {reason_code_list[0]}') 423 | else: 424 | logging.info(f'Broker granted the following QoS: {reason_code_list[0].value}') 425 | 426 | 427 | def on_unsubscribe(self, client, userdata, mid, reason_code_list, properties): 428 | ''' 429 | Callback for when the client receives a UNSUBACK response from the server. 430 | 431 | :param client: The client instance for this callback 432 | :param userdata: The private user data as set in Client() or user_data_set() 433 | :param mid: The message ID of the unsubscribe request 434 | :param reason_code_list: A list of UNSUBACK reason codes 435 | :param properties: The properties returned by the broker 436 | ''' 437 | 438 | # reason_code_list is only present in MQTTv5, it will always be empty in MQTTv3 439 | if len(reason_code_list) == 0 or not reason_code_list[0].is_failure: 440 | logging.info('Broker accepted the unsubscription(s)') 441 | else: 442 | logging.error(f'Broker replied with failure: {reason_code_list[0]}') 443 | 444 | # Disconnect from the broker 445 | client.disconnect() 446 | 447 | 448 | 449 | if __name__ == '__main__': 450 | parser = argparse.ArgumentParser(description='Meshtastic MQTT Interface') 451 | parser.add_argument('--broker', default='mqtt.meshtastic.org', help='MQTT broker address') 452 | parser.add_argument('--port', default=1883, type=int, help='MQTT broker port') 453 | parser.add_argument('--root', default='#', help='Root topic') 454 | parser.add_argument('--tls', action='store_true', help='Enable TLS/SSL') 455 | parser.add_argument('--username', default='meshdev', help='MQTT username') 456 | parser.add_argument('--password', default='large4cats', help='MQTT password') 457 | parser.add_argument('--key', default='AQ==', help='Encryption key') 458 | args = parser.parse_args() 459 | 460 | client = MeshtasticMQTT() 461 | client.connect(args.broker, args.port, args.root, args.tls, args.username, args.password, args.key) --------------------------------------------------------------------------------