adafruit-io

Adafruit IO для Meshtastic

Adafruit IO можно использовать для построения графиков телеметрии и сообщений из сети Meshtastic через json/mqtt. Следующий пример скрипта будет прослушивать пакеты узлов и публиковать напряжение, rssi, snr и сообщения в отдельные фиды на Adafruit IO. Если фид не существует, он будет создан. Однако имейте в виду, что бесплатный аккаунт Adafruit ограничен 10 фидами. После того как ваши фиды начнут заполняться данными, вы можете создать панели для отображения графиков и индикаторов с данными.

:::info

Чтобы использовать этот скрипт, вам нужен аккаунт Adafruit IO, настроенный брокер MQTT и узел mesh, который публикует данные в брокер MQTT.

:::

Вам потребуется изменить образец ini-файла с вашими учетными данными Adafruit IO и MQTT
Репозиторий кода находится здесь [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

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 doesn'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 doesn'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 doesn'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)