meshttpd/meshttpd.py

324 lines
34 KiB
Python
Raw Normal View History

2024-05-08 22:02:07 +08:00
import cherrypy
import threading
import time
from pubsub import pub
import meshtastic
import meshtastic.tcp_interface
import json
import datetime
import hashlib
import random
2024-05-08 22:02:07 +08:00
class MeshAPI(object):
def __init__(self, hostname='meshtastic.local'):
"""
Initialize the MeshAPI.
Args:
hostname (str): The hostname or IP address of the Meshtastic device.
"""
self.hostname = hostname
self.iface = None
self.device_telemetry_cache = {} # Cache to store device telemetry data
self.environment_telemetry_cache = {} # Cache to store environment telemetry data
self.message_cache = {} # Cache to store messages
self.seen_nodes = {} # Dictionary to store information about seen nodes
self.connection_attempts = 0 # Number of connection attempts made
self.last_connection_time = None # Time of the last successful connection
2024-05-08 22:02:07 +08:00
self.connection_thread = threading.Thread(target=self.connect_to_mesh, daemon=True)
def on_receive(self, packet, interface):
"""
Callback function called when a packet arrives.
"""
# Debug
print(f"Received: from={packet.get('from')} to={packet.get('to')} portnum={packet['decoded'].get('portnum', 'UNKNOWN')}")
# Check if the received packet contains telemetry data
if 'decoded' in packet and 'telemetry' in packet['decoded']:
node_id = packet['from']
telemetry_data = packet['decoded']['telemetry']
# Check if telemetry data contains deviceMetrics
if 'deviceMetrics' in telemetry_data:
self.device_telemetry_cache[node_id] = {
"time": telemetry_data["time"],
"deviceMetrics": {
"batteryLevel": telemetry_data["deviceMetrics"].get("batteryLevel"),
"voltage": telemetry_data["deviceMetrics"].get("voltage"),
"channelUtilization": telemetry_data["deviceMetrics"].get("channelUtilization"),
"airUtilTx": telemetry_data["deviceMetrics"].get("airUtilTx")
}
}
# Check if telemetry data contains environmentMetrics
if 'environmentMetrics' in telemetry_data:
self.environment_telemetry_cache[node_id] = {
"time": telemetry_data["time"],
"environmentMetrics": {
"temperature": telemetry_data["environmentMetrics"].get("temperature"),
"relativeHumidity": telemetry_data["environmentMetrics"].get("relativeHumidity"),
"barometricPressure": telemetry_data["environmentMetrics"].get("barometricPressure")
}
}
# Check if the received packet is a message
if 'decoded' in packet and 'text' in packet['decoded']:
node_id = packet['from']
message_data = packet['decoded']['text']
internal_message_id = self.generate_internal_message_id(node_id, message_data)
self.message_cache[internal_message_id] = {"node_id": node_id, "message": message_data}
2024-05-08 22:02:07 +08:00
if len(self.message_cache) > 100:
self.message_cache.pop(0)
# Update seen nodes dictionary
if 'fromId' in packet:
node_id = packet['from']
long_id = packet['fromId']
if node_id not in self.seen_nodes:
self.seen_nodes[node_id] = {"long_id": long_id}
def on_connection(self, interface, topic=pub.AUTO_TOPIC):
"""
Callback function called when we (re)connect to the radio.
"""
self.last_connection_time = time.time()
self.connection_attempts += 1
print(f"Connected to Meshtastic device at {self.hostname}")
@cherrypy.expose
def index(self):
"""
Exposed endpoint for the index page.
2024-05-08 22:02:07 +08:00
"""
html = """
<html>
<head><title>Meshttpd Poor Man's Swagger</title></head>
<style>
body {
font-family: Arial, sans-serif;
background-image: url('data:image/png;base64,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
background-repeat: no-repeat;
background-position: right;
background-position-y: center;
background-position-y: 1px;
}
h1 {
color: #333;
}
ul {
list-style-type: none;
padding: 0;
}
li {
margin-bottom: 10px;
}
li a {
color: #007bff;
text-decoration: none;
}
</style>
<body>
<h1>Meshttpd Poor Man's Swagger</h1>
<h2>Endpoints:</h2>
<ul>
<li>
<a href="/api/mesh/send_message">/api/mesh/send_message</a>: POST endpoint to send a message
<ul>
<li>Parameters:
<ul>
<li><b>message</b> (str): The message to be sent.</li>
<li><b>node_id</b> (optional, str): The ID of the node to which the message should be sent.</li>
</ul>
</li>
</ul>
</li>
<li>
<a href="/api/mesh/get_device_telemetry">/api/mesh/get_device_telemetry</a>: GET endpoint to retrieve device telemetry data
</li>
<li>
<a href="/api/mesh/get_environment_telemetry">/api/mesh/get_environment_telemetry</a>: GET endpoint to retrieve environment telemetry data
</li>
<li>
<a href="/api/mesh/get_last_messages">/api/mesh/get_last_messages</a>: GET endpoint to retrieve the last cached messages
</li>
<li>
<a href="/api/mesh/delete_message">/api/mesh/delete_message</a>: POST endpoint to delete a message from the cache
<ul>
<li>Parameters:
<ul>
<li><b>message_id</b> (str): The ID of the message to be deleted.</li>
</ul>
</li>
</ul>
</li>
2024-05-08 22:02:07 +08:00
<li>
<a href="/api/mesh/nodes">/api/mesh/nodes</a>: GET endpoint to list all seen nodes
</li>
<li>
<a href="/api/mesh/status">/api/mesh/status</a>: GET endpoint to check connection status
</li>
</ul>
<p>Made by luhf for <a href="https://monocul.us">Monocul.us Mesh</a></p>
2024-05-08 22:02:07 +08:00
</body>
</html>
"""
return html
@cherrypy.expose
@cherrypy.tools.json_out()
def send_message(self, message=None, node_id=None):
"""
Exposed endpoint for sending a message.
Args:
message (str): The message to be sent.
node_id (optional, str): The ID of the node to which the message should be sent.
Returns:
dict: A JSON object containing the status of the operation.
Raises:
404 (Missing parameters): If the `message` parameter is missing.
400 (Invalid node ID): If the specified node ID is invalid.
"""
if message is None:
cherrypy.response.status = 404
return {"error": "Missing parameters: message"}
try:
if self.iface:
if node_id:
try:
self.iface.sendText(message, node_id)
except meshtastic.node_manager.NodeIdNotFound:
raise cherrypy.HTTPError(400, "Invalid node ID")
else:
self.iface.sendText(message)
return {"status": "success", "message": "Message sent successfully"}
else:
return {"status": "error", "message": "Mesh interface not initialized"}
except Exception as ex:
return {"status": "error", "message": str(ex)}
@cherrypy.expose
@cherrypy.tools.json_out()
def get_device_telemetry(self):
"""
Exposed endpoint for retrieving device telemetry data from connected nodes.
"""
return self.device_telemetry_cache
@cherrypy.expose
@cherrypy.tools.json_out()
def get_environment_telemetry(self):
"""
Exposed endpoint for retrieving environment telemetry data from connected nodes.
"""
return self.environment_telemetry_cache
@cherrypy.expose
@cherrypy.tools.json_out()
def get_last_messages(self):
"""
Exposed endpoint for retrieving the last X cached messages.
"""
return self.message_cache
@cherrypy.expose
@cherrypy.tools.json_out()
def delete_message(self, message_id=None):
"""
Exposed endpoint for deleting a message from the cache.
Args:
message_id (str): The ID of the message to be deleted.
Returns:
dict: A JSON object containing the status of the operation.
Raises:
404 (Missing parameters): If the `message_id` parameter is missing.
400 (Invalid message ID): If the specified message ID is invalid.
"""
if message_id is None:
cherrypy.response.status = 404
return {"error": "Missing parameters: message_id"}
if message_id not in self.message_cache:
raise cherrypy.HTTPError(400, "Invalid message ID")
del self.message_cache[message_id]
return {"status": "success", "message": "Message deleted successfully"}
2024-05-08 22:02:07 +08:00
@cherrypy.expose
@cherrypy.tools.json_out()
def nodes(self):
"""
Exposed endpoint for listing all seen nodes.
"""
return self.seen_nodes
@cherrypy.expose
@cherrypy.tools.json_out()
def status(self):
"""
Exposed endpoint for checking connection status.
"""
return {
"connected": bool(self.iface),
"last_connection_time": self.last_connection_time,
"total_connection_attempts": self.connection_attempts
}
def connect_to_mesh(self):
"""
Function to connect to the Meshtastic device.
"""
while True:
try:
self.iface = meshtastic.tcp_interface.TCPInterface(hostname=self.hostname)
return
except Exception as ex:
print(f"Error: Could not connect to {self.hostname} {ex}")
time.sleep(1) # Wait for 1 second before attempting to reconnect
def generate_internal_message_id(self, node_id, message):
"""
Function to generate an internal message ID using MD5 hashing of random data, node ID, and message.
Args:
node_id (str): The ID of the node.
message (str): The message.
Returns:
str: The generated internal message ID.
"""
random_data = str(random.random()).encode()
2024-05-08 23:52:14 +08:00
hash_input = random_data + str(node_id).encode() + message.encode()
hash_object = hashlib.md5(hash_input)
return hash_object.hexdigest()[:10]
2024-05-08 22:02:07 +08:00
def start(self):
"""
Function to start the connection thread.
"""
self.connection_thread.start()
if __name__ == '__main__':
# Change the IP address of the meshtastic device here.
# With some networks the local domain will not work, if that is your case use the IP address of the device.
mesh_api = MeshAPI(hostname='meshtastic.local')
# Start the connection thread
mesh_api.start()
# Subscribe to receive events
pub.subscribe(mesh_api.on_receive, "meshtastic.receive")
pub.subscribe(mesh_api.on_connection, "meshtastic.connection.established")
cherrypy.tree.mount(mesh_api, '/api/mesh')
#Please use a reverse proxy if you are planning to expose this to the internet!
#Set server.socket_host to 127.0.0.1
cherrypy.config.update({'server.socket_host': '0.0.0.0'})
cherrypy.engine.start()
cherrypy.engine.block()