An hour of Meshtastic traffic on the Bay Area Mesh…
This will be a bit of a rambling technical ride on a particularly nerdy topic, so buckle up (or bail out now while you still can.)
I’ve been interested in Meshtastic for quite some time. It promises to be a decentralized network that allows users to create a mesh network which is independent of any kind of internet/cell service, and exchange short text messages using inexpensive radios that they access from WiFi or Bluetooth, usually from an application which runs on their cell phone (but which does not utilize the cell network).
The underlying technology is based upon a radio technology called Lora which uses spread-spectrum technology to permit low-power, long range radio communications. Nodes are connected in a mesh network, so that intermediate nodes can forward messages across multiple hops. It uses license-free radio spectrum in the 900Mhz band in the United States.
The hardware that can connect you to this network is pretty cheap, ranging from the low end of just under $10 to about $30 or a bit more for fully integrated systems like the Lilygo T-Deck which includes a keyboard, and means you don’t even need to access it via a cell phone app.
I’ve got a variety of different options, including the Xiao S3 above, Heltec V3 modules and a T1000-E from Seeedstudio.
I live in the SF Bay Area, where a local regional mesh is organized via bayme.sh/. It’s hard to judge how many nodes are active (more on this later) but there are live maps available on the Internet which show that it probably numbers around 250 nodes or so at any given moment. This makes it seem like a pretty active group, and I’ve attended a few live presentations at the Maker Faire and at some of the hacker groups in the area.
But here enters my problem, if you examine the map on the right, you’ll see that there are a lot of nodes down in the flats surrounding Emeryville and Oakland, and even a couple in Concord. But I live in the area labeled “Pinole Valley Watershed”. One thing that I haven’t mentioned is that the frequencies which are used by the Meshtastic network are pretty much strictly line of sight. If there is a clear path (no trees, buildings, or mountains) then the Lora radios can easily travel for kilometers at very low power. But if you have any obstructions, then it becomes impossible to establish any kind of link. And of course that’s presents a problem for me.
I live in one of the many little basins that dot the Pinole Valley Watershed, rather near the northern end of the San Pablo Dam reservoir. Thus establishing a line of site to any of the existing sites is pretty much impossible.
I had a similar problem when I became interested in another wireless network technology, the AREDN network which uses more conventional WiFi hardware on licensed ham radio bands (perhaps I’ll write up more about my experiences with that at some point). But AREDN allows you to “tunnel” traffic across a normal Internet link. This is viewed as suboptimal: clearly one of the purposes of AREDN (and Meshtastic) is to create networks which are independent of any commercial infrastructure. But the AREDN community generally views such things rather pragmatically: it’s hard to get people to invest time, money and effort into creating nodes which extend the range and availability of the network without getting them linked in, and for people like myself, that’s hard to do over RF links. Internet linking allows people in remote “islands” to participate in the network, and can give some incentives to building out the network while growing the expertise and enthusiasm of individuals.
Meshtastic (or perhaps more properly, the Bay Area group) takes a dimmer view of internet linking, even though it is possible. The Meshtastic firmware isn’t a full TCP/IP stack (like AREDN) but it allows messages to be transmitted to an MQTT server, and nodes can even accept messages from such servers and broadcast them over RF. But in general, the latter is entirely discouraged, for a variety of reasons:
- It is viewed as “less pure”, and generally just thought of rather dimly.
- The Internet clearly has a much higher capacity than the underlying RF links, and so it is possible for an Internet feed to flood messages onto the RF network, making it impossible to exchange messages on the RF local nets.
- This can be exacerbated by having lots of poorly configured nodes. Often people configure nodes as ROUTER (gateway) nodes which would be much better configured as CLIENT (edge) nodes.
- There is simply a lack of really practical information to help people. While documentation exists, it’s not written in a way which really helps people understand the functioning of the network.
Whew, this is getting long.
On to my current tinkering.
While I am still working on getting some RF nodes working, I thought I might look at statistics of tracking the traffic on the bayme.sh MQTT server. This is the node which feeds the various mapping efforts and the like, and in theory should give me a good view of the traffic that are occurring across the peninsula. To do this, I wanted to write some Python code to subscribe to that server, and decode/aggregate information about that traffic it hears. It was a bit more complicated than I had hoped, but meshtastic does have a Python module which was helpful, and extracting basic information from that, I tinkered the following together:
#!/usr/bin/env python
import sys
import re
from inspect import getmembers
import paho.mqtt.client as mqtt
import json
import base64
import textwrap
from meshtastic.protobuf import mqtt_pb2, mesh_pb2, portnums_pb2, telemetry_pb2
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
app_dict = {
portnums_pb2.ADMIN_APP : 'ADMIN_APP',
portnums_pb2.AUDIO_APP : 'AUDIO_APP',
portnums_pb2.DETECTION_SENSOR_APP : 'DETECTION_SENSOR_APP',
portnums_pb2.IP_TUNNEL_APP : 'IP_TUNNEL_APP',
portnums_pb2.MAP_REPORT_APP : 'MAP_REPORT_APP',
portnums_pb2.NEIGHBORINFO_APP : 'NEIGHBORINFO_APP',
portnums_pb2.NODEINFO_APP : 'NODEINFO_APP',
portnums_pb2.PAXCOUNTER_APP : 'PAXCOUNTER_APP',
portnums_pb2.POSITION_APP : 'POSITION_APP',
portnums_pb2.POWERSTRESS_APP : 'POWERSTRESS_APP',
portnums_pb2.PRIVATE_APP : 'PRIVATE_APP',
portnums_pb2.RANGE_TEST_APP : 'RANGE_TEST_APP',
portnums_pb2.REMOTE_HARDWARE_APP : 'REMOTE_HARDWARE_APP',
portnums_pb2.REPLY_APP : 'REPLY_APP',
portnums_pb2.ROUTING_APP : 'ROUTING_APP',
portnums_pb2.SERIAL_APP : 'SERIAL_APP',
portnums_pb2.SIMULATOR_APP : 'SIMULATOR_APP',
portnums_pb2.STORE_FORWARD_APP : 'STORE_FORWARD_APP',
portnums_pb2.TELEMETRY_APP : 'TELEMETRY_APP',
portnums_pb2.TEXT_MESSAGE_APP : 'TEXT_MESSAGE_APP',
portnums_pb2.TEXT_MESSAGE_COMPRESSED_APP : 'TEXT_MESSAGE_COMPRESSED_APP',
portnums_pb2.TRACEROUTE_APP : 'TRACEROUTE_APP',
portnums_pb2.UNKNOWN_APP : 'UNKNOWN_APP',
portnums_pb2.WAYPOINT_APP : 'WAYPOINT_APP',
portnums_pb2.ZPS_APP : 'ZPS_APP'
}
# Replace with your actual MQTT broker address, port, and credentials
MQTT_BROKER = "mqtt.bayme.sh"
MQTT_PORT = 1883
MQTT_USERNAME = "meshdev"
MQTT_PASSWORD = "large4cats"
MQTT_TOPIC = "msh/US/bayarea/#" # Subscribe to all Meshtastic topics
default_key = "1PG7OiApB1nwvP+rz05pAQ==" # AKA AQ==
# Replace with your encryption key (if using encryption)
ENCRYPTION_KEY = b'your_encryption_key' # 32-byte key (e.g., generated with Fernet.generate_key())
def on_connect(client, userdata, flags, rc, properties=None):
if rc == 0:
print(f"CONNECTED")
client.subscribe(MQTT_TOPIC)
def decode_encrypted(mp):
try:
kb = base64.b64decode(default_key.encode("ascii"))
nonce_packet_id = getattr(mp, "id").to_bytes(8, "little")
nonce_from_node = getattr(mp, "from").to_bytes(8, "little")
nonce = nonce_packet_id + nonce_from_node
cipher = Cipher(algorithms.AES(kb), modes.CTR(nonce), backend=default_backend())
decryptor = cipher.decryptor()
db = decryptor.update(getattr(mp, "encrypted")) + decryptor.finalize()
data = mesh_pb2.Data()
data.ParseFromString(db)
mp.decoded.CopyFrom(data)
except Exception as e:
print(f"DECRYPT FAILURE: {e}")
port_dict = dict()
def on_message(client, userdata, msg, properties=None):
global wrapper, port_set
try:
se = mqtt_pb2.ServiceEnvelope()
se.ParseFromString(msg.payload)
mp = se.packet
if mp.encrypted:
decode_encrypted(mp)
if not mp.HasField("decoded"):
return
print(mp)
port_dict[mp.decoded.portnum] = port_dict.get(mp.decoded.portnum, 0) + 1
if mp.decoded.portnum == portnums_pb2.TEXT_MESSAGE_APP:
print("TEXT_MESSAGE_APP")
try:
tp = mp.decoded.payload.decode("utf-8")
for l in str(tp).split("\n"):
print(f" {l}")
except Exception as e:
print(f"PROBLEM DECODING TEXT_MESSAGE_APP {e}")
elif mp.decoded.portnum == portnums_pb2.NODEINFO_APP:
print("NODEINFO_APP")
info = mesh_pb2.User()
try:
info.ParseFromString(mp.decoded.payload)
for l in str(info).split("\n"):
print(f" {l}")
except Exception as e:
print(f"PROBLEM DECODING NODEINFO_APP {e}")
elif mp.decoded.portnum == portnums_pb2.POSITION_APP:
print("POSITION_APP")
pos = mesh_pb2.Position()
try:
pos.ParseFromString(mp.decoded.payload)
for l in str(pos).split("\n"):
print(f" {l}")
except Exception as e:
print(f"PROBLEM DECODING POSITION_APP {e}")
elif mp.decoded.portnum == portnums_pb2.ROUTING_APP:
print("ROUTING_APP")
route = mesh_pb2.Routing()
try:
route.ParseFromString(mp.decoded.payload)
for l in str(route).split("\n"):
print(f" {l}")
except Exception as e:
print(f"PROBLEM DECODING ROUTING_APP {e}")
sys.exit(0)
elif mp.decoded.portnum == portnums_pb2.TELEMETRY_APP:
print("TELEMETRY_APP")
telemetry = telemetry_pb2.Telemetry()
try:
telemetry.ParseFromString(mp.decoded.payload)
for l in str(telemetry).split("\n"):
print(f" {l}")
except Exception as e:
print(f"PROBLEM DECODING TELEMETRY_APP {e}")
# we could do more
# but for now...
except json.JSONDecodeError:
print(f"Error decoding JSON: {msg.payload}")
except Exception as e:
print(f"An error occurred: {e}")
port_dict_view = [ (v, k) for k, v in port_dict.items() ]
port_dict_view.sort(reverse=True)
for v, k in port_dict_view:
print(f"{app_dict[k]} ({k}): {v}")
print()
wrapper = textwrap.TextWrapper()
wrapper.initial_indent = " > "
client = mqtt.Client(protocol=mqtt.MQTTv5, callback_api_version=mqtt.CallbackAPIVersion.VERSION2)
client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
client.on_connect = on_connect
client.on_message = on_message
client.connect(MQTT_BROKER, MQTT_PORT, 60)
client.loop_forever()
It’s not perfect, or elegant, but embodies some of the very basics you’d need to receive and decode messages from the MQTT. I let it run for an hour late on Sunday night, and it recorded 791 messages to the MQTT server. They were broken down into six different “applications”, with the following distribution.
NODEINFO_APP (4): 318
TELEMETRY_APP (67): 281
POSITION_APP (3): 148
NEIGHBORINFO_APP (71): 28
MAP_REPORT_APP (73): 13
STORE_FORWARD_APP (65): 3
NODEINFO_APP packets are used to transmit information about individual nodes. Of the 318 sent, 96 distinct nodes were logged. Information for a (randomly chosen) node might look like this:
NODEINFO_APP
id: "!336a194c"
long_name: "Capt Amesha"
short_name: "CA"
macaddr: "d\3503j\031L"
hw_model: HELTEC_V3
role: ROUTER
Scanning the log, it appears that 42 of such nodes are configured as having a role of ROUTER, which seems… interesting.
Further scraping that information yields the following breakdown on types of hardware used:
94 hw_model: RAK4631
54 hw_model: HELTEC_V3
42 hw_model: T_ECHO
34 hw_model: STATION_G2
33 hw_model: TBEAM
12 hw_model: HELTEC_WSL_V3
10 hw_model: HELTEC_V2_1
8 hw_model: T_DECK
8 hw_model: LILYGO_TBEAM_S3_CORE
6 hw_model: HELTEC_WIRELESS_PAPER
5 hw_model: TLORA_T3_S3
4 hw_model: PORTDUINO
3 hw_model: TRACKER_T1000_E
2 hw_model: SEEED_XIAO_S3
2 hw_model: DIY_V1
1 hw_model: TLORA_V2_1_1P6
Not sure what to make of this data yet, but at least I can gather it and use it to think more about the state of the network and how its configured, including total number of messages sent and the locations of nodes, with the possibility of finding misconfigured nodes.
I probably will continue to tinker with this, most basically by creating an sqlite database to log all this information and allow more general queries over time.
And I will probably work on getting a solar powered node at the top of the hill on my property, in the hope of creating a resource for my neighborhood.
All for now, hope you all are having a good December. Feel free to comment if you have any hints/suggestions.
I suspect the world would be better if that percentage were even greater.
Apparently 15% of all web traffic is cat related. There's no reason for Brainwagon be any different.
Thanks Mal! I'm trying to reclaim the time that I was using doom scrolling and writing pointless political diatribes on…
Brainwagons back! I can't help you with a job, not least because I'm on the other side of our little…
Congrats, glad to hear all is well.