Compare commits

...

31 Commits

Author SHA1 Message Date
SpudGunMan
6b7d795a31 Update README.md 2025-10-13 10:04:13 -07:00
SpudGunMan
1f093c4bc2 Update system.py 2025-10-13 10:02:22 -07:00
SpudGunMan
fe1c4a1ad0 Update locationdata.py 2025-10-13 10:02:20 -07:00
SpudGunMan
11687cb7ba ‼️UPDATE LOCATION🗺️
this is a fail safe change to fuzzing the default location. This may change the way you use the bot today and should evaluate the change specifically test the auto alerts for proper data for emergency alerts etc.`fuzzConfigLocation = True`
2025-10-13 09:49:10 -07:00
SpudGunMan
b07a7fb0cc Update radio.py 2025-10-13 08:40:21 -07:00
SpudGunMan
b876d87ba9 enhance 2025-10-13 08:38:27 -07:00
SpudGunMan
0a63e89633 waitTooLong!
haha I well sorry
2025-10-13 08:23:07 -07:00
SpudGunMan
848f5609c2 Update README.md 2025-10-12 23:22:33 -07:00
SpudGunMan
0ccbed6165 fix Lemons 2025-10-12 23:19:08 -07:00
SpudGunMan
646517db71 Update mesh_bot.py 2025-10-12 21:27:14 -07:00
SpudGunMan
7d347bb80a enhance 2025-10-12 21:24:58 -07:00
SpudGunMan
e199d4f5eb Update mesh_bot.py 2025-10-12 20:03:03 -07:00
SpudGunMan
a9767b58c4 Update mesh_bot.py 2025-10-12 20:00:22 -07:00
SpudGunMan
69dfde047e Update lemonade.py 2025-10-12 20:00:20 -07:00
SpudGunMan
da33b6f1b9 Update dopewar.py 2025-10-12 19:55:43 -07:00
SpudGunMan
8a7125358b Update lemonade.py 2025-10-12 18:23:14 -07:00
SpudGunMan
ae558052f7 hey chirpy
vox trapping
2025-10-12 18:17:05 -07:00
SpudGunMan
5074d71eb7 defaults 2025-10-12 17:22:02 -07:00
SpudGunMan
632f42477a Update settings.py 2025-10-12 17:18:44 -07:00
SpudGunMan
b3df38d15e Update radio.py
aaarg
2025-10-12 17:17:31 -07:00
SpudGunMan
b76b8ca718 Update radio.py 2025-10-12 17:17:02 -07:00
SpudGunMan
d66a9e745b enhance 2025-10-12 17:13:41 -07:00
SpudGunMan
717bbccea3 Omg 2025-10-12 16:25:43 -07:00
SpudGunMan
50fd1c0410 Update tictactoe.py 2025-10-12 16:22:25 -07:00
SpudGunMan
ae89788ea4 Update settings.py 2025-10-12 15:33:02 -07:00
SpudGunMan
4220b095ee Update addFav.py 2025-10-12 15:27:30 -07:00
SpudGunMan
ef28341cdb Update addFav.py 2025-10-12 15:07:27 -07:00
SpudGunMan
b5d610728c Update addFav.py
ffs
2025-10-12 15:03:44 -07:00
SpudGunMan
bc238ef476 Update addFav.py 2025-10-12 14:38:20 -07:00
SpudGunMan
feb3544014 fixBug 2025-10-12 14:32:59 -07:00
SpudGunMan
31322dc0cd lessWait
remove some waits now that 2 seconds is needed by firmware
2025-10-12 14:26:23 -07:00
12 changed files with 341 additions and 326 deletions

View File

@@ -41,6 +41,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
### Proximity Alerts
- **Location-Based Alerts**: Get notified when members arrive back at a configured lat/long, perfect for remote locations like campsites.
- **High Flying Alerts**: Get notified when nodes with high altitude are seen on mesh
- **Hey Chirpy**: Voice activate send messages with "hey chirpy"
### CheckList / Check In Out
- **Asset Tracking**: Maintain a list of node/asset checkin and checkout. Useful foraccountability of people, assets. Radio-Net, FEMA, Trailhead.
@@ -63,6 +64,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
### Radio Frequency Monitoring
- **SNR RF Activity Alerts**: Monitor a radio frequency and get alerts when high SNR RF activity is detected.
- **Hamlib Integration**: Use Hamlib (rigctld) to watch the S meter on a connected radio.
- **Speech to Text Brodcasting to Mesh** Using [vosk](https://alphacephei.com/vosk/models) to translate to text.
### EAS Alerts
- **FEMA iPAWS/EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from FEMA
@@ -253,6 +255,10 @@ The weather forecasting defaults to NOAA, for locations outside the USA, you can
enabled = True
lat = 48.50
lon = -123.0
# To fuzz the location of the above
fuzzConfigLocation = True
# Fuzz all values in all data
fuzzItAll = False
UseMeteoWxAPI = True
coastalEnabled = False # NOAA Coastal Data Enable NOAA Coastal Waters Forecasts and Tide
@@ -579,7 +585,7 @@ I used ideas and snippets from other responder bots and want to call them out!
- **mikecarper**: ideas, and testing. hamtest
- **c.merphy360**: high altitude alerts
- **Iris**: testing and finding 🐞
- **Cisien, bitflip, Woof, propstg, snydermesh, trs2982, FJRPilot, Josh, mesb1, and Hailo1999**: For testing and feature ideas on Discord and GitHub.
- **Cisien, bitflip, Woof, propstg, snydermesh, trs2982, FJRPilot, F0X, mesb1, and Hailo1999**: For testing and feature ideas on Discord and GitHub.
- **Meshtastic Discord Community**: For tossing out ideas and testing code.
### Tools

View File

@@ -171,6 +171,8 @@ bbsAPI_enabled = False
enabled = True
lat = 48.50
lon = -123.0
fuzzConfigLocation = True
fuzzItAll = False
# Default to metric units rather than imperial
useMetric = False
@@ -300,6 +302,12 @@ signalCycleLimit = 5
voxDetectionEnabled = False
# description to use in the alert message
voxDescription = VOX
useLocalVoxModel = False
voxLanguage = en-us
voxInputDevice = default
voxOnTrapList = True
voxTrapList = chirpy
[fileMon]
filemon_enabled = False

View File

@@ -141,20 +141,24 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
if len(cmds) > 0:
# sort the commands by index value
cmds = sorted(cmds, key=lambda k: k['index'])
logger.debug(f"System: Bot detected Commands:{cmds} From: {get_name_from_number(message_from_id)}")
# check the command isnt a isDM only command
if cmds[0]['cmd'] in restrictedCommands and not isDM:
bot_response = restrictedResponse
# Check if user is already playing a game
playing, game = isPlayingGame(message_from_id)
# Block restricted commands if not DM, or if already playing a game
if (cmds[0]['cmd'] in restrictedCommands and not isDM) or (cmds[0]['cmd'] in restrictedCommands and playing):
if playing:
bot_response = f"🤖You are already playing {game}, finish that first."
else:
bot_response = restrictedResponse
else:
logger.debug(f"System: Bot detected Commands:{cmds} From: {get_name_from_number(message_from_id)}")
# run the first command after sorting
bot_response = command_handler[cmds[0]['cmd']]()
# append the command to the cmdHistory list for lheard and history
if len(cmdHistory) > 50:
cmdHistory.pop(0)
cmdHistory.append({'nodeID': message_from_id, 'cmd': cmds[0]['cmd'], 'time': time.time()})
# wait a responseDelay to avoid message collision from lora-ack
time.sleep(responseDelay)
return bot_response
def handle_cmd(message, message_from_id, deviceID):
@@ -273,8 +277,6 @@ def handle_emergency(message_from_id, deviceID, message):
if enableSMTP:
for user in sysopEmails:
send_email(user, f"Emergency Assistance Requested by {nodeInfo} in {message}", message_from_id)
# respond to the user
time.sleep(responseDelay + 2)
return EMERGENCY_RESPONSE
def handle_motd(message, message_from_id, isDM):
@@ -546,7 +548,6 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
llmTotalRuntime.append(end - start)
return response
def handleDopeWars(message, nodeID, rxNode):
global dwPlayerTracker, dwHighScore
@@ -554,7 +555,7 @@ def handleDopeWars(message, nodeID, rxNode):
player = next((p for p in dwPlayerTracker if p.get('userID') == nodeID), None)
# If not found, add new player
if not player and nodeID != 0:
if not player and nodeID != 0 and not isPlayingGame(nodeID)[0]:
player = {
'userID': nodeID,
'last_played': time.time(),
@@ -566,14 +567,17 @@ def handleDopeWars(message, nodeID, rxNode):
high_score = getHighScoreDw()
msg += 'The High Score is $' + "{:,}".format(high_score.get('cash')) + ' by user ' + get_name_from_number(high_score.get('userID'), 'short', rxNode) + '\n'
msg += playDopeWars(nodeID, message)
else:
# Update last_played
elif player:
# Update last_played and cmd for the player
for p in dwPlayerTracker:
if p.get('userID') == nodeID:
p['last_played'] = time.time()
msg = playDopeWars(nodeID, message)
time.sleep(responseDelay + 1)
# if message starts wth 'e'xit remove player from tracker
if message.lower().startswith('e'):
dwPlayerTracker[:] = [p for p in dwPlayerTracker if p.get('userID') != nodeID]
msg = 'You have exited Dope Wars.'
return msg
def handle_gTnW(chess = False):
@@ -599,6 +603,7 @@ def handle_gTnW(chess = False):
def handleLemonade(message, nodeID, deviceID):
global lemonadeTracker, lemonadeCups, lemonadeLemons, lemonadeSugar, lemonadeWeeks, lemonadeScore, lemon_starting_cash, lemon_total_weeks
msg = ""
def create_player(nodeID):
# create new player
lemonadeTracker.append({'nodeID': nodeID, 'cups': 0, 'lemons': 0, 'sugar': 0, 'cash': lemon_starting_cash, 'start': lemon_starting_cash, 'cmd': 'new', 'last_played': time.time()})
@@ -607,34 +612,39 @@ def handleLemonade(message, nodeID, deviceID):
lemonadeSugar.append({'nodeID': nodeID, 'cost': 3.00, 'count': 15, 'min': 1.50, 'unit': 0.00})
lemonadeScore.append({'nodeID': nodeID, 'value': 0.00, 'total': 0.00})
lemonadeWeeks.append({'nodeID': nodeID, 'current': 1, 'total': lemon_total_weeks, 'sales': 99, 'potential': 0, 'unit': 0.00, 'price': 0.00, 'total_sales': 0})
#initalize player variables
if lemonadeTracker == []:
lemonadeTracker = []
# get player's last command from tracker if not new player
last_cmd = ""
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
last_cmd = lemonadeTracker[i]['cmd']
logger.debug(f"System: {nodeID} PlayingGame lemonstand last_cmd: {last_cmd}")
# create new player if not in tracker
if last_cmd == "" and nodeID != 0 and "lemonstand" in message.lower():
# If player not found, create if message is for lemonstand
if nodeID != 0 and "lemonstand" in message.lower():
create_player(nodeID)
msg += "Welcome🍋🥤"
last_cmd = "new"
# high score
highScore = {"userID": 0, "cash": 0, "success": 0}
highScore = getHighScoreLemon()
if highScore != 0:
if highScore['userID'] != 0:
nodeName = get_name_from_number(highScore['userID'])
if nodeName.isnumeric() and multiple_interface:
logger.debug(f"System: TODO is multiple interface fix mention this please nodeName: {nodeName}")
#nodeName = get_name_from_number(highScore['userID'], 'long', 2)
msg += f" HighScore🥇{nodeName} 💰{round(highScore['cash'], 2)}k "
if last_cmd != "":
msg += playLemonstand(nodeID=nodeID, message=message, celsius=False)
# Play lemonstand with newgame=True
fruit = playLemonstand(nodeID=nodeID, message=message, celsius=False, newgame=True)
if fruit:
msg += fruit
return msg
# if message starts wth 'e'xit remove player from tracker
if message.lower().startswith("e"):
logger.debug(f"System: Lemonade: {nodeID} is leaving the stand")
msg = "You have left the Lemonade Stand."
highScore = getHighScoreLemon()
if highScore != 0 and highScore['userID'] != 0:
nodeName = get_name_from_number(highScore['userID'])
msg += f" HighScore🥇{nodeName} 💰{round(highScore['cash'], 2)}k "
# remove player from player tracker and inventory trackers
lemonadeTracker[:] = [p for p in lemonadeTracker if p['nodeID'] != nodeID]
lemonadeCups[:] = [p for p in lemonadeCups if p['nodeID'] != nodeID]
lemonadeLemons[:] = [p for p in lemonadeLemons if p['nodeID'] != nodeID]
lemonadeSugar[:] = [p for p in lemonadeSugar if p['nodeID'] != nodeID]
lemonadeWeeks[:] = [p for p in lemonadeWeeks if p['nodeID'] != nodeID]
lemonadeScore[:] = [p for p in lemonadeScore if p['nodeID'] != nodeID]
return msg
# play lemonstand (not newgame)
if ("lemonstand" not in message.lower() and message != ""):
fruit = playLemonstand(nodeID=nodeID, message=message, celsius=False, newgame=False)
if fruit:
msg += fruit
return msg
def handleBlackJack(message, nodeID, deviceID):
@@ -749,8 +759,6 @@ def handleMmind(message, nodeID, deviceID):
return msg
msg += start_mMind(nodeID=nodeID, message=message)
# wait a second to keep from message collision
time.sleep(responseDelay + 1)
return msg
def handleGolf(message, nodeID, deviceID):
@@ -781,8 +789,6 @@ def handleGolf(message, nodeID, deviceID):
msg += f"Clubs: (D)river, (L)ow Iron, (M)id Iron, (H)igh Iron, (G)ap Wedge, Lob (W)edge\n"
msg += playGolf(nodeID=nodeID, message=message)
# wait a second to keep from message collision
time.sleep(responseDelay + 1)
return msg
def handleHangman(message, nodeID, deviceID):
@@ -809,8 +815,6 @@ def handleHangman(message, nodeID, deviceID):
)
msg = "🧩Hangman🤖 'end' to cut rope🪢\n"
msg += hangman.play(nodeID, message)
time.sleep(responseDelay + 1)
return msg
def handleHamtest(message, nodeID, deviceID):
@@ -844,8 +848,6 @@ def handleHamtest(message, nodeID, deviceID):
# if the message is an answer A B C or D upper or lower case
if response[0].upper() in ['A', 'B', 'C', 'D']:
msg = hamtest.answer(nodeID, response[0])
time.sleep(responseDelay + 1)
return msg
def handleTicTacToe(message, nodeID, deviceID):
@@ -874,8 +876,6 @@ def handleTicTacToe(message, nodeID, deviceID):
msg = "🎯Tic-Tac-Toe🤖 '(e)nd'\n"
msg += tictactoe.play(nodeID, message)
time.sleep(responseDelay + 1)
return msg
def quizHandler(message, nodeID, deviceID):
@@ -1336,35 +1336,57 @@ def check_and_play_game(tracker, message_from_id, message_string, rxNode, channe
logger.debug(f"System: LLM Disabled for {message_from_id} for duration of {game_name}")
send_message(handle_game_func(message_string, message_from_id, rxNode), channel_number, message_from_id, rxNode)
return True, game_name
else:
tracker.pop(i)
return False, game_name
return False, "None"
def checkPlayingGame(message_from_id, message_string, rxNode, channel_number):
gameTrackers = [
(dwPlayerTracker, "DopeWars", handleDopeWars) if 'dwPlayerTracker' in globals() else None,
(lemonadeTracker, "LemonadeStand", handleLemonade) if 'lemonadeTracker' in globals() else None,
(vpTracker, "VideoPoker", handleVideoPoker) if 'vpTracker' in globals() else None,
(jackTracker, "BlackJack", handleBlackJack) if 'jackTracker' in globals() else None,
(mindTracker, "MasterMind", handleMmind) if 'mindTracker' in globals() else None,
(golfTracker, "GolfSim", handleGolf) if 'golfTracker' in globals() else None,
(hangmanTracker, "Hangman", handleHangman) if 'hangmanTracker' in globals() else None,
(hamtestTracker, "HamTest", handleHamtest) if 'hamtestTracker' in globals() else None,
(tictactoeTracker, "TicTacToe", handleTicTacToe) if 'tictactoeTracker' in globals() else None,
(surveyTracker, "Survey", surveyHandler) if 'surveyTracker' in globals() else None,
#quiz does not use a tracker (quizGamePlayer) always active
]
def isPlayingGame(message_from_id):
global gameTrackers
trackers = gameTrackers.copy()
playingGame = False
game = "None"
trackers = [tracker for tracker in trackers if tracker is not None]
for tracker, game_name, handle_game_func in trackers:
for i in range(len(tracker)-1, -1, -1): # iterate backwards for safe removal
id_key = 'userID' if game_name == "DopeWars" else 'nodeID'
id_key = 'id' if game_name == "Survey" else id_key
if tracker[i].get(id_key) == message_from_id:
last_played_key = 'last_played' if 'last_played' in tracker[i] else 'time'
if tracker[i].get(last_played_key, 0) > (time.time() - GAMEDELAY):
playingGame = True
game = game_name
break
if playingGame:
break
return playingGame, game
def checkPlayingGame(message_from_id, message_string, rxNode, channel_number):
global gameTrackers
trackers = gameTrackers.copy()
playingGame = False
game = "None"
trackers = [
(dwPlayerTracker, "DopeWars", handleDopeWars) if 'dwPlayerTracker' in globals() else None,
(lemonadeTracker, "LemonadeStand", handleLemonade) if 'lemonadeTracker' in globals() else None,
(vpTracker, "VideoPoker", handleVideoPoker) if 'vpTracker' in globals() else None,
(jackTracker, "BlackJack", handleBlackJack) if 'jackTracker' in globals() else None,
(mindTracker, "MasterMind", handleMmind) if 'mindTracker' in globals() else None,
(golfTracker, "GolfSim", handleGolf) if 'golfTracker' in globals() else None,
(hangmanTracker, "Hangman", handleHangman) if 'hangmanTracker' in globals() else None,
(hamtestTracker, "HamTest", handleHamtest) if 'hamtestTracker' in globals() else None,
(tictactoeTracker, "TicTacToe", handleTicTacToe) if 'tictactoeTracker' in globals() else None,
(surveyTracker, "Survey", surveyHandler) if 'surveyTracker' in globals() else None,
#quiz does not use a tracker (quizGamePlayer) always active
]
trackers = [tracker for tracker in trackers if tracker is not None]
for tracker, game_name, handle_game_func in trackers:
playingGame, game = check_and_play_game(tracker, message_from_id, message_string, rxNode, channel_number, game_name, handle_game_func)
if playingGame:
break
return playingGame
def onReceive(packet, interface):
@@ -1535,13 +1557,15 @@ def onReceive(packet, interface):
# DM is useful for games or LLM
if games_enabled and (hop == "Direct" or hop_count < game_hop_limit):
playingGame = checkPlayingGame(message_from_id, message_string, rxNode, channel_number)
else:
elif hop_count >= game_hop_limit:
if games_enabled:
logger.warning(f"Device:{rxNode} Ignoring Request to Play Game: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)} with hop count: {hop}")
send_message(f"Your hop count exceeds safe playable distance at {hop_count} hops", channel_number, message_from_id, rxNode)
time.sleep(responseDelay)
else:
playingGame = False
else:
playingGame = False
if not playingGame:
if llm_enabled and llmReplyToNonCommands:

View File

@@ -366,7 +366,8 @@ def get_location_table(nodeID, choice=0):
return loc_table_string
def endGameDw(nodeID):
global dwCashDb, dwInventoryDb, dwLocationDb, dwGameDayDb, dwHighScore
global dwCashDb, dwInventoryDb, dwLocationDb, dwGameDayDb, dwHighScore, dwPlayerTracker
cash = 0
msg = ''
dwHighScore = getHighScoreDw()
# Confirm the cash for the user
@@ -375,23 +376,6 @@ def endGameDw(nodeID):
cash = dwCashDb[i].get('cash')
logger.debug("System: DopeWars: Game Over for user: " + str(nodeID) + " with cash: " + str(cash))
# remove the player from the game databases
for i in range(0, len(dwCashDb)):
if dwCashDb[i].get('userID') == nodeID:
dwCashDb.pop(i)
for i in range(0, len(dwInventoryDb)):
if dwInventoryDb[i].get('userID') == nodeID:
dwInventoryDb.pop(i)
for i in range(0, len(dwLocationDb)):
if dwLocationDb[i].get('userID') == nodeID:
dwLocationDb.pop(i)
for i in range(0, len(dwGameDayDb)):
if dwGameDayDb[i].get('userID') == nodeID:
dwGameDayDb.pop(i)
for i in range(0, len(dwPlayerTracker)):
if dwPlayerTracker[i].get('userID') == nodeID:
dwPlayerTracker.pop(i)
# checks if the player's score is higher than the high score and writes a new high score if it is
if cash > dwHighScore.get('cash'):
dwHighScore = ({'userID': nodeID, 'cash': round(cash, 2)})

View File

@@ -146,6 +146,10 @@ def playGolf(nodeID, message, finishedHole=False):
par = golfTracker[i]['par']
total_strokes = golfTracker[i]['total_strokes']
total_to_par = golfTracker[i]['total_to_par']
#update last played time
for i in range(len(golfTracker)):
if golfTracker[i]['nodeID'] == nodeID:
golfTracker[i]['last_played'] = time.time()
if last_cmd == "" or last_cmd == "new":
# Start a new hole

View File

@@ -18,7 +18,6 @@ locale.setlocale(locale.LC_ALL, '')
lemon_starting_cash = 30.00
lemon_total_weeks = 7
lemonadeTracker = [{'nodeID': 0, 'cups': 0, 'lemons': 0, 'sugar': 0, 'cash': lemon_starting_cash, 'start': lemon_starting_cash, 'cmd': 'new', 'last_played': time.time()}]
lemonadeCups = [{'nodeID': 0, 'cost': 2.50, 'count': 25, 'min': 0.99, 'unit': 0.00}]
lemonadeLemons = [{'nodeID': 0, 'cost': 4.00, 'count': 8, 'min': 2.00, 'unit': 0.00}]
lemonadeSugar = [{'nodeID': 0, 'cost': 3.00, 'count': 15, 'min': 1.50, 'unit': 0.00}]
@@ -50,13 +49,14 @@ def getHighScoreLemon():
pickle.dump(high_score, file)
return high_score
def playLemonstand(nodeID, message, celsius=False):
def playLemonstand(nodeID, message, celsius=False, newgame=False):
global lemonadeTracker, lemonadeCups, lemonadeLemons, lemonadeSugar, lemonadeWeeks, lemonadeScore
msg = ""
potential = 0
unit = 0.0
price = 0.0
total_sales = 0
lemonsLastCmd = ''
high_score = getHighScoreLemon()
@@ -95,33 +95,6 @@ def playLemonstand(nodeID, message, celsius=False):
lemonadeScore[i]['value'] = score.value
lemonadeScore[i]['total'] = score.total
def endGame(nodeID):
# remove the player from the tracker
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker.pop(i)
for i in range(len(lemonadeCups)):
if lemonadeCups[i]['nodeID'] == nodeID:
lemonadeCups.pop(i)
for i in range(len(lemonadeLemons)):
if lemonadeLemons[i]['nodeID'] == nodeID:
lemonadeLemons.pop(i)
for i in range(len(lemonadeSugar)):
if lemonadeSugar[i]['nodeID'] == nodeID:
lemonadeSugar.pop(i)
for i in range(len(lemonadeWeeks)):
if lemonadeWeeks[i]['nodeID'] == nodeID:
lemonadeWeeks.pop(i)
for i in range(len(lemonadeScore)):
if lemonadeScore[i]['nodeID'] == nodeID:
lemonadeScore.pop(i)
logger.debug("System: Lemonade: Game Over for " + str(nodeID))
# Check for end of game
if message.lower().startswith("e"):
endGame(nodeID)
return "Goodbye!👋"
title="LemonStand🍋"
# Define the temperature unit symbols
fahrenheit_unit = "ºF"
@@ -240,22 +213,35 @@ def playLemonstand(nodeID, message, celsius=False):
if lemonadeScore[i]['nodeID'] == nodeID:
score.value = lemonadeScore[i]['value']
score.total = lemonadeScore[i]['total']
#handle last command
lemonsLastCmd = 'new'
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonsLastCmd = lemonadeTracker[i]['cmd']
if (newgame):
# reset the game values
inventory.cups = 0
inventory.lemons = 0
inventory.sugar = 0
inventory.cash = lemon_starting_cash
inventory.start = lemon_starting_cash
cups.cost = 2.50
cups.unit = round(cups.cost / cups.count, 2)
lemons.cost = 4.00
lemons.unit = round(lemons.cost / lemons.count, 2)
sugar.cost = 3.00
sugar.unit = round(sugar.cost / sugar.count, 2)
weeks.current = 1
weeks.total_sales = 0
weeks.summary = []
score.value = 0.00
score.total = 0.00
lemonsLastCmd = "cups"
# set the last command to new in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "cups"
lemonadeTracker[i]['last_played'] = time.time()
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
# Start the main loop
if (weeks.current <= weeks.total):
if "new" in lemonsLastCmd:
if newgame or "new" in lemonsLastCmd:
logger.debug("System: Lemonade: New Game: " + str(nodeID))
# set the last command to cups in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "cups"
# Create a new display buffer for the text messages
buffer= ""
@@ -300,8 +286,8 @@ def playLemonstand(nodeID, message, celsius=False):
sugar.unit = round(sugar.cost / sugar.count, 2)
# Calculate the unit cost and display the estimated sales from the forecast potential
unit = cups.unit + lemons.unit + sugar.unit
buffer += " SupplyCost" + locale.currency(unit, grouping=True) + " a cup."
unit = max(0.01, min(cups.unit + lemons.unit + sugar.unit, 4.0)) # limit the unit cost between $0.01 and $4.00
buffer += " SupplyCost" + locale.currency(round(unit, 2), grouping=True) + " a cup."
buffer += " Sales Potential:" + str(potential) + " cups."
# Display the current inventory
@@ -312,21 +298,16 @@ def playLemonstand(nodeID, message, celsius=False):
# Display the updated item prices
buffer += f"\nPrices: "
buffer += "🥤:" + \
locale.currency(cups.cost, grouping=True) + " 📦 of " + str(cups.count) + "."
buffer += " 🍋:" + \
locale.currency(lemons.cost, grouping=True) + " 🧺 of " + str(lemons.count) + "."
buffer += " 🍚:" + \
locale.currency(sugar.cost, grouping=True) + " bag for " + str(sugar.count) + "🥤."
buffer += "🥤:" + locale.currency(round(cups.cost, 2), grouping=True) + " 📦 of " + str(cups.count) + "."
buffer += " 🍋:" + locale.currency(round(lemons.cost, 2), grouping=True) + " 🧺 of " + str(lemons.count) + "."
buffer += " 🍚:" + locale.currency(round(sugar.cost, 2), grouping=True) + " bag for " + str(sugar.count) + "🥤."
# Display the current cash
gainloss = inventory.cash - inventory.start
buffer += " 💵:" + \
locale.currency(inventory.cash, grouping=True)
buffer += " 💵:" + locale.currency(round(inventory.cash, 2), grouping=True)
# if the player is in the red
pnl = locale.currency(gainloss, grouping=True)
pnl = locale.currency(round(gainloss, 2), grouping=True)
if "0.00" not in pnl:
if pnl.startswith("-"):
buffer += "📊P&L📉" + pnl
@@ -337,7 +318,7 @@ def playLemonstand(nodeID, message, celsius=False):
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
return buffer
if "cups" in lemonsLastCmd:
if "cups" in lemonsLastCmd and not newgame:
# Read the number of cup boxes to purchase
newcups = -1
if "n" in message.lower():
@@ -351,22 +332,22 @@ def playLemonstand(nodeID, message, celsius=False):
inventory.cups += (newcups * cups.count)
inventory.cash -= cost
msg = "Purchased " + str(newcups) + " 📦 "
msg += str(inventory.cups) + " 🥤 in inventory. " + locale.currency(inventory.cash, grouping=True) + f" remaining"
msg += str(inventory.cups) + " 🥤 in inventory. " + locale.currency(round(inventory.cash, 2), grouping=True) + f" remaining"
else:
msg = "No 🥤 were purchased"
except Exception as e:
return "invalid input, enter the number of 🥤 to purchase or (N)one"
msg += f"\n 🍋 to buy? Have {inventory.lemons}🥤 of 🍋 Cost {locale.currency(lemons.cost, grouping=True)} a 🧺 for {str(lemons.count)}🥤"
# set the last command to lemons in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "lemons"
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
msg += f"\n 🍋 to buy? Have {inventory.lemons}🥤 of 🍋 Cost {locale.currency(lemons.cost, grouping=True)} a 🧺 for {str(lemons.count)}🥤"
return msg
if "lemons" in lemonsLastCmd:
if "lemons" in lemonsLastCmd and not newgame:
# Read the number of lemon bags to purchase
newlemons = -1
if "n" in message.lower():
@@ -387,15 +368,15 @@ def playLemonstand(nodeID, message, celsius=False):
newlemons = -1
return "invalid input, enter the number of 🍋 to purchase"
msg += f"\n 🍚 to buy? You have {inventory.sugar}🥤 of 🍚, Cost {locale.currency(sugar.cost, grouping=True)} a bag for {str(sugar.count)}🥤"
# set the last command to sugar in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "sugar"
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
msg += f"\n 🍚 to buy? You have {inventory.sugar}🥤 of 🍚, Cost {locale.currency(sugar.cost, grouping=True)} a bag for {str(sugar.count)}🥤"
return msg
if "sugar" in lemonsLastCmd:
if "sugar" in lemonsLastCmd and not newgame:
# Read the number of sugar bags to purchase
newsugar = -1
if "n" in message.lower():
@@ -415,8 +396,8 @@ def playLemonstand(nodeID, message, celsius=False):
except Exception as e:
return "invalid input, enter the number of 🍚 bags to purchase"
msg += f"Cost of goods is {locale.currency(unit, grouping=True)}"
msg += f"per 🥤 {locale.currency(inventory.cash, grouping=True)} 💵 remaining."
msg += f"Cost of goods is {locale.currency(round(unit, 2), grouping=True)}"
msg += f"per 🥤 {locale.currency(round(inventory.cash, 2), grouping=True)} 💵 remaining."
msg += f"\nPrice to Sell? or (G)rocery to buy more 🥤🍋🍚"
# set the last command to price in the inventory db
@@ -426,7 +407,7 @@ def playLemonstand(nodeID, message, celsius=False):
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
return msg
if "price" in lemonsLastCmd:
if "price" in lemonsLastCmd and not newgame:
# set the last command to sales in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
@@ -456,7 +437,7 @@ def playLemonstand(nodeID, message, celsius=False):
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
if "sales" in lemonsLastCmd:
if "sales" in lemonsLastCmd and not newgame:
# Calculate the weekly sales based on price and lowest inventory level
# (higher markup price = fewer sales, limited by the inventory on-hand)
sales = get_sales_amount(potential, unit, price)
@@ -571,16 +552,15 @@ def playLemonstand(nodeID, message, celsius=False):
else:
# keep playing
weeks.current = weeks.current + 1
msg += f"Play another week🥤? or (E)nd Game"
# set the last command to new in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
lemonadeTracker[i]['cmd'] = "new"
lemonadeTracker[i]['last_played'] = time.time()
weeks.current = weeks.current + 1
msg += f"Play another week🥤? or (E)nd Game"
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
return msg
else:

View File

@@ -29,7 +29,7 @@ class TicTacToe:
if id in self.game:
games = self.game[id]["games"]
won = self.game[id]["won"]
if games > 0:
if games > 3:
if won / games >= 3.14159265358979323846: # win rate > pi
ret += random.choice(positiveThoughts) + "\n"
else:
@@ -156,7 +156,7 @@ class TicTacToe:
if winner == X:
g["won"] += 1
return "🎉You won! (n)ew (e)nd"
elif winner == X:
elif winner == O:
return "🤖Bot wins! (n)ew (e)nd"
else:
return "🤝Tie, The only winning move! (n)ew (e)nd"

View File

@@ -16,6 +16,7 @@ trap_list_location = ("whereami", "wx", "wxa", "wxalert", "rlist", "ea", "ealert
def where_am_i(lat=0, lon=0, short=False, zip=False):
whereIam = ""
grid = mh.to_maiden(float(lat), float(lon))
location = lat, lon
if int(float(lat)) == 0 and int(float(lon)) == 0:
logger.error("Location: No GPS data, try sending location")
@@ -171,6 +172,7 @@ def getArtSciRepeaters(lat=0, lon=0):
def get_NOAAtide(lat=0, lon=0):
station_id = ""
location = lat,lon
if float(lat) == 0 and float(lon) == 0:
logger.error("Location:No GPS data, try sending location for tide")
return NO_DATA_NOGPS
@@ -235,6 +237,7 @@ def get_NOAAtide(lat=0, lon=0):
def get_NOAAweather(lat=0, lon=0, unit=0):
# get weather report from NOAA for forecast detailed
weather = ""
location = lat,lon
if float(lat) == 0 and float(lon) == 0:
return NO_DATA_NOGPS
@@ -338,14 +341,15 @@ def abbreviate_noaa(row):
line = row
for key, value in replacements.items():
# case insensitive replace
line = line.replace(key, value).replace(key.capitalize(), value).replace(key.upper(), value)
for variant in (key, key.capitalize(), key.upper()):
if variant != value:
line = line.replace(variant, value)
return line
def getWeatherAlertsNOAA(lat=0, lon=0, useDefaultLatLon=False):
# get weather alerts from NOAA limited to ALERT_COUNT with the total number of alerts found
alerts = ""
location = lat,lon
if float(lat) == 0 and float(lon) == 0 and not useDefaultLatLon:
return NO_DATA_NOGPS
else:
@@ -422,6 +426,7 @@ def alertBrodcastNOAA():
def getActiveWeatherAlertsDetailNOAA(lat=0, lon=0):
# get the latest details of weather alerts from NOAA
alerts = ""
location = lat,lon
if float(lat) == 0 and float(lon) == 0:
logger.warning("Location:No GPS data, try sending location for weather alerts")
return NO_DATA_NOGPS
@@ -813,6 +818,7 @@ def distance(lat=0,lon=0,nodeID=0, reset=False):
# part of the howfar function, calculates the distance between two lat/lon points
msg = ""
dupe = False
location = lat,lon
r = 6371 # Radius of earth in kilometers # haversine formula
if lat == 0 and lon == 0:

View File

@@ -2,23 +2,36 @@
# detect signal strength and frequency of active channel if appears to be in use send to mesh network
# depends on rigctld running externally as a network service
# also can use VOX detection with a microphone and vosk speech to text to send voice messages to mesh network
# requires vosk and sounddevice python modules
# requires vosk and sounddevice python modules. will auto download needed. more from https://alphacephei.com/vosk/models and unpack
# 2024 Kelly Keeton K7MHI
previousVoxState = False
from modules.log import *
import asyncio
# verbose debug logging for trap words function
debugVoxTmsg = False
if radio_detection_enabled:
# used by hamlib detection
import socket
if voxDetectionEnabled:
# module global variables
previousVoxState = False
voxHoldTime = signalHoldTime
try:
import sounddevice as sd # pip install sounddevice sudo apt install portaudio19-dev
from vosk import Model, KaldiRecognizer # pip install vosk
import json
q = asyncio.Queue()
q = asyncio.Queue(maxsize=10) # what is a reasonable limit?
if useLocalVoxModel:
voxModel = Model(lang=localVoxModelPath) # use built in model for specified language
else:
voxModel = Model(lang=voxLanguage) # use built in model for specified language
except Exception as e:
print(f"RadioMon: Error importing VOX dependencies: {e}")
print(f"To use VOX detection please install the vosk and sounddevice python modules")
@@ -27,8 +40,56 @@ if voxDetectionEnabled:
voxDetectionEnabled = False
logger.error(f"RadioMon: VOX detection disabled due to import error")
FREQ_NAME_MAP = {
462562500: "GRMS CH1",
462587500: "GRMS CH2",
462612500: "GRMS CH3",
462637500: "GRMS CH4",
462662500: "GRMS CH5",
462687500: "GRMS CH6",
462712500: "GRMS CH7",
467562500: "GRMS CH8",
467587500: "GRMS CH9",
467612500: "GRMS CH10",
467637500: "GRMS CH11",
467662500: "GRMS CH12",
467687500: "GRMS CH13",
467712500: "GRMS CH14",
467737500: "GRMS CH15",
462550000: "GRMS CH16",
462575000: "GMRS CH17",
462600000: "GMRS CH18",
462625000: "GMRS CH19",
462675000: "GMRS CH20",
462670000: "GMRS CH21",
462725000: "GMRS CH22",
462725500: "GMRS CH23",
467575000: "GMRS CH24",
467600000: "GMRS CH25",
467625000: "GMRS CH26",
467650000: "GMRS CH27",
467675000: "GMRS CH28",
467700000: "FRS CH1",
462650000: "FRS CH5",
462700000: "FRS CH7",
462737500: "FRS CH16",
146520000: "2M Simplex Calling",
446000000: "70cm Simplex Calling",
156800000: "Marine CH16",
# Add more as needed
}
def get_freq_common_name(freq):
freq = int(freq)
name = FREQ_NAME_MAP.get(freq)
if name:
return name
else:
# Return MHz if not found
return f"{freq/1000000} Mhz"
def get_hamlib(msg="f"):
# get data from rigctld server
try:
rigControlSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
rigControlSocket.settimeout(2)
@@ -50,105 +111,6 @@ def get_hamlib(msg="f"):
except Exception as e:
logger.error(f"RadioMon: Error fetching data from rigctld: {e}")
return ERROR_FETCHING_DATA
def get_freq_common_name(freq):
freq = int(freq)
if freq == 462562500:
return "GRMS CH1"
elif freq == 462587500:
return "GRMS CH2"
elif freq == 462612500:
return "GRMS CH3"
elif freq == 462637500:
return "GRMS CH4"
elif freq == 462662500:
return "GRMS CH5"
elif freq == 462687500:
return "GRMS CH6"
elif freq == 462712500:
return "GRMS CH7"
elif freq == 467562500:
return "GRMS CH8"
elif freq == 467587500:
return "GRMS CH9"
elif freq == 467612500:
return "GRMS CH10"
elif freq == 467637500:
return "GRMS CH11"
elif freq == 467662500:
return "GRMS CH12"
elif freq == 467687500:
return "GRMS CH13"
elif freq == 467712500:
return "GRMS CH14"
elif freq == 467737500:
return "GRMS CH15"
elif freq == 462550000:
return "GRMS CH16"
elif freq == 462575000:
return "GMRS CH17"
elif freq == 462600000:
return "GMRS CH18"
elif freq == 462625000:
return "GMRS CH19"
elif freq == 462675000:
return "GMRS CH20"
elif freq == 462670000:
return "GMRS CH21"
elif freq == 462725000:
return "GMRS CH22"
elif freq == 462725500:
return "GMRS CH23"
elif freq == 467575000:
return "GMRS CH24"
elif freq == 467600000:
return "GMRS CH25"
elif freq == 467625000:
return "GMRS CH26"
elif freq == 467650000:
return "GMRS CH27"
elif freq == 467675000:
return "GMRS CH28"
elif freq == 467700000:
return "FRS CH1"
elif freq == 462575000:
return "FRS CH2"
elif freq == 462600000:
return "FRS CH3"
elif freq == 462650000:
return "FRS CH5"
elif freq == 462675000:
return "FRS CH6"
elif freq == 462700000:
return "FRS CH7"
elif freq == 462725000:
return "FRS CH8"
elif freq == 462562500:
return "FRS CH9"
elif freq == 462587500:
return "FRS CH10"
elif freq == 462612500:
return "FRS CH11"
elif freq == 462637500:
return "FRS CH12"
elif freq == 462662500:
return "FRS CH13"
elif freq == 462687500:
return "FRS CH14"
elif freq == 462712500:
return "FRS CH15"
elif freq == 462737500:
return "FRS CH16"
elif freq == 146520000:
return "2M Simplex Calling"
elif freq == 446000000:
return "70cm Simplex Calling"
elif freq == 156800000:
return "Marine CH16"
else:
#return Mhz
freq = freq/1000000
return f"{freq} Mhz"
def get_sig_strength():
strength = get_hamlib('l STRENGTH')
@@ -190,18 +152,21 @@ def make_vox_callback(loop, q):
logger.warning(f"RadioMon: VOX input status: {status}")
try:
loop.call_soon_threadsafe(q.put_nowait, bytes(indata))
except asyncio.QueueFull:
# Optionally log or just drop the oldest
logger.debug("RadioMon: VOX queue full, dropping audio frame")
except RuntimeError:
# Loop may be closed
pass
return vox_callback
voxInputDevice = None
async def voxMonitor():
global previousVoxState, voxMsgQueue
try:
model = Model(lang="en-us")
model = voxModel
device_info = sd.query_devices(voxInputDevice, 'input')
samplerate = 16000
logger.debug(f"RadioMon: VOX monitor started on device {device_info['name']} with samplerate {samplerate}")
logger.debug(f"RadioMon: VOX monitor started on device {device_info['name']} with samplerate {samplerate} using trap words: {voxTrapList if voxOnTrapList else 'none'}")
rec = KaldiRecognizer(model, samplerate)
loop = asyncio.get_running_loop()
callback = make_vox_callback(loop, q)
@@ -218,9 +183,26 @@ async def voxMonitor():
if rec.AcceptWaveform(data):
result = rec.Result()
text = json.loads(result).get("text", "")
if text and text != "huh":
logger.info(f"🎙Detected {voxDescription}: {text}")
voxMsgQueue.append(f"🎙Detected {voxDescription}: {text}")
# check for trap words
if text and text != 'huh':
if voxOnTrapList:
if isinstance(voxTrapList, str):
traps = [voxTrapList]
else:
traps = voxTrapList
if any(trap.lower() in text.lower() for trap in traps):
#remove the trap words from the text
for trap in traps:
text = text.replace(trap, '')
text = text.strip()
if text:
logger.debug(f"RadioMon: VOX 🎙Trapped {voxTrapList} in: {text}")
voxMsgQueue.append(f"🎙Trapped {voxDescription}: {text}")
else:
if debugVoxTmsg:
logger.debug(f"RadioMon: VOX ignored text not on trap list: {text}")
else:
voxMsgQueue.append(f"🎙Detected {voxDescription}: {text}")
await asyncio.sleep(0.5)
except Exception as e:
logger.error(f"RadioMon: Error in VOX monitor: {e}")

View File

@@ -273,6 +273,8 @@ try:
location_enabled = config['location'].getboolean('enabled', True)
latitudeValue = config['location'].getfloat('lat', 48.50)
longitudeValue = config['location'].getfloat('lon', -123.0)
fuzz_config_location = config['location'].getboolean('fuzzConfigLocation', True) # default True
fuzzItAll = config['location'].getboolean('fuzzAllLocations', False) # default False, only fuzz config location
use_meteo_wxApi = config['location'].getboolean('UseMeteoWxAPI', False) # default False use NOAA
use_metric = config['location'].getboolean('useMetric', False) # default Imperial units
repeater_lookup = config['location'].get('repeaterLookup', 'rbook') # default repeater lookup source
@@ -368,6 +370,12 @@ try:
signalCycleLimit = config['radioMon'].getint('signalCycleLimit', 5) # default 5 cycles, used with SIGNAL_COOLDOWN
voxDetectionEnabled = config['radioMon'].getboolean('voxDetectionEnabled', False) # default VOX detection disabled
voxDescription = config['radioMon'].get('voxDescription', 'VOX') # default VOX detected audio message
useLocalVoxModel = config['radioMon'].getboolean('useLocalVoxModel', False) # default False
localVoxModelPath = config['radioMon'].get('localVoxModelPath', 'no') # default models/vox.tflite
voxLanguage = config['radioMon'].get('voxLanguage', 'en-US') # default en-US
voxInputDevice = config['radioMon'].get('voxInputDevice', 'default') # default default
voxOnTrapList = config['radioMon'].getboolean('voxOnTrapList', False) # default False
voxTrapList = config['radioMon'].get('voxTrapList', 'chirpy').split(',') # default chirpy
# file monitor
file_monitor_enabled = config['fileMon'].getboolean('filemon_enabled', False)
@@ -378,7 +386,7 @@ try:
news_random_line_only = config['fileMon'].getboolean('news_random_line', False) # default False
enable_runShellCmd = config['fileMon'].getboolean('enable_runShellCmd', False) # default False
allowXcmd = config['fileMon'].getboolean('allowXcmd', False) # default False
xCmd2factorEnabled = config['fileMon'].getboolean('2factor_enabled', False) # default False
xCmd2factorEnabled = config['fileMon'].getboolean('2factor_enabled', True) # default True
xCmd2factor_timeout = config['fileMon'].getint('2factor_timeout', 100) # default 100 seconds
# games

View File

@@ -518,39 +518,45 @@ def get_node_list(nodeInt=1):
return node_list
def get_node_location(nodeID, nodeInt=1, channel=0):
def get_node_location(nodeID, nodeInt=1, channel=0, round_digits=2):
"""
Returns [latitude, longitude] for a node.
- Always returns a fuzzed (rounded) config location as fallback.
- returns their actual position if available, else fuzzed config location.
"""
interface = globals()[f'interface{nodeInt}']
# Get the location of a node by its number from nodeDB on device
# if no location data, return default location
latitude = latitudeValue
longitude = longitudeValue
position = [latitudeValue,longitudeValue]
fuzzed_position = [round(latitudeValue, round_digits), round(longitudeValue, round_digits)]
config_position = [latitudeValue, longitudeValue]
# Try to find an exact location for the requested node
if interface.nodes:
for node in interface.nodes.values():
if nodeID == node['num']:
if 'position' in node and node['position'] is not {}:
pos = node.get('position')
if (
pos and isinstance(pos, dict)
and pos.get('latitude') is not None
and pos.get('longitude') is not None
):
try:
latitude = node['position']['latitude']
longitude = node['position']['longitude']
logger.debug(f"System: location data for {nodeID} is {latitude},{longitude}")
position = [latitude,longitude]
# Got a valid position
latitude = pos['latitude']
longitude = pos['longitude']
if fuzzItAll:
latitude = round(latitude, round_digits)
longitude = round(longitude, round_digits)
logger.debug(f"System: Fuzzed location data for {nodeID}")
return [latitude, longitude]
except Exception as e:
logger.debug(f"System: No location data for {nodeID} use default location")
return position
else:
logger.debug(f"System: No location data for {nodeID} using default location")
# request location data
# try:
# logger.debug(f"System: Requesting location data for {number}")
# interface.sendPosition(destinationId=number, wantResponse=False, channelIndex=channel)
# except Exception as e:
# logger.error(f"System: Error requesting location data for {number}. Error: {e}")
return position
else:
logger.warning(f"System: Location for NodeID {nodeID} not found in nodeDb")
return position
logger.warning(f"System: Error processing position for node {nodeID}: {e}")
if fuzz_config_location:
# Return fuzzed config location if no valid position found
return fuzzed_position
else:
return config_position
def get_closest_nodes(nodeInt=1,returnCount=3):
interface = globals()[f'interface{nodeInt}']
node_list = []
@@ -595,16 +601,21 @@ def get_closest_nodes(nodeInt=1,returnCount=3):
logger.warning(f"System: No nodes found in closest_nodes on interface {nodeInt}")
return ERROR_FETCHING_DATA
def handleFavoritNode(nodeInt=1, nodeID=0, aor=False):
#aor is add or remove if True add, if False remove
def handleFavoriteNode(nodeInt=1, nodeID=0, aor=False):
# Add or remove a favorite node for the given interface. aor: True to add, False to remove.
interface = globals()[f'interface{nodeInt}']
myNodeNumber = globals().get(f'myNodeNum{nodeInt}')
if aor:
interface.getNode(myNodeNumber).setFavorite(nodeID)
logger.info(f"System: Added {nodeID} to favorites for device {nodeInt}")
else:
interface.getNode(myNodeNumber).removeFavorite(nodeID)
logger.info(f"System: Removed {nodeID} from favorites for device {nodeInt}")
try:
if aor:
result = interface.getNode(myNodeNumber).setFavorite(nodeID)
logger.info(f"System: Added {nodeID} to favorites for device {nodeInt}")
else:
result = interface.getNode(myNodeNumber).removeFavorite(nodeID)
logger.info(f"System: Removed {nodeID} from favorites for device {nodeInt}")
return result
except Exception as e:
logger.error(f"System: Error handling favorite node {nodeID} on device {nodeInt}: {e}")
return None
def getFavoritNodes(nodeInt=1):
interface = globals()[f'interface{nodeInt}']
@@ -1217,22 +1228,22 @@ def consumeMetadata(packet, rxNode=0, channel=-1):
except Exception as e:
logger.debug(f"System: TELEMETRY_APP iaq error: Device: {rxNode} Channel: {channel} {e} packet {packet}")
# Track localStats
# if telemetry_packet.get('localStats'):
# localStats = telemetry_packet['localStats']
# try:
# # Check if 'numPacketsTx' and 'numPacketsRx' exist and are not zero
# if localStats.get('numPacketsTx') is not None and localStats.get('numPacketsRx') is not None and localStats['numPacketsTx'] != 0:
# # Assign the values to the telemetry dictionary
# keys = [
# 'numPacketsTx', 'numPacketsRx', 'numOnlineNodes',
# 'numOfflineNodes', 'numPacketsTxErr', 'numPacketsRxErr', 'numTotalNodes']
# for key in keys:
# if localStats.get(key) is not None:
# telemetryData[rxNode][key] = localStats.get(key)
# except Exception as e:
# logger.debug(f"System: TELEMETRY_APP localStats error: Device: {rxNode} Channel: {channel} {e} packet {packet}")
# POSITION_APP packets
# Collect localStats for telemetryData
if telemetry_packet.get('localStats'):
localStats = telemetry_packet['localStats']
try:
# Check if 'numPacketsTx' and 'numPacketsRx' exist and are not zero
if localStats.get('numPacketsTx') is not None and localStats.get('numPacketsRx') is not None and localStats['numPacketsTx'] != 0:
# Assign the values to the telemetry dictionary
keys = [
'numPacketsTx', 'numPacketsRx', 'numOnlineNodes',
'numOfflineNodes', 'numPacketsTxErr', 'numPacketsRxErr', 'numTotalNodes']
for key in keys:
if localStats.get(key) is not None:
telemetryData[rxNode][key] = localStats.get(key)
except Exception as e:
logger.debug(f"System: TELEMETRY_APP localStats error: Device: {rxNode} Channel: {channel} {e} packet {packet}")
#POSITION_APP packets
if packet_type == 'POSITION_APP':
try:
if debugMetadata and 'POSITION_APP' not in metadataFilter:

View File

@@ -28,6 +28,7 @@ if __name__ == "__main__":
# welcome header
print("meshing-around: addFav - Auto-Add favorite nodes to all interfaces from config.ini data")
print("This script may need API improvments still in progress")
print("---------------------------------------------------------------")
try:
@@ -93,7 +94,7 @@ try:
count_devices = set([fav['deviceID'] for fav in favList])
count_nodes = set([fav['nodeID'] for fav in favList])
for fav in favList:
print(f"Device: {fav.get('deviceID', 'N/A')} Node: {fav.get('nodeID', 'N/A')} Interface: {fav.get('interface', 'N/A')}")
print(f"addFav: adding nodeID {fav['nodeID']} meshtastic --set-favorite-node {fav['nodeID']}")
confirm = input(f"Are you sure you want to add these {len(count_nodes)} favorite nodes to {len(count_devices)} device(s)? (y/n): ").strip().lower()
if confirm != 'y':
print("Operation cancelled by user.")
@@ -109,8 +110,9 @@ if favList:
# for each node,interface tuple add the favorite node
for fav in favList:
try:
handleFavoritNode(fav['deviceID'], fav['nodeID'], True)
time.sleep(1)
handleFavoriteNode(fav['deviceID'], fav['nodeID'], True)
logger.info(f"addFav: waiting 15 seconds to avoid API rate limits")
time.sleep(15) # wait to avoid API rate limits
except Exception as e:
logger.error(f"addFav: Error adding favorite node {fav['nodeID']} to device {fav['deviceID']}: {e}")
else: