Compare commits

...

10 Commits

Author SHA1 Message Date
Kelly 8a0eb62574 fixBug 2026-05-04 18:38:40 -07:00
Kelly 873509b3cc tidySolarData 2026-05-04 18:36:50 -07:00
Kelly b03c5c9c2e fixSchedDay
day + time + interval=1 uses daily at time.
day + time + interval>1 uses every N days at time.
day without time uses every N days.
2026-05-04 17:39:59 -07:00
Kelly 2ce976ca8a superfluous redundant 2026-05-04 17:11:51 -07:00
Kelly 76a5913e08 threaded response
all messages tap-back with a thread now in a channel @NomDeTom for the idea :)
2026-05-04 16:41:50 -07:00
Kelly 3819791fcd Update mesh_bot.py
better protection for memory and bad data
2026-04-24 20:57:11 -07:00
Kelly 9fe580a3cb Update launch.sh 2026-04-18 17:17:56 -07:00
Kelly 8567c3ad84 Update launch.sh 2026-04-18 15:35:30 -07:00
Kelly f68f7f10ca logger
logger enhance https://github.com/SpudGunMan/meshing-around/issues/308
2026-04-18 15:30:17 -07:00
dependabot[bot] a02025d4a0 Bump docker/build-push-action from 7.0.0 to 7.1.0 (#307)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 7.0.0 to 7.1.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/d08e5c354a6adb9ed34480a06d141179aa583294...bcafcacb16a39f128d818304e6c9c0c18556b85f)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 18:30:45 -07:00
7 changed files with 139 additions and 61 deletions
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
- name: Build and push Docker image
id: push
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f
with:
context: .
push: true
+2 -2
View File
@@ -326,9 +326,9 @@ schedulerMotd = False
# 'tide' (time/day), 'solar' (time/day) for automated information broadcasts, matching module needs enabled!
# 'custom' for module/scheduler.py custom schedule examples
value =
# interval to use when time is not set (e.g. every 2 days)
# interval for recurring schedules (e.g. every 2 days, or every 2 days at a set time)
interval =
# time of day in 24:00 hour format when value is 'day' and interval is not set
# time of day in 24:00 hour format when value is 'day' (optional with interval)
# Process run :00,:20,:40 try and vary the 20 minute offsets to avoid collision
time =
+1 -1
View File
@@ -19,7 +19,7 @@ fi
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
export HOME=$(pwd)
# launch the application
if [[ "$1" == pong* ]]; then
python3 pong_bot.py
+68 -34
View File
@@ -591,6 +591,11 @@ llmRunCounter = 0
llmTotalRuntime = []
llmLocationTable = [{'nodeID': 1234567890, 'location': 'No Location'},]
# Runtime safety caps to avoid unbounded growth on long-lived systems.
MAX_SEEN_NODES = 5000
MAX_LLM_LOCATION_ENTRIES = 50
MAX_LLM_RUNTIME_SAMPLES = 50
def handle_satpass(message_from_id, deviceID, message='', vox=False):
if vox:
location = (my_settings.latitudeValue, my_settings.longitudeValue)
@@ -656,6 +661,8 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
# likely a DM
user_input = message
# consider this a command use for the cmdHistory list
if len(cmdHistory) > 50:
cmdHistory.pop(0)
cmdHistory.append({'nodeID': message_from_id, 'cmd': 'llm-use', 'time': time.time()})
# check for a welcome message (is this redundant?)
@@ -679,6 +686,8 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
# if not in table add the location
if not any(d['nodeID'] == message_from_id for d in llmLocationTable):
llmLocationTable.append({'nodeID': message_from_id, 'location': location_name})
if len(llmLocationTable) > MAX_LLM_LOCATION_ENTRIES:
llmLocationTable = llmLocationTable[-MAX_LLM_LOCATION_ENTRIES:]
user_input = user_input.strip()
@@ -709,6 +718,8 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
end = time.time()
llmRunCounter += 1
llmTotalRuntime.append(end - start)
if len(llmTotalRuntime) > MAX_LLM_RUNTIME_SAMPLES:
llmTotalRuntime = llmTotalRuntime[-MAX_LLM_RUNTIME_SAMPLES:]
return response
@@ -1854,14 +1865,23 @@ def onReceive(packet, interface):
# Priocess the incoming packet, handles the responses to the packet with auto_response()
# Sends the packet to the correct handler for processing
if not isinstance(packet, dict):
logger.warning(f"System: Ignoring malformed packet type: {type(packet).__name__}")
return
decoded = packet.get('decoded')
if not isinstance(decoded, dict):
decoded = {}
# extract interface details from inbound packet
rxType = type(interface).__name__
# Values assinged to the packet
packet_id = None
rxNode = message_from_id = snr = rssi = hop = hop_away = channel_number = hop_start = hop_count = hop_limit = 0
pkiStatus = (False, 'ABC')
rxNodeHostName = None
replyIDset = False
replyIDset = None
emojiSeen = False
simulator_flag = False
isDM = False
@@ -1899,8 +1919,8 @@ def onReceive(packet, interface):
if rxNode is None:
# default to interface 1 ## FIXME needs better like a default interface setting or hash lookup
if 'decoded' in packet and packet['decoded']['portnum'] in ['ADMIN_APP', 'SIMULATOR_APP']:
session_passkey = packet.get('decoded', {}).get('admin', {}).get('sessionPasskey', None)
if decoded.get('portnum') in ['ADMIN_APP', 'SIMULATOR_APP']:
session_passkey = decoded.get('admin', {}).get('sessionPasskey', None)
rxNode = 1
# check if the packet has a channel flag use it ## FIXME needs to be channel hash lookup
@@ -1940,17 +1960,22 @@ def onReceive(packet, interface):
# logger.debug(f"System: Received Packet on Channel:{channel_number} Name:{channel_name} on Interface:{rxNode}")
# check if the packet has a simulator flag
simulator_flag = packet.get('decoded', {}).get('simulator', False)
simulator_flag = decoded.get('simulator', False)
if isinstance(simulator_flag, dict):
# assume Software Simulator
simulator_flag = True
# set the message_from_id
message_from_id = packet['from']
message_from_id = packet.get('from')
if message_from_id is None:
logger.warning(f"System: Ignoring packet missing 'from' field on Device:{rxNode}")
return
# if message_from_id is not in the seenNodes list add it
if not any(node.get('nodeID') == message_from_id for node in seenNodes):
seenNodes.append({'nodeID': message_from_id, 'rxInterface': rxNode, 'channel': channel_number, 'welcome': False, 'first_seen': time.time(), 'lastSeen': time.time()})
if len(seenNodes) > MAX_SEEN_NODES:
seenNodes = seenNodes[-MAX_SEEN_NODES:]
else:
# update lastSeen time
for node in seenNodes:
@@ -1958,7 +1983,7 @@ def onReceive(packet, interface):
node['lastSeen'] = time.time()
break
# BBS DM MAIL CHECKER
if bbs_enabled and 'decoded' in packet:
if bbs_enabled and decoded:
msg = bbs_check_dm(message_from_id)
if msg:
logger.info(f"System: BBS DM Delivery: {msg[1]} For: {get_name_from_number(message_from_id, 'long', rxNode)}")
@@ -1973,18 +1998,25 @@ def onReceive(packet, interface):
# handle TEXT_MESSAGE_APP
try:
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
message_bytes = packet['decoded']['payload']
message_string = message_bytes.decode('utf-8')
via_mqtt = packet['decoded'].get('viaMqtt', False)
if decoded.get('portnum') == 'TEXT_MESSAGE_APP':
message_bytes = decoded.get('payload', b'')
if isinstance(message_bytes, bytes):
message_string = message_bytes.decode('utf-8', errors='replace')
elif isinstance(message_bytes, str):
message_string = message_bytes
else:
logger.warning(f"System: Ignoring TEXT_MESSAGE_APP with invalid payload type: {type(message_bytes).__name__}")
return
message_log_string = message_string.replace('\r', ' ').replace('\n', ' ')
via_mqtt = decoded.get('viaMqtt', False)
transport_mechanism = (
packet.get('transport_mechanism')
or packet.get('transportMechanism')
or (packet.get('decoded', {}).get('transport_mechanism'))
or (packet.get('decoded', {}).get('transportMechanism'))
or decoded.get('transport_mechanism')
or decoded.get('transportMechanism')
or 'unknown'
)
rx_time = packet['decoded'].get('rxTime', time.time())
rx_time = decoded.get('rxTime', time.time())
# check if the packet is from us
if message_from_id in [myNodeNum1, myNodeNum2, myNodeNum3, myNodeNum4, myNodeNum5, myNodeNum6, myNodeNum7, myNodeNum8, myNodeNum9]:
@@ -1999,9 +2031,11 @@ def onReceive(packet, interface):
if packet.get('publicKey'):
pkiStatus = packet.get('pkiEncrypted', False), packet.get('publicKey', 'ABC')
# check if the packet has replyId flag // currently unused in the code
if packet.get('replyId'):
replyIDset = packet.get('replyId', False)
# Use packet id for threaded replies;
packet_id = packet.get('id', None)
# existing reply - unused for tracking
replyIDSet = packet.get('replyIDSet', None)
# check if the packet has emoji flag set it // currently unused in the code
if packet.get('emoji'):
@@ -2066,13 +2100,13 @@ def onReceive(packet, interface):
return
# If the packet is a DM (Direct Message) respond to it, otherwise validate its a message for us on the channel
if packet['to'] in [myNodeNum1, myNodeNum2, myNodeNum3, myNodeNum4, myNodeNum5, myNodeNum6, myNodeNum7, myNodeNum8, myNodeNum9]:
if packet.get('to') in [myNodeNum1, myNodeNum2, myNodeNum3, myNodeNum4, myNodeNum5, myNodeNum6, myNodeNum7, myNodeNum8, myNodeNum9]:
# message is DM to us
isDM = True
# check if the message contains a trap word, DMs are always responded to
if (messageTrap(message_string) and not llm_enabled) or messageTrap(message_string.split()[0]):
# log the message to stdout
logger.info(f"Device:{rxNode} Channel: {channel_number} " + CustomFormatter.green + f"Received DM: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
logger.info(f"Device:{rxNode} Channel: {channel_number} " + CustomFormatter.green + f"Received DM: " + CustomFormatter.white + f"{message_log_string} " + CustomFormatter.purple +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
# respond with DM
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
@@ -2082,7 +2116,7 @@ def onReceive(packet, interface):
playingGame = checkPlayingGame(message_from_id, message_string, rxNode, channel_number)
elif hop_count >= my_settings.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}")
logger.warning(f"Device:{rxNode} Ignoring Request to Play Game: {message_log_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)
else:
playingGame = False
@@ -2096,7 +2130,7 @@ def onReceive(packet, interface):
send_message(llm, channel_number, message_from_id, rxNode)
else:
# respond with welcome message on DM
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
logger.warning(f"Device:{rxNode} Ignoring DM: {message_log_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
# if seenNodes list is not marked as welcomed send welcome message
if not any(node['nodeID'] == message_from_id and node['welcome'] == True for node in seenNodes):
@@ -2122,26 +2156,26 @@ def onReceive(packet, interface):
# log the message to the message log
if log_messages_to_file:
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | DM | " + message_string.replace('\n', '-nl-'))
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | DM | " + message_log_string)
else:
# message is on a channel
if messageTrap(message_string):
# message is for us to respond to, or is it...
if my_settings.ignoreDefaultChannel and channel_number == my_settings.publicChannel:
logger.debug(f"System: Ignoring CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Default Channel:{channel_number}")
logger.debug(f"System: Ignoring CMD:{message_log_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Default Channel:{channel_number}")
elif str(message_from_id) in my_settings.bbs_ban_list:
logger.debug(f"System: Ignoring CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Cantankerous Node")
logger.debug(f"System: Ignoring CMD:{message_log_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Cantankerous Node")
elif str(channel_number) in my_settings.ignoreChannels:
logger.debug(f"System: Ignoring CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Ignored Channel:{channel_number}")
logger.debug(f"System: Ignoring CMD:{message_log_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Ignored Channel:{channel_number}")
elif my_settings.cmdBang and not message_string.startswith("!"):
logger.debug(f"System: Ignoring CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Didnt sound like they meant it")
logger.debug(f"System: Ignoring CMD:{message_log_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Didnt sound like they meant it")
else:
# message is for bot to respond to, seriously this time..
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "ReceivedChannel: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "ReceivedChannel: " + CustomFormatter.white + f"{message_log_string} " + CustomFormatter.purple +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
if my_settings.useDMForResponse:
# respond to channel message via direct message
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode, reply_id=packet_id)
else:
# or respond to channel message on the channel itself
if channel_number == my_settings.publicChannel and my_settings.antiSpam:
@@ -2149,10 +2183,10 @@ def onReceive(packet, interface):
logger.warning(f"System: AntiSpam protection, sending DM to: {get_name_from_number(message_from_id, 'long', rxNode)}")
# respond to channel message via direct message
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode, reply_id=packet_id)
else:
# respond to channel message on the channel itself
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, 0, rxNode)
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, 0, rxNode, reply_id=packet_id)
else:
# message is not for us to respond to
@@ -2172,9 +2206,9 @@ def onReceive(packet, interface):
# print the message to the log and sdout
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Ignoring Message:" + CustomFormatter.white +\
f" {message_string} " + CustomFormatter.purple + "From:" + CustomFormatter.white + f" {get_name_from_number(message_from_id)}")
f" {message_log_string} " + CustomFormatter.purple + "From:" + CustomFormatter.white + f" {get_name_from_number(message_from_id)}")
if my_settings.log_messages_to_file:
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_log_string)
# repeat the message on the other device
if my_settings.repeater_enabled and my_settings.multiple_interface:
@@ -2204,7 +2238,7 @@ def onReceive(packet, interface):
hello(message_from_id, name)
# send a hello message as a DM
if not my_settings.train_qrz:
send_message(f"Hello {name} {qrz_hello_string}", channel_number, message_from_id, rxNode)
send_message(f"Hello {name} {qrz_hello_string}", channel_number, message_from_id, rxNode, reply_id=packet_id)
# handle mini games
if my_settings.wordOfTheDay:
@@ -2232,8 +2266,8 @@ def onReceive(packet, interface):
else:
# Evaluate non TEXT_MESSAGE_APP packets
consumeMetadata(packet, rxNode, channel_number)
except KeyError as e:
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
except Exception as e:
logger.exception(f"System: Error processing packet: {e} Device:{rxNode}")
logger.debug(f"System: Error Packet = {packet}")
async def start_rx():
+13 -4
View File
@@ -104,14 +104,23 @@ def setup_scheduler(
# Basic Scheduler Options
basicOptions = ['day', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun', 'hour', 'min']
effective_interval = schedulerIntervalInt
if any(option in schedulerValue for option in basicOptions):
if schedulerValue == 'day':
day_interval = safe_int(schedulerInterval, 1, type="interval")
if day_interval < 1:
logger.debug(f"System: Scheduler config interval '{schedulerInterval}' invalid for day schedule, using default 1")
day_interval = 1
effective_interval = day_interval
if schedulerTime:
# Specific time each day
schedule.every().day.at(schedulerTime).do(send_sched_msg)
# Specific time at a daily or multi-day interval
if day_interval == 1:
schedule.every().day.at(schedulerTime).do(send_sched_msg)
else:
schedule.every(day_interval).days.at(schedulerTime).do(send_sched_msg)
else:
# Every N days
schedule.every(schedulerIntervalInt).days.do(send_sched_msg)
schedule.every(day_interval).days.do(send_sched_msg)
elif 'mon' in schedulerValue and schedulerTime:
schedule.every().monday.at(schedulerTime).do(send_sched_msg)
elif 'tue' in schedulerValue and schedulerTime:
@@ -130,7 +139,7 @@ def setup_scheduler(
schedule.every(schedulerIntervalInt).hours.do(send_sched_msg)
elif 'min' in schedulerValue:
schedule.every(schedulerIntervalInt).minutes.do(send_sched_msg)
logger.debug(f"System: Starting the basic scheduler to send '{scheduler_message}' on schedule '{schedulerValue}' every {schedulerIntervalInt} interval at time '{schedulerTime}' on Device:{schedulerInterface} Channel:{schedulerChannel}")
logger.debug(f"System: Starting the basic scheduler to send '{scheduler_message}' on schedule '{schedulerValue}' every {effective_interval} interval at time '{schedulerTime}' on Device:{schedulerInterface} Channel:{schedulerChannel}")
elif 'joke' in schedulerValue:
schedule.every(schedulerIntervalInt).minutes.do(
lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface)
+7 -1
View File
@@ -37,6 +37,12 @@ def hf_band_conditions():
def solar_conditions():
# radio related solar conditions from hamsql.com
solar_cond = ""
solar_a_index = ""
solar_k_index = ""
solar_xray = ""
solar_flux = ""
sunspots = ""
signalnoise = ""
try:
solar_cond = requests.get("https://www.hamqsl.com/solarxml.php", timeout=urlTimeoutSeconds)
if solar_cond.ok:
@@ -52,7 +58,7 @@ def solar_conditions():
solar_flux = i.getElementsByTagName("solarflux")[0].childNodes[0].data
sunspots = i.getElementsByTagName("sunspots")[0].childNodes[0].data
signalnoise = i.getElementsByTagName("signalnoise")[0].childNodes[0].data
solar_cond = "A-Index: " + solar_a_index + "\nK-Index: " + solar_k_index + "\nSunspots: " + sunspots + "\nX-Ray Flux: " + solar_xray + "\nSolar Flux: " + solar_flux + "\nSignal Noise: " + signalnoise
solar_cond = "A: " + solar_a_index + "\nK: " + solar_k_index + "\nSunspots: " + sunspots + "\nX-Ray Flux: " + solar_xray + "\nSolar Flux: " + solar_flux + "\nNoise: " + signalnoise
else:
logger.error("Solar: Error fetching solar conditions")
solar_cond = ERROR_FETCHING_DATA
+47 -18
View File
@@ -852,7 +852,7 @@ def messageChunker(message):
except Exception as e:
logger.warning(f"System: Exception during message chunking: {e} (message length: {len(message)})")
def send_message(message, ch, nodeid=0, nodeInt=1, bypassChuncking=False):
def send_message(message, ch, nodeid=0, nodeInt=1, bypassChuncking=False, reply_id=None):
# Send a message to a channel or DM
interface = globals()[f'interface{nodeInt}']
# Check if the message is empty
@@ -860,6 +860,28 @@ def send_message(message, ch, nodeid=0, nodeInt=1, bypassChuncking=False):
return False
try:
def _send_with_reply(**kwargs):
# For threaded replies, send as DATA payload to match Meshtastic inline-reply behavior. no API call today.
if reply_id is not None:
text_payload = kwargs.pop('text', '')
if isinstance(text_payload, str):
raw_payload = text_payload.encode('utf-8')
else:
raw_payload = text_payload
data_kwargs = {
# 1 == TEXT_MESSAGE_APP, required so clients render payload as chat text.
'portNum': 1,
'channelIndex': kwargs.get('channelIndex', ch),
'wantAck': kwargs.get('wantAck', wantAck),
}
if kwargs.get('destinationId'):
data_kwargs['destinationId'] = kwargs.get('destinationId')
# send the data payload with the replyId for threading
return interface.sendData(raw_payload, replyId=reply_id, **data_kwargs)
# Otherwise, send as normal text message
return interface.sendText(**kwargs)
# Force chunking and log if message exceeds maxBuffer
if len(message.encode('utf-8')) > maxBuffer:
logger.debug(f"System: Message length {len(message.encode('utf-8'))} exceeds maxBuffer{maxBuffer}, forcing chunking.")
@@ -880,20 +902,20 @@ def send_message(message, ch, nodeid=0, nodeInt=1, bypassChuncking=False):
# Send to channel
if wantAck:
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + f"req.ACK " + f"Chunker{chunkOf} SendingChannel: " + CustomFormatter.white + m.replace('\n', ' '))
interface.sendText(text=m, channelIndex=ch, wantAck=True)
_send_with_reply(text=m, channelIndex=ch, wantAck=True)
else:
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + f"Chunker{chunkOf} SendingChannel: " + CustomFormatter.white + m.replace('\n', ' '))
interface.sendText(text=m, channelIndex=ch)
_send_with_reply(text=m, channelIndex=ch)
else:
# Send to DM
if wantAck:
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + f"req.ACK " + f"Chunker{chunkOf} Sending DM: " + CustomFormatter.white + m.replace('\n', ' ') + CustomFormatter.purple +\
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
interface.sendText(text=m, channelIndex=ch, destinationId=nodeid, wantAck=True)
_send_with_reply(text=m, channelIndex=ch, destinationId=nodeid, wantAck=True)
else:
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + f"Chunker{chunkOf} Sending DM: " + CustomFormatter.white + m.replace('\n', ' ') + CustomFormatter.purple +\
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
interface.sendText(text=m, channelIndex=ch, destinationId=nodeid)
_send_with_reply(text=m, channelIndex=ch, destinationId=nodeid)
# Throttle the message sending to prevent spamming the device
if (message_list.index(m)+1) % 4 == 0:
@@ -908,20 +930,20 @@ def send_message(message, ch, nodeid=0, nodeInt=1, bypassChuncking=False):
# Send to channel
if wantAck:
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "req.ACK " + "SendingChannel: " + CustomFormatter.white + message.replace('\n', ' '))
interface.sendText(text=message, channelIndex=ch, wantAck=True)
_send_with_reply(text=message, channelIndex=ch, wantAck=True)
else:
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "SendingChannel: " + CustomFormatter.white + message.replace('\n', ' '))
interface.sendText(text=message, channelIndex=ch)
_send_with_reply(text=message, channelIndex=ch)
else:
# Send to DM
if wantAck:
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "req.ACK " + "Sending DM: " + CustomFormatter.white + message.replace('\n', ' ') + CustomFormatter.purple +\
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
interface.sendText(text=message, channelIndex=ch, destinationId=nodeid, wantAck=True)
_send_with_reply(text=message, channelIndex=ch, destinationId=nodeid, wantAck=True)
else:
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending DM: " + CustomFormatter.white + message.replace('\n', ' ') + CustomFormatter.purple +\
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
interface.sendText(text=message, channelIndex=ch, destinationId=nodeid)
_send_with_reply(text=message, channelIndex=ch, destinationId=nodeid)
# Throttle the message sending to prevent spamming the device
time.sleep(responseDelay)
return True
@@ -929,17 +951,24 @@ def send_message(message, ch, nodeid=0, nodeInt=1, bypassChuncking=False):
logger.error(f"System: Exception during send_message: {e} (message length: {len(message)})")
return False
def send_raw_bytes(nodeid, raw_bytes, nodeInt=1, channel=0, portnum=256, want_ack=True):
def send_raw_bytes(nodeid, raw_bytes, nodeInt=1, channel=0, portnum=256, want_ack=True, reply_id=None):
# Send raw bytes to a node using the Meshtastic interface.
interface = globals()[f'interface{nodeInt}']
try:
interface.sendData(
raw_bytes,
destinationId=nodeid,
portNum=portnum,
channelIndex=channel,
wantAck=want_ack
)
send_kwargs = {
'destinationId': nodeid,
'portNum': portnum,
'channelIndex': channel,
'wantAck': want_ack,
}
if reply_id is not None:
try:
interface.sendData(raw_bytes, replyId=reply_id, **send_kwargs)
except TypeError:
logger.debug("System: replyId/replyID unsupported for sendData; sending without threaded reply")
interface.sendData(raw_bytes, **send_kwargs)
else:
interface.sendData(raw_bytes, **send_kwargs)
# Throttle the message sending to prevent spamming the device
logger.debug(f"System: Sent raw bytes to {nodeid} on portnum {portnum} via Device{nodeInt}")
time.sleep(responseDelay)
@@ -2442,7 +2471,7 @@ def saveAllData():
save_bbsBanList()
logger.debug("Persistence: Ban list saved")
logger.info("Persistence: Save completed")
#logger.info("Persistence: Save completed")
except Exception as e:
logger.error(f"Persistence: Save error: {e}")