Repository navigation
Expand file tree
/
Copy pathapp.py
More file actions
391 lines (354 loc) · 16.6 KB
/
Copy pathapp.py
File metadata and controls
391 lines (354 loc) · 16.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
from flask import Flask, render_template, jsonify
from flask_socketio import SocketIO, emit
import meshtastic
import meshtastic.serial_interface
from pubsub import pub
import subprocess
from database import init_database, save_message, get_recent_messages, get_message_count, upsert_node, get_all_nodes, get_node
app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*")
# Initialize database
init_database()
# Connect to your meshtastic device
interface = None
def on_receive(packet, interface):
"""Called when a message is received"""
if 'decoded' in packet and 'text' in packet['decoded']:
channel_index = packet.get('channel', 0)
message_data = {
'from': packet['from'],
'to': packet['to'],
'text': packet['decoded']['text'],
'time': packet.get('rxTime', 'unknown'),
'channel': channel_index
}
# Debug: print packet keys to see what info is available
print(f"DEBUG: Packet keys: {packet.keys()}", flush=True)
if 'fromId' in packet:
print(f"DEBUG: fromId = {packet.get('fromId')}", flush=True)
if 'from' in packet:
print(f"DEBUG: from = {packet.get('from')}", flush=True)
# Save sender's node info if available
from_node_num = message_data['from']
print(f"DEBUG: interface.nodes has {len(interface.nodes) if interface and interface.nodes else 0} nodes", flush=True)
if interface and interface.nodes and from_node_num in interface.nodes:
node = interface.nodes[from_node_num]
user_obj = node.get('user')
if user_obj:
upsert_node(
from_node_num,
user_obj.get('shortName'),
user_obj.get('longName'),
user_obj.get('id')
)
else:
# Node not in interface.nodes - save what we know from the packet
# This ensures we at least have the node_id (fromId) for later lookup
from_id = packet.get('fromId')
if from_id:
print(f"DEBUG: Saving unknown node {from_node_num} with fromId {from_id}", flush=True)
upsert_node(
from_node_num,
None, # shortName unknown
None, # longName unknown
from_id
)
# Save to database
if message_data['time'] != 'unknown':
save_message(
from_node=message_data['from'],
to_node=message_data['to'],
text=message_data['text'],
timestamp=message_data['time'],
channel_index=channel_index
)
# Send to all connected web clients
socketio.emit('new_message', message_data)
print(f"Received: {message_data}")
# Speak the message aloud using gspeak (Google TTS)
# Skip MediumFast channel (channelIndex 0 / broadcast to 0xffffffff)
# Only speak DMs and other channels
is_broadcast = message_data['to'] == 4294967295 # 0xffffffff
channel_index = packet.get('channel', 0)
# Don't speak if it's the MediumFast channel (channel 0 broadcast)
if not (is_broadcast and channel_index == 0):
try:
# Get sender's short name (4 letters/numbers)
sender_name = None
# First try live node data
print(f"TTS: Looking up node {from_node_num}", flush=True)
if interface and interface.nodes and from_node_num in interface.nodes:
node = interface.nodes[from_node_num]
user_obj = node.get('user')
print(f"TTS: Found in interface.nodes, user_obj = {user_obj}", flush=True)
if user_obj and user_obj.get('shortName'):
sender_name = user_obj.get('shortName')
print(f"TTS: Got shortName from live data: {sender_name}", flush=True)
else:
print(f"TTS: Node {from_node_num} not in interface.nodes", flush=True)
# Fallback to database if not found
if not sender_name:
db_node = get_node(from_node_num)
print(f"TTS: Database lookup result: {db_node}", flush=True)
if db_node and db_node.get('shortName'):
sender_name = db_node.get('shortName')
print(f"TTS: Got shortName from database: {sender_name}", flush=True)
# Fallback to last 4 chars of fromId (e.g., "!a3059abc" -> "9abc")
if not sender_name and packet.get('fromId'):
from_id = packet.get('fromId')
sender_name = from_id[-4:] # Last 4 characters
print(f"TTS: Using last 4 chars of fromId: {sender_name}", flush=True)
# Last resort: use node number
if not sender_name:
sender_name = str(from_node_num)
print(f"TTS: Falling back to node number: {sender_name}", flush=True)
# Add periods between characters so TTS spells out each one clearly
sender_name_spaced = '. '.join(sender_name.upper())
# Replace emoticons with speakable text
message_text = message_data['text']
message_text = message_text.replace(':)', 'smiley face')
message_text = message_text.replace(':(', 'sad face')
message_text = message_text.replace(':D', 'big smile')
message_text = message_text.replace(';)', 'wink')
message_text = message_text.replace('<3', 'heart')
print(f"TTS: Speaking with sender_name_spaced = '{sender_name_spaced}'")
text_to_speak = f"incoming message from {sender_name_spaced}. {message_text}"
subprocess.run(['gspeak', text_to_speak], check=False)
except Exception as e:
print(f"Error speaking message: {e}")
@app.route('/')
def index():
return render_template('index.html')
@socketio.on('connect')
def handle_connect(auth=None):
global interface
if interface is None:
print("Initializing meshtastic interface...")
# Subscribe to message receive events
pub.subscribe(on_receive, "meshtastic.receive")
# Connect to device (auto-finds USB device)
try:
interface = meshtastic.serial_interface.SerialInterface()
print(f"Meshtastic interface connected: {interface}")
except Exception as e:
print(f"Error connecting to meshtastic: {e}")
print('Client connected')
# Send initial node list
if interface:
send_node_list()
send_channel_list()
# Send message history
send_message_history()
# Send our own node number
send_my_node_number()
@socketio.on('get_my_node_number')
def send_my_node_number():
"""Send our own node number to the client"""
try:
if interface and interface.myInfo:
my_node_num = interface.myInfo.my_node_num
emit('my_node_number', {'node_number': my_node_num})
print(f"Sent my node number: {my_node_num}")
else:
emit('my_node_number', {'node_number': None})
except Exception as e:
print(f"Error sending node number: {e}")
emit('my_node_number', {'node_number': None})
@socketio.on('get_message_history')
def send_message_history(limit=100):
"""Send message history to client"""
try:
all_messages = get_recent_messages(limit=limit, is_dm=None)
channel_messages = get_recent_messages(limit=limit, is_dm=False)
dm_messages = get_recent_messages(limit=limit, is_dm=True)
# Enrich messages with node information from database
stored_nodes = get_all_nodes()
for msg in all_messages + channel_messages + dm_messages:
msg['channel_index'] = msg.get('channel_index', 0)
# Add sender/recipient node info if available
from_node = stored_nodes.get(msg['from'])
if from_node:
msg['from_name'] = from_node.get('shortName')
msg['from_long_name'] = from_node.get('longName')
msg['from_id'] = from_node.get('id')
to_node = stored_nodes.get(msg['to'])
if to_node:
msg['to_name'] = to_node.get('shortName')
msg['to_long_name'] = to_node.get('longName')
msg['to_id'] = to_node.get('id')
emit('message_history', {
'all': all_messages,
'channel': channel_messages,
'dms': dm_messages,
'total_count': get_message_count()
})
print(f"Sent message history: {len(all_messages)} total messages")
except Exception as e:
print(f"Error sending message history: {e}")
emit('message_history', {
'all': [],
'channel': [],
'dms': [],
'total_count': 0
})
@socketio.on('get_channels')
def send_channel_list():
"""Send list of configured channels to client"""
if interface and interface.localNode:
channels = []
try:
# Get channel configuration from local node
channel_config = interface.localNode.channels
for i, channel in enumerate(channel_config):
if channel and hasattr(channel, 'settings'):
settings = channel.settings
channel_info = {
'index': i,
'name': settings.name if settings.name else f'Channel {i}',
'role': str(channel.role) if hasattr(channel, 'role') else 'DISABLED'
}
# Only include enabled channels
if channel_info['role'] != 'DISABLED':
channels.append(channel_info)
emit('channel_list', {'channels': channels})
print(f"Sent {len(channels)} channels to client: {channels}")
except Exception as e:
print(f"Error getting channels: {e}")
emit('channel_list', {'channels': []})
else:
emit('channel_list', {'channels': []})
@socketio.on('get_nodes')
def send_node_list():
"""Send list of visible nodes to client, merging with stored nodes from database"""
try:
# Start with nodes from database (includes offline nodes)
stored_nodes = get_all_nodes()
# Update with currently visible nodes from interface
# Create a copy to avoid "dictionary changed size during iteration" error
if interface and interface.nodes:
nodes_snapshot = dict(interface.nodes.items())
# Process nodes and save to database if new or changed
for node_id, node in nodes_snapshot.items():
# Extract user data
user_obj = node.get('user')
if user_obj:
node_num = node['num']
short_name = user_obj.get('shortName')
long_name = user_obj.get('longName')
node_id_str = user_obj.get('id')
# Check if this node is new or has changed BEFORE updating stored_nodes
existing = stored_nodes.get(node_num)
if not existing or (
existing.get('shortName') != short_name or
existing.get('longName') != long_name or
existing.get('id') != node_id_str
):
try:
upsert_node(node_num, short_name, long_name, node_id_str)
except Exception as e:
print(f"Error saving node {node_num}: {e}")
# Update stored_nodes with current info (for sending to client)
stored_nodes[node_num] = {
'shortName': short_name,
'longName': long_name,
'id': node_id_str
}
# Convert to list format for sending to client
nodes = []
for node_num, node_data in stored_nodes.items():
node_info = {
'num': node_num,
'user': {
'shortName': node_data.get('shortName'),
'longName': node_data.get('longName'),
'id': node_data.get('id')
}
}
nodes.append(node_info)
emit('node_list', {'nodes': nodes})
print(f"Sent {len(nodes)} nodes to client (including stored nodes)")
except Exception as e:
print(f"Error in send_node_list: {e}")
import traceback
traceback.print_exc()
# Send empty list on error
emit('node_list', {'nodes': []})
@socketio.on('send_message')
def handle_send(data):
"""Send a message through meshtastic"""
print(f"Received send_message event with data: {data}")
text = data.get('text', '')
recipient = data.get('recipient', 'channel:0') # 'channel:X' or node number
print(f"Text to send: '{text}' to {recipient}")
print(f"Interface status: {interface}")
if interface and text:
print(f"Attempting to send: '{text}' to {recipient}")
try:
# Get our own node number for the database
my_node_num = interface.myInfo.my_node_num if interface.myInfo else 0
import time
# Convert recipient to string if it's an integer
recipient_str = str(recipient) if isinstance(recipient, int) else recipient
if recipient_str.startswith('channel:'):
# Send to specific channel
channel_index = int(recipient_str.split(':')[1])
packet = interface.sendText(text, channelIndex=channel_index)
packet_id = packet.id if packet else None
print(f"Message sent to channel {channel_index} successfully! Packet ID: {packet_id}")
# Save to database
save_message(
from_node=my_node_num,
to_node=4294967295, # Broadcast
text=text,
timestamp=int(time.time()),
channel_index=channel_index
)
emit('message_sent', {'status': 'success', 'packet_id': packet_id})
else:
# Send DM to specific node with ACK request
def ack_handler(packet):
"""Handle ACK/NAK for this message"""
print(f"ACK/NAK received for packet {packet.get('id')}: {packet}")
# Determine if this is an ACK or NAK
routing = packet.get('routing', {})
error_reason = routing.get('errorReason', 'NONE')
if error_reason == 'NONE':
# ACK received
socketio.emit('message_ack', {
'packet_id': packet.get('id'),
'status': 'ack'
})
print(f"✓ ACK received for packet {packet.get('id')}")
else:
# NAK received
socketio.emit('message_ack', {
'packet_id': packet.get('id'),
'status': 'nak',
'error': error_reason
})
print(f"✗ NAK received for packet {packet.get('id')}: {error_reason}")
packet = interface.sendText(
text,
destinationId=int(recipient),
wantAck=True,
onResponse=ack_handler
)
packet_id = packet.id if packet else None
print(f"DM sent to node {recipient} successfully! Packet ID: {packet_id}, waiting for ACK...")
# Save to database
save_message(
from_node=my_node_num,
to_node=int(recipient),
text=text,
timestamp=int(time.time()),
channel_index=0
)
emit('message_sent', {'status': 'success', 'packet_id': packet_id})
except Exception as e:
print(f"Error sending message: {e}")
emit('message_sent', {'status': 'error', 'message': str(e)})
else:
print(f"Cannot send - interface: {interface}, text: '{text}'")
emit('message_sent', {'status': 'error'})
if __name__ == '__main__':
socketio.run(app, host='0.0.0.0', port=5000, allow_unsafe_werkzeug=True)