diff --git a/FIRMWARE_HACKS.md b/FIRMWARE.md similarity index 96% rename from FIRMWARE_HACKS.md rename to FIRMWARE.md index 3e00759..a0d7015 100644 --- a/FIRMWARE_HACKS.md +++ b/FIRMWARE.md @@ -28,7 +28,6 @@ As far as I know, at the time of writing this, the onyl way to change the Ringtone is from the App... - ## Compile & flash firmware - Select `PlatformIO: Pick Project Environment` & select your board. - Run `PLatformIO: Build` to compile the firmware. diff --git a/README.md b/README.md index 1b43d04..2af03d7 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,25 @@ # Meshtastic Utilities -> Experiments with Meshtastic, MQTT, Lora, & more.... -## WORK-IN-PROGRESS - -## Information 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). 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. The hardware I am experimenting with: [Lilygo T-Deck](https://www.lilygo.cc/products/t-deck), [Lilygo T-Beam](https://www.lilygo.cc/products/t-beam-v1-1-esp32-lora-module), [Heltec Lora 32 v3](https://heltec.org/project/wifi-lora-32-v3/), and [RAK Wireless 4631](https://store.rakwireless.com/products/wisblock-core-modules?variant=42440631419078) -## Notes to self & developers -- The node id formula is: f'!{hex(node_num)[2:]}' +## Documentation +- [Firmware Hacks & Customization](./FIRMWARE.md) +- [Setup Hardware](./SETUP.md) -## Hardware & Software related Issues + +## Bugs & Issues - T-Deck 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... - T-Deck using a custom MQTT with TLS & auth will cause a reboot loop *(Need to fix this ASAP)* - `event_node` event is called **AS** we are defining the interface, so using `self.interface` in that callback will error. ## Roadmap -- Asyncronous meshtastic interface *(Priority)* +- Asyncronous meshtastic interface - MQTT interface with working decryption - Documentation on MQTT bridging for high availability -- Create a simple setup script to provision new devices over serial - 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)* ___ diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 0000000..d21f952 --- /dev/null +++ b/SETUP.md @@ -0,0 +1,41 @@ +# Setup Hardware + +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. + +- `pip install meshtastic` to install the CLI tool +- Plug in your device *(Make sure the USB cable you are using allows data transfer & not just power)* +- Run the commands below *(Each command will make the device reboot after setting it)* + +###### NAME +``` +meshtastic --set-owner 'CHANGEME' --set-owner-short 'CHNG' +``` + +**Note:** Short name can only be 4 alphanumeric characters. + +###### LORA +``` +meshtastic --set lora.region US +``` + +###### CHANNEL +``` +meshtastic --ch-set name "SUPERNETS" --ch-set psk "CHANGEME" --ch-set uplink_enabled true --ch-set downlink_enabled true --ch-index 0 +``` + +###### WIFI +``` +meshtastic --set network.wifi_enabled true --set network.wifi_ssid "CHANGEME" --set network.wifi_psk "CHANGEME" +``` + +###### MQTT +``` +meshtastic --set mqtt.enabled true --set mqtt.address "CHANGEME" --set mqtt.username "CHANGEME" --set mqtt.password "CHANGEME" --set mqtt.tls_enabled true --set mqtt.root "msh/CHANGEME" --set mqtt.encryption_enabled true +``` + +###### BLUETOOTH +``` +meshtastic --set bluetooth.enabled true +``` + +**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. diff --git a/meshtastic_serial.py b/meshapi.py similarity index 85% rename from meshtastic_serial.py rename to meshapi.py index 27566ed..ffd4e46 100644 --- a/meshtastic_serial.py +++ b/meshapi.py @@ -8,9 +8,6 @@ import time try: import meshtastic - from meshtastic.serial_interface import SerialInterface - from meshtastic.util import findPorts - from meshtastic.tcp_interface import TCPInterface except ImportError: raise ImportError('meshtastic library not found (pip install meshtastic)') @@ -24,12 +21,27 @@ except ImportError: logging.basicConfig(level=logging.INFO, format='%(asctime)s | %(levelname)9s | %(funcName)s | %(message)s', datefmt='%Y-%m-%d %I:%M:%S') +def now(): + '''Returns the current date and time in a formatted string''' + + return time.strftime('%Y-%m-%d %H:%M:%S') + + class MeshtasticClient(object): - def __init__(self): + def __init__(self, option: str, value: str): self.interface = None - self.me = {} + self.me = {} self.nodes = {} + self.interface_option = option + self.interface_value = value + + if self.interface_option == 'serial': + from meshtastic.serial_interface import SerialInterface as MeshInterface + from meshtastic.util import findPorts + elif self.interface_option == 'tcp': + from meshtastic.tcp_interface import TCPInterface as MeshInterface + def connect(self, option: str, value: str): ''' @@ -56,16 +68,16 @@ class MeshtasticClient(object): raise SystemExit('Invalid interface option') except Exception as e: - logging.error(f'Failed to connect to the Meshtastic interface: {e}') - logging.error('Retrying in 10 seconds...') - time.sleep(10) + logging.error(f'Failed to connect to the radio: {e}') + logging.error('Retrying in 15 seconds...') + time.sleep(15) else: self.me = self.interface.getMyNodeInfo() break - def sendmsg(self, message: str, destination: int, channelIndex: int = 0): + def send(self, message: str): ''' Send a message to the Meshtastic interface @@ -76,7 +88,7 @@ class MeshtasticClient(object): logging.warning('Message exceeds 255 characters') message = message[:255] - self.interface.sendText(message, destination, wantAck=True, channelIndex=channelIndex) # Do we need wantAck? + self.interface.sendText(message) logging.info(f'Sent broadcast message: {message}') @@ -115,7 +127,7 @@ class MeshtasticClient(object): :param interface: Meshtastic interface ''' - logging.info(f'Data update: {packet}') + logging.info(f'Data update: {data}') def event_disconnect(self, interface, topic=pub.AUTO_TOPIC): @@ -141,9 +153,12 @@ class MeshtasticClient(object): :param node: Node information ''' + # Node ID Formula = f'!{hex(node_num)[2:]}' + self.nodes[node['num']] = node - logging.info(f'Node recieved: {node["user"]["id"]} - {node["user"]["shortName"].ljust(4)} - {node["user"]["longName"]}') + logging.info(f'Node found: {node["user"]["id"]} - {node["user"]["shortName"].ljust(4)} - {node["user"]["longName"]}') + print(node) def event_position(self, packet: dict, interface): @@ -155,7 +170,7 @@ class MeshtasticClient(object): ''' sender = packet['from'] - msg = packet['decoded']['payload'].hex() # What exactly is contained in this payload? + msg = packet['decoded']['payload'].hex() id = self.nodes[sender]['user']['id'] if sender in self.nodes else '!unk ' name = self.nodes[sender]['user']['longName'] if sender in self.nodes else 'UNK' longitude = packet['decoded']['position']['longitudeI'] / 1e7 @@ -164,7 +179,7 @@ class MeshtasticClient(object): snr = packet['rxSnr'] rssi = packet['rxRssi'] - logging.info(f'Position recieved: {id} - {name}: {longitude}, {latitude}, {altitude}m (SNR: {snr}, RSSI: {rssi}) - {msg}') + logging.info(f'{id} - {name}: {longitude}, {latitude}, {altitude}m (SNR: {snr}, RSSI: {rssi}) - {msg}') def event_text(self, packet: dict, interface): @@ -181,7 +196,7 @@ class MeshtasticClient(object): name = self.nodes[sender]['user']['longName'] if sender in self.nodes else 'UNK' target = self.nodes[to]['user']['longName'] if to in self.nodes else 'UNK' - logging.info(f'Message recieved: {id} {name} -> {target}: {msg}') + logging.info(f'{id} {name} -> {target}: {msg}') print(packet) diff --git a/meshmqtt.py b/meshmqtt.py new file mode 100644 index 0000000..1653f30 --- /dev/null +++ b/meshmqtt.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python +# Meshtastic MQTT Interface - Developed by Acidvegas in Python (https://git.acid.vegas/meshtastic) + +import base64 +import random + +try: + from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + from cryptography.hazmat.backends import default_backend +except ImportError: + raise ImportError('cryptography library not found (pip install cryptography)') + +try: + from meshtastic import mesh_pb2, mqtt_pb2, portnums_pb2, telemetry_pb2 +except ImportError: + raise ImportError('meshtastic library not found (pip install meshtastic)') + +try: + import paho.mqtt.client as mqtt +except ImportError: + raise ImportError('paho-mqtt library not found (pip install paho-mqtt)') + + +# MQTT Configuration +MQTT_BROKER = 'localhost' +MQTT_PORT = 1883 +MQTT_USERNAME = 'username' +MQTT_PASSWORD = 'password' +MQTT_ROOT_TOPIC = 'msh/US/2/c/' +CHANNEL_KEY = 'channel_key' + + +def on_connect(client, userdata, flags, rc, properties): + ''' + Callback for when the client receives a CONNACK response from the server. + + :param client: The client instance for this callback + :param userdata: The private user data as set in Client() or user_data_set() + :param flags: Response flags sent by the broker + :param rc: The connection result + :param properties: The properties returned by the broker + ''' + + if rc == 0: + print('Connected to MQTT broker') + + else: + print(f"Failed to connect to MQTT broker with result code {str(rc)}") + + +def on_message(client, userdata, msg): + ''' + Callback for when a PUBLISH message is received from the server. + + :param client: The client instance for this callback + :param userdata: The private user data as set in Client() or user_data_set() + :param msg: An instance of MQTTMessage. This is a class with members topic, payload, qos, retain. + ''' + + service_envelope = mqtt_pb2.ServiceEnvelope() + + try: + service_envelope.ParseFromString(msg.payload) + print(service_envelope) + + message_packet = service_envelope.packet + print(message_packet) + + except Exception as e: + print(f'error on message: {e}') + + else: + if message_packet.HasField('encrypted') and not message_packet.HasField('decoded'): # Do we need to check for both? + pass # Need to finish this + + + +if __name__ == '__main__': + client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) + client.on_connect = on_connect + client.username_pw_set(username=MQTT_USERNAME, password=MQTT_PASSWORD) + client.connect(MQTT_BROKER, MQTT_PORT, 60) + client.on_message = on_message + client.subscribe(MQTT_ROOT_TOPIC, 0) # This is the topic that the Meshtastic device is publishing to + + # Keep-alive loop + while client.loop() == 0: + pass \ No newline at end of file