Skip to main content

Adafruit IO

Adafruit IO for Meshtastic

Adafruit IO can be used to graph telemetry and messages from a Meshtastic network via json/mqtt. The following example script will listen for node packets and publish voltage, rssi, snr and messages to individual feeds on adafruit IO. If a feed doesn't exist it will be created. Be aware, however, that the free Adafruit account is limited to 10 feeds. Once your feeds are being populated with data you can create dashboards to display graphs and gauges with the data.

info

To utilize this script you need to have an Adafruit IO account, a working mqtt broker setup and a mesh node that is publishing to the mqtt broker.

You will need to modify the ini sample file with your Adafruit IO and mqtt credentials Code repository is here [https://bitbucket.org/tavdog/mesh_aio_logger/src/main/]

[GENERAL]
PRINT_CONFIG = false
; https://pynative.com/list-all-timezones-in-python/
TIMEZONE = US/Hawaii
CHANNEL_LIST = LongFast,Private

[MQTT]
SERVER = mqtt.server.net
PORT = 1883
USERNAME = a
PASSWORD = a

[AIO]
USER = a
KEY = a
;leave FEED_GROUP as Default is you don't want a separate group.
FEED_GROUP = Default
[LOG]
VOLTAGE = true
MESSAGE = true
POSITION = false
SNR = false
RSSI = false
# Persistent mqtt client, watch for voltage, telemetry, message packets and publish them to io

# Import Adafruit IO REST client.
from Adafruit_IO import Client, Feed, Data, RequestError
from datetime import datetime
import paho.mqtt.client as mqtt
import os
import sys
import json
import time
import pytz
import configparser

config = configparser.ConfigParser()
# Read the configuration file
config.read('config.ini')

if config.getboolean('GENERAL','PRINT_CONFIG'):
# Iterate through sections and options to print all config values
for section in config.sections():
print(f"[{section}]")
for key, value in config.items(section):
print(f"{key} = {value}")
print() # Add an empty line between sections for better readability

# set your timezone here
TIMEZONE = config['GENERAL']['TIMEZONE']
CHANNEL_LIST = config['GENERAL']['CHANNEL_LIST']
MQTT_SERVER = config['MQTT']['SERVER']
MQTT_PORT = int(config['MQTT']['PORT'])
MQTT_USERNAME = config['MQTT']['USERNAME']
MQTT_PASSWORD = config['MQTT']['PASSWORD']
AIO_USER = config['AIO']['USER']
AIO_KEY = config['AIO']['KEY']
AIO_FEED_GROUP = config['AIO']['FEED_GROUP'] # leave as Default is you don't want a separate group.
LOG_SNR = config.getboolean('LOG','SNR')
LOG_VOLTAGE = config.getboolean('LOG','VOLTAGE')
LOG_RSSI = config.getboolean('LOG','RSSI')
LOG_MESSAGE = config.getboolean('LOG','MESSAGE')
LOG_POSITION = config.getboolean('LOG','POSITION')

###### END SETTINGS ######

my_timezone = pytz.timezone(TIMEZONE)
channel_array = CHANNEL_LIST.split(',')
print("\n")
mqttClient = mqtt.Client("mesh_client_rssi")
mqttClient.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)

aio = Client(AIO_USER,AIO_KEY)

# bootstrap your node_db here if you want
#node_db = dict()
node_db = { 947782216: 'G1 Kula',
1839130823: 'HR-Haleakala Repeater',
1969840052: 'MR8-Magnet Rak 8dbi',
}

# get feed object or create it if it doesn't exist yet.
def get_feed(full_name,kind):
name = full_name.split('-')[0]
#print("getting feed: " + name)
if "\\x00" in name or "\\u0000" in name or "\x00" in name: # json don't like emojis
name = full_name.split('-')[1].replace(' ','-').replace('.','-')
try:
feed = aio.feeds(f"{AIO_FEED_GROUP}.{name.lower()}-{kind}")
except:
print("creating feed:" + f"{AIO_FEED_GROUP}.{name.lower()}-{kind}")
# Create Feed
new_feed = Feed(name=f"{name}_{kind}")
feed = aio.create_feed(feed=new_feed,group_key=AIO_FEED_GROUP)

return feed

def publish_rssi(data,metadata):
name = node_db[data['from']]
feed = get_feed(name,"rssi") # use a function here because we will create the feed if it doens't exist already.
#print(feed.key + " \t\t: " + str(data['rssi']))
aio.send_data(feed.key, data['rssi'],metadata)

def publish_snr(data,metadata):
name = node_db[data['from']]
feed = get_feed(name,"snr") # use a function here because we will create the feed if it doens't exist already.
#print(feed.key + " \t\t: " + str(data['snr']))
aio.send_data(feed.key, data['snr'],metadata)

def publish_voltage(data,metadata):
name = node_db[data['from']]
feed = get_feed(name,"voltage") # use a function here because we will create the feed if it doens't exist already. print(feed.key + " \t: " + str(data['payload'].get('voltage',"none")))
#print(feed.key + " \t: " + str(data['payload']['voltage']))
aio.send_data(feed.key, data['payload'].get('voltage',0),metadata)

def publish_packet(data):

feed = aio.feeds(AIO_FEED_GROUP+".messages")
if (data['from'] in node_db):
data['fname'] = node_db[data['from']]
if (data['to'] in node_db):
data['tname'] = node_db[data['to']]
# trim down the data. we really only want to see the message content
if 'stamp' in data:
stamp = datetime.fromtimestamp(data['timestamp'],my_timezone).strftime('%Y-%m-%d %H:%M:%S')
else:
current_time = datetime.now() # Assuming UTC time
# Convert to the desired timezone (e.g., 'America/Los_Angeles' or your preferred timezone)
current_time = current_time.astimezone(my_timezone)
stamp = current_time.strftime('%Y-%m-%d %H:%M:%S')

trimmed = {
'from' : data.get('fname',None) or data.get('from'),
'to' : data.get('tname',None) or data.get('to'),
'channel' : data['channel'],
'message' : data['payload'],
'stamp' : stamp,
}
aio.send_data(feed.key, json.dumps(trimmed, indent=4))
print(trimmed)

def on_message(client, userdata, message):
try:
data = json.loads(str(message.payload.decode("utf-8")))
except Exception as e:
print(e)
return

# check the topic of the message
if data['type'] == "text" and LOG_MESSAGE:
# publish all message packets to the message log
print(data)
try:
publish_packet(data)
except Exception as e:
print("error in publish:",e)

# update node_db if needed
if data['type'] == 'nodeinfo':
# add to the node_db if we haven't seen it before
if data['from'] not in node_db:
node_db[data['from']] = data['payload']['shortname'] + '-' + data['payload']['longname']
print(str(node_db))
return # don't publish data from nodedb packets.

# "payload":{"altitude":113,"latitude_i":208759687,"longitude_i":-1565037665
metadata = None
if data['type'] == 'position' and LOG_POSITION:
metadata = {
'lat': data['payload']['latitude_i'] / 10000000, #40.726190,
'lon': data['payload']['longitude_i'] / 10000000, #-74.005334,
'ele': data['payload'].get('altitude',None),
'created_at': str(datetime.now(my_timezone)),
}
# add to the node_db if we know who they are already

# if metadata:
# print(metadata)

if data['from'] in node_db:
try:
if LOG_RSSI and 'rssi' in data and data['rssi'] != 0:
publish_rssi( data, metadata )
if LOG_SNR and 'snr' in data and data['snr'] != 0:
publish_snr( data, metadata )
if LOG_VOLTAGE and 'payload' in data and 'voltage' in data['payload'] and data['payload'].get('voltage',0) != 0:
publish_voltage( data, metadata )
pass
except Exception as e:
print("Error sending to IO:", str(e))


mqttClient.on_message = on_message

#def on_log(client, userdata, level, buf):
#print("log: ",buf)

#mqttClient.on_log=on_log

while(True):
if (not mqttClient.is_connected()) :
print("Connecting to mqtt server")
mqttClient.connect(MQTT_SERVER, MQTT_PORT)
mqttClient.loop_start()
for channel in channel_array:
print("Subscribing to %s" % channel)
mqttClient.subscribe("mesh/2/json/%s/#" % (channel))
time.sleep(1)

time.sleep(.01)