Recreating broken commits
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
logs/
|
||||
meshtastic/
|
||||
.vscode/
|
||||
__pycache__/
|
||||
119
MeshtasticDataClasses.py
Normal file
119
MeshtasticDataClasses.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
@dataclass
|
||||
class Message:
|
||||
portnum: str
|
||||
payload: str
|
||||
text: str
|
||||
bitfield: Optional[str] = None
|
||||
replyId: Optional[str] = None
|
||||
emoji: Optional[str] = None
|
||||
|
||||
@dataclass
|
||||
class RxPacket:
|
||||
_from: int
|
||||
to: int
|
||||
decoded: Message
|
||||
id: int
|
||||
rxTime: int
|
||||
rxSnr: float
|
||||
rxRssi: float
|
||||
hopStart: int
|
||||
raw: str
|
||||
fromId: str
|
||||
toId: str
|
||||
hopLimit: Optional[int] = None
|
||||
channel: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Position:
|
||||
latitudeI: int
|
||||
longitudeI: int
|
||||
altitude: float
|
||||
time: int
|
||||
locationSource: str
|
||||
PDOP: int
|
||||
groundSpeed: int
|
||||
groundTrack: int
|
||||
satsInView: int
|
||||
precisionBits: int
|
||||
raw: str
|
||||
latitude: float
|
||||
longitude: float
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
id: str
|
||||
longName: str
|
||||
shortName: str
|
||||
macaddr: str
|
||||
hwModel: str
|
||||
publicKey: str
|
||||
role: Optional[str] = None
|
||||
raw: Optional[str] = None
|
||||
|
||||
@dataclass
|
||||
class DeviceMetrics:
|
||||
batteryLevel: float
|
||||
voltage: float
|
||||
channelUtilization: float
|
||||
airUtilTx: float
|
||||
uptimeSeconds: int
|
||||
|
||||
@dataclass
|
||||
class NodeData:
|
||||
num: int
|
||||
user: User
|
||||
snr: float
|
||||
lastHeard: int
|
||||
hopsAway: int
|
||||
deviceMetrics: Optional[DeviceMetrics] = None
|
||||
position: Optional[Position] = None
|
||||
lastReceived: Optional[str] = None
|
||||
hopLimit: Optional[int] = None
|
||||
|
||||
def to_rx_packet(rawPacket: dict) -> RxPacket:
|
||||
if "from" in rawPacket:
|
||||
rawPacket["_from"] = rawPacket.pop("from")
|
||||
|
||||
parsed_packet = RxPacket(**rawPacket)
|
||||
parsed_packet.decoded = Message(**parsed_packet.decoded)
|
||||
return parsed_packet
|
||||
|
||||
def to_node_data(rawNode: dict) -> NodeData:
|
||||
# parse the dictionary into a dataclass
|
||||
parsed_node = NodeData(**rawNode)
|
||||
parsed_node.user = User(**parsed_node.user)
|
||||
parsed_node.deviceMetrics = DeviceMetrics(**parsed_node.deviceMetrics)
|
||||
return parsed_node
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_packet = {
|
||||
'from': 541024136,
|
||||
'to': 4294967295,
|
||||
'channel': 1,
|
||||
'decoded':
|
||||
{
|
||||
'portnum': 'TEXT_MESSAGE_APP',
|
||||
'payload': b'Test',
|
||||
'bitfield': 0,
|
||||
'text': 'Test'
|
||||
},
|
||||
'id': 1489259583,
|
||||
'rxTime': 1743434610,
|
||||
'rxSnr': 6.25,
|
||||
'hopLimit': 3,
|
||||
'rxRssi': -32,
|
||||
'hopStart': 3,
|
||||
'raw':"",
|
||||
'fromId':"",
|
||||
'toId':"",
|
||||
}
|
||||
|
||||
test = to_rx_packet(test_packet)
|
||||
print(test)
|
||||
|
||||
|
||||
|
||||
73
MeshtasticLogger.py
Normal file
73
MeshtasticLogger.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import meshtastic
|
||||
import meshtastic.serial_interface
|
||||
from pubsub import pub
|
||||
from MeshtasticDataClasses import RxPacket,to_rx_packet
|
||||
from MeshtasticDataClasses import NodeData,to_node_data
|
||||
from datetime import datetime
|
||||
|
||||
class MeshtasticLogger:
|
||||
def __init__(self, interface: meshtastic.serial_interface.SerialInterface, log_path : str, channel : int = 1, message_received_callback = None):
|
||||
# Subscribe to the recieve text event topic
|
||||
pub.subscribe(self.onReceive, "meshtastic.receive.text")
|
||||
self.interface = interface
|
||||
self.channel: int = channel
|
||||
self.log_path: str = log_path
|
||||
self.message_received_callback = message_received_callback
|
||||
|
||||
def _cap_string_length_bytes(self, message : str, max_bytes: int):
|
||||
'''
|
||||
Cap the length of a string to be under max_bytes. It will just drop the excess from the string.
|
||||
'''
|
||||
capped_message = ""
|
||||
capped_message_byte_length = 0
|
||||
for char in message:
|
||||
next_char_byte_size = len(char.encode('utf-8'))
|
||||
if capped_message_byte_length + next_char_byte_size <= max_bytes:
|
||||
capped_message_byte_length += next_char_byte_size
|
||||
capped_message += char
|
||||
return capped_message
|
||||
|
||||
def _log(self, message: str):
|
||||
print(message)
|
||||
with open(self.log_path, "a") as file:
|
||||
file.write(message)
|
||||
|
||||
def send(self, message: str):
|
||||
self.interface.sendText(message, channelIndex=self.channel)
|
||||
timestamp: str = datetime.now().strftime("%m-%d %I:%M:%S %p")
|
||||
self._log(f"{timestamp} You on channel {self.channel}: {message}\n")
|
||||
|
||||
|
||||
def onReceive(self, packet: dict, interface: meshtastic.serial_interface.SerialInterface):
|
||||
# parse the packet we recieved into a data class so we have nice type hints
|
||||
try:
|
||||
parsed_packet: RxPacket = to_rx_packet(packet)
|
||||
except Exception as e:
|
||||
self._log(e)
|
||||
return
|
||||
|
||||
# get the current time
|
||||
timestamp: str = datetime.now().strftime("%m-%d %I:%M:%S %p")
|
||||
|
||||
# try to grab the sender's ID. Otherwise it will just be unkown sender
|
||||
sender_node_name = "Unkown Sender"
|
||||
if parsed_packet.fromId in interface.nodes:
|
||||
try:
|
||||
sender_node: NodeData = to_node_data(interface.nodes[parsed_packet.fromId])
|
||||
except Exception as e:
|
||||
self._log(e)
|
||||
return
|
||||
sender_node_name = sender_node.user.longName
|
||||
|
||||
# construct and _log the message
|
||||
message = f"{timestamp} {sender_node_name}: On Channel {parsed_packet.channel}, message: {parsed_packet.decoded.text}\n"
|
||||
self._log(message)
|
||||
|
||||
# parrot out the message but make sure the message length doesn't go above 233 bytes
|
||||
if parsed_packet.channel == 1:
|
||||
print("Parroting message")
|
||||
interface.sendText(self._cap_string_length_bytes(message, 233), channelIndex=self.channel)
|
||||
|
||||
if not self.message_received_callback is None:
|
||||
self.message_received_callback(message)
|
||||
|
||||
14
MeshtasticLogger.service
Normal file
14
MeshtasticLogger.service
Normal file
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=A simple chat interface and autoresponder for meshtastic
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/home/quinn/Projects/meshtastic/
|
||||
ExecStart=sudo /home/quinn/Projects/meshtastic/meshtastic/bin/python -m flask --app /home/quinn/Projects/meshtastic/app.py run --host=0.0.0.0 --port=80
|
||||
Restart=always
|
||||
User=quinn
|
||||
StandardOutput=append:/home/quinn/Projects/meshtastic/logs/meshtastic_system.log
|
||||
StandardError=append:/home/quinn/Projects/meshtastic/logs/meshtastic_system_error.log
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
23
README.md
Normal file
23
README.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Create a Virtual Environment
|
||||
Create the virtual environment and activate it
|
||||
`python -m venv meshtastic && source meshtastic/bin/activate`
|
||||
|
||||
Install the requirements
|
||||
|
||||
pip install requirements.txt
|
||||
|
||||
# Deploying Service Script Changes
|
||||
Type the following commands:
|
||||
|
||||
`sudo cp /home/quinn/Projects/meshtastic/MeshtasticLogger.service /etc/systemd/system/ && sudo systemctl daemon-reload`
|
||||
|
||||
To start the service run
|
||||
`sudo systemctl start MeshtasticLogger`
|
||||
|
||||
You can get the status of the service by running:
|
||||
|
||||
`sudo systemctl status MeshtasticLogger`
|
||||
|
||||
And you can stop the service by running:
|
||||
|
||||
`sudo systemctl stop MeshtasticLogger`
|
||||
89
app.py
Normal file
89
app.py
Normal file
@@ -0,0 +1,89 @@
|
||||
from flask import Flask, render_template, request, jsonify
|
||||
import meshtastic
|
||||
from MeshtasticLogger import MeshtasticLogger
|
||||
import os
|
||||
|
||||
messages = []
|
||||
app = Flask(__name__)
|
||||
interface = meshtastic.serial_interface.SerialInterface()
|
||||
log_path = "logs/mesh_logs.log"
|
||||
|
||||
try:
|
||||
with open(log_path, 'r') as file:
|
||||
for line in file:
|
||||
messages.append(line)
|
||||
except FileNotFoundError:
|
||||
# make the log directory if it isn't already there
|
||||
with open(log_path, "w") as file:
|
||||
file.write("Begin logs")
|
||||
|
||||
def web_print(message: str):
|
||||
with app.app_context():
|
||||
print(message)
|
||||
messages.append(message)
|
||||
get_messages()
|
||||
|
||||
def callback(message: str):
|
||||
web_print(message)
|
||||
|
||||
def help_response():
|
||||
help_message: str = "Commands:\nhelp - show this message\clear - clear the text log\nsend <text> - sends text to all channels\nchannel <number> - sets the current channel (0-7)\n"
|
||||
web_print(help_message)
|
||||
|
||||
logger = MeshtasticLogger(interface, "logs/mesh_logs.log", channel=1, message_received_callback=callback)
|
||||
|
||||
def channel_response(args: list[str]):
|
||||
if(len(args) < 2):
|
||||
web_print(f"Current channel number: {logger.channel}")
|
||||
return
|
||||
channel: int = int(args[1])
|
||||
if channel > 7 or channel < 0:
|
||||
web_print("Channel must be between 0 and 7")
|
||||
else:
|
||||
try:
|
||||
logger.channel = channel
|
||||
except Exception as e:
|
||||
web_print(e)
|
||||
else:
|
||||
web_print(f"Channel set to {channel}")
|
||||
|
||||
def send_response(args: list[str], command: str):
|
||||
if len(args) < 2:
|
||||
web_print("Please provide text to send")
|
||||
else:
|
||||
logger.send(command[5:])
|
||||
web_print(f"Sent: {command[5:]}")
|
||||
|
||||
def parse_command(command):
|
||||
args = command.split(" ")
|
||||
if len(args) < 1:
|
||||
return True
|
||||
|
||||
if args[0] == "clear":
|
||||
messages.clear()
|
||||
elif args[0] == "help":
|
||||
help_response()
|
||||
elif args[0] == "channel":
|
||||
channel_response(args)
|
||||
elif args[0] == "send":
|
||||
send_response(args, command)
|
||||
else:
|
||||
web_print("Command not recognized. Type \'help\' for a list of commands.")
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/send', methods=['POST'])
|
||||
def send():
|
||||
data = request.json
|
||||
message = data.get("message", "")
|
||||
parse_command(message)
|
||||
return jsonify({"messages": messages})
|
||||
|
||||
@app.route('/messages', methods=['GET'])
|
||||
def get_messages():
|
||||
return jsonify({"messages": messages})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=80, debug=True)
|
||||
28
requirements.txt
Normal file
28
requirements.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
argcomplete==3.6.1
|
||||
bleak==0.22.3
|
||||
blinker==1.9.0
|
||||
certifi==2025.1.31
|
||||
charset-normalizer==3.4.1
|
||||
click==8.1.8
|
||||
dbus-fast==2.44.0
|
||||
dotmap==1.3.30
|
||||
Flask==3.1.0
|
||||
idna==3.10
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.6
|
||||
MarkupSafe==3.0.2
|
||||
meshtastic==2.6.0
|
||||
packaging==24.2
|
||||
print-color==0.4.6
|
||||
protobuf==6.30.2
|
||||
Pypubsub==4.0.3
|
||||
PyQRCode==1.2.1
|
||||
pyserial==3.5
|
||||
pytap2==2.3.0
|
||||
PyYAML==6.0.2
|
||||
requests==2.32.3
|
||||
tabulate==0.9.0
|
||||
typing_extensions==4.13.0
|
||||
urllib3==2.3.0
|
||||
wcwidth==0.2.13
|
||||
Werkzeug==3.1.3
|
||||
101
templates/index.html
Normal file
101
templates/index.html
Normal file
@@ -0,0 +1,101 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Simple Chat</title>
|
||||
<style>
|
||||
#inputBox { width: 80%; }
|
||||
#sendButton { width: 18%; }
|
||||
#displayBox { width: 100%; height: 200px; margin-top: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<input type="text" id="inputBox" placeholder="Enter message">
|
||||
<button id="sendButton">Send</button>
|
||||
<textarea id="displayBox" readonly></textarea>
|
||||
|
||||
<script>
|
||||
document.getElementById('sendButton').addEventListener('click', sendMessage);
|
||||
document.getElementById('inputBox').addEventListener('keypress', function(event) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault(); // Prevents form submission if inside a form
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
function sendMessage() {
|
||||
let message = document.getElementById('inputBox').value;
|
||||
fetch('/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message: message })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('displayBox').value = data.messages.join('\n');
|
||||
document.getElementById('inputBox').value = '';
|
||||
displayBox.scrollTop = displayBox.scrollHeight; // auto-scroll to bottom
|
||||
});
|
||||
}
|
||||
|
||||
function fetchMessages() {
|
||||
fetch('/messages')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
let displayBox = document.getElementById('displayBox');
|
||||
let newMessages = data.messages.join('\n');
|
||||
|
||||
if (displayBox.value !== newMessages) { // Only update if there's a change
|
||||
displayBox.value = newMessages;
|
||||
displayBox.scrollTop = displayBox.scrollHeight; // Auto-scroll to bottom
|
||||
}
|
||||
});
|
||||
}
|
||||
// Poll every second for new messages
|
||||
setInterval(fetchMessages, 1000);
|
||||
</script>
|
||||
</body>
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
#chatContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#inputRow {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#inputBox {
|
||||
flex-grow: 0;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#sendButton {
|
||||
padding: 10px 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#displayBox {
|
||||
flex-grow: 1; /* Takes up the remaining space */
|
||||
width: 100%;
|
||||
resize: none; /* Prevents manual resizing */
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
overflow-y: auto; /* Enables scrolling */
|
||||
}
|
||||
</style>
|
||||
</html>
|
||||
Reference in New Issue
Block a user