forked from iarv/meshing-around
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b7d795a31 | ||
|
|
1f093c4bc2 | ||
|
|
fe1c4a1ad0 | ||
|
|
11687cb7ba | ||
|
|
b07a7fb0cc | ||
|
|
b876d87ba9 | ||
|
|
0a63e89633 | ||
|
|
848f5609c2 | ||
|
|
0ccbed6165 | ||
|
|
646517db71 | ||
|
|
7d347bb80a | ||
|
|
e199d4f5eb | ||
|
|
a9767b58c4 | ||
|
|
69dfde047e | ||
|
|
da33b6f1b9 | ||
|
|
8a7125358b | ||
|
|
ae558052f7 | ||
|
|
5074d71eb7 | ||
|
|
632f42477a | ||
|
|
b3df38d15e | ||
|
|
b76b8ca718 | ||
|
|
d66a9e745b | ||
|
|
717bbccea3 | ||
|
|
50fd1c0410 | ||
|
|
ae89788ea4 | ||
|
|
4220b095ee | ||
|
|
ef28341cdb | ||
|
|
b5d610728c | ||
|
|
bc238ef476 | ||
|
|
feb3544014 | ||
|
|
31322dc0cd |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
160
mesh_bot.py
160
mesh_bot.py
@@ -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:
|
||||
|
||||
@@ -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)})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
204
modules/radio.py
204
modules/radio.py
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user