Compare commits

..

206 Commits

Author SHA1 Message Date
SpudGunMan
410d32947c Update system.py 2025-07-21 20:13:07 -07:00
SpudGunMan
748652ac62 onDisconnect
correcting multiple issues adding config.ini feature for dont_retry_disconnect

https://github.com/SpudGunMan/meshing-around/issues/137
https://github.com/SpudGunMan/meshing-around/issues/156
2025-07-21 20:10:59 -07:00
SpudGunMan
d715cb6b4d Update system.py 2025-07-21 04:33:04 -07:00
SpudGunMan
1895a365ae fix retry and failure
correcting multiple issues with some bad code

https://github.com/SpudGunMan/meshing-around/issues/137

https://github.com/SpudGunMan/meshing-around/issues/156
2025-07-20 05:41:50 -07:00
SpudGunMan
cc58a38165 Update system.py 2025-07-18 21:24:55 -07:00
SpudGunMan
a8ccb05d56 Update system.py
bug of undefined for interface retry
2025-07-16 08:57:39 -07:00
SpudGunMan
a90a533a30 USGS Alerts
documented
2025-07-15 21:20:37 -07:00
SpudGunMan
57a4e5d68c Update README.md 2025-07-15 21:12:23 -07:00
SpudGunMan
7c99b684ad riverflow
never got documented well
2025-07-15 21:11:48 -07:00
SpudGunMan
b957c89d70 Update README.md 2025-07-15 20:32:51 -07:00
SpudGunMan
9b986dd57a Update locationdata.py
allow FIPS only
2025-07-15 20:30:43 -07:00
SpudGunMan
9e348332e5 SAME code back in iPAWS
the state only FIPS codes are too wide
2025-07-15 19:09:27 -07:00
SpudGunMan
0cfe759ef6 Update mesh_bot.py 2025-07-15 15:34:32 -07:00
SpudGunMan
e95902ef98 fix Excessive queries to FEMA
issue raised https://github.com/SpudGunMan/meshing-around/issues/165

Co-Authored-By: DEVAFRS <180097515+devafrs@users.noreply.github.com>
2025-07-15 15:22:53 -07:00
SpudGunMan
c7df4d88d1 Update config.template
Co-Authored-By: Russell Schmidt <836646+rfschmid@users.noreply.github.com>
2025-07-15 14:28:25 -07:00
SpudGunMan
6d01c5a986 further adjustments for 2.7.2
https://github.com/SpudGunMan/meshing-around/pull/164#pullrequestreview-3021705851

and

https: //github.com/SpudGunMan/meshing-around/issues/162
Co-Authored-By: SudoRand <25190078+sudorand@users.noreply.github.com>
2025-07-15 11:14:29 -07:00
SpudGunMan
3f882dcfcd fix message.log
fixing issue for log in https://github.com/SpudGunMan/meshing-around/pull/161

Co-Authored-By: SudoRand <25190078+sudorand@users.noreply.github.com>
2025-07-15 09:41:34 -07:00
SpudGunMan
b146fd6f64 Revert "enhance sysinfo"
This reverts commit 8709e5aed5.
2025-07-14 22:55:41 -07:00
SpudGunMan
8709e5aed5 enhance sysinfo
ChUtil/Node value
2025-07-14 22:13:49 -07:00
SpudGunMan
caf8a2708b Update log.py
fix time display
2025-07-14 22:04:22 -07:00
SpudGunMan
9b4200c198 Update config.template
adjustments for 2.7.2 firmware might change again
2025-07-14 21:54:30 -07:00
SpudGunMan
0a260b28b6 Update llm.py
last time?
2025-06-26 19:30:33 -07:00
SpudGunMan
3f5c6f2e9a Update llm.py 2025-06-26 19:03:13 -07:00
SpudGunMan
8a4f7a904a Update llm.py 2025-06-26 18:49:16 -07:00
SpudGunMan
0bc3d392cf fix Interface logic
a condition where TCP interfaces can fail leaving a none condition. this should resolve the errored interface better.
2025-06-25 07:30:46 -07:00
SpudGunMan
5eaef8b5b8 Update sysEnv.sh
enhance with git update check

Co-Authored-By: Johannes le Roux <dade@dade.co.za>
2025-06-25 07:25:04 -07:00
SpudGunMan
3a0007771d Update update.sh 2025-06-22 20:04:18 -07:00
SpudGunMan
67ba2b1fb5 Create update.sh 2025-06-22 19:59:31 -07:00
SpudGunMan
f2e7a9aa5c Update config.template 2025-06-18 12:15:44 -07:00
SpudGunMan
9d22270dde highfly_ignoreList
some nodes have bad altimeters
2025-06-18 12:14:14 -07:00
SpudGunMan
409d07436e Update settings.py 2025-06-10 11:22:32 -07:00
SpudGunMan
5ab0001f2b Update system.py
https://github.com/SpudGunMan/meshing-around/issues/154
2025-06-08 20:32:28 -07:00
SpudGunMan
5e34537af7 fix
reference https://github.com/SpudGunMan/meshing-around/issues/154
2025-06-08 19:26:51 -07:00
SpudGunMan
1764bdf4f3 enhance
the database page to include qrz.db
2025-06-07 21:05:49 -07:00
SpudGunMan
2290f07351 Update locationdata.py 2025-06-06 17:54:31 -07:00
SpudGunMan
ee01051cf7 Update config.template 2025-06-06 17:54:29 -07:00
Kelly
de50a52fa6 Merge pull request #153 from rfschmid/allow-dms-to-numeric-short-names 2025-06-05 19:38:44 -07:00
SpudGunMan
8eabfaa9c4 enhance ignore logic 2025-06-04 10:37:58 -07:00
Russell Schmidt
ca7114b058 Allow DMs to numeric short names 2025-06-04 11:55:30 -05:00
Kelly
8b94dc8111 Merge pull request #152 from SudoRand/main 2025-06-01 19:03:01 -07:00
SudoRand
5b26aabb00 Config for whether ollama responds to non-commands
The current behavior is that whenever ollama is enabled the LLM replies
to all non-command message. This setting allows limiting it the LLM to
run only in response to the "ask:" and "askai" commands.

Default is True to keep consistent with previous behavior.
2025-06-01 15:32:08 -06:00
SudoRand
67b3c67348 Fix bug that always enabled news_random_line_only
This looks like a simple typo that accidentally used read_news_enabled
for the news_random_line_only parameter. As a result, the
news_random_line_only setting was always treated as True (since this
line it only executed if read_news_enabled was True). Now it obeys the
configuration value.
2025-06-01 14:02:19 -06:00
SpudGunMan
860cceec59 Update locationdata.py 2025-05-28 15:20:33 -07:00
Kelly
53a0535e55 Merge pull request #149 from SpudGunMan/lab
High Altitude Alerts
2025-05-23 19:14:51 -07:00
SpudGunMan
621f4ad916 enhance 2025-05-23 19:10:15 -07:00
SpudGunMan
118857ec15 Update system.py 2025-05-21 16:52:01 -07:00
SpudGunMan
1be13be92a highfly enhancements 2025-05-21 16:43:56 -07:00
SpudGunMan
895fc3fd37 Update README.md 2025-05-21 16:36:55 -07:00
SpudGunMan
0e0bda60ad Update system.py 2025-05-21 16:36:52 -07:00
SpudGunMan
903767f4b3 Update settings.py 2025-05-21 16:36:46 -07:00
SpudGunMan
f54d362ea0 Update config.template 2025-05-21 16:36:43 -07:00
SpudGunMan
60bb68c6b5 Update system.py 2025-05-21 16:20:54 -07:00
SpudGunMan
feb9a1d9b3 FlightDetector
high alt detection
2025-05-21 16:18:46 -07:00
SpudGunMan
d055c35c96 Update install.sh 2025-05-11 13:07:52 -07:00
SpudGunMan
27820daaf4 Cron4W3 2025-05-10 15:34:33 -07:00
SpudGunMan
56e8e1c0d5 🐇🪵
config register set in config.ini for hop logs
2025-05-01 10:35:43 -07:00
SpudGunMan
4545b8f4a4 Update locationdata.py 2025-04-28 23:10:52 -07:00
SpudGunMan
6ed48d49ce Update videopoker.py
fix gameplay
2025-04-24 15:54:47 -07:00
SpudGunMan
a3a54b081d Update system.py 2025-04-23 19:34:13 -07:00
SpudGunMan
ab420af63e Update system.py 2025-04-23 09:24:51 -07:00
SpudGunMan
a55c61c47d Update web.py 2025-04-23 07:17:02 -07:00
SpudGunMan
7236f47eb7 Update README.md 2025-04-17 13:30:53 -07:00
SpudGunMan
05e11ae5f8 Update README.md 2025-04-13 10:04:15 -07:00
SpudGunMan
f8ffcc19b1 Update config.template 2025-04-13 10:04:13 -07:00
SpudGunMan
ea20eec604 Update README.md 2025-04-11 14:51:50 -07:00
SpudGunMan
d1204d2c26 ignoreEAS and USGS alert word
enhance with list to ignore words not wanted for broadcast
2025-04-11 14:32:51 -07:00
SpudGunMan
654d8b3ff7 Update README.md 2025-04-10 16:14:32 -07:00
SpudGunMan
3bf12d62b5 Update mesh_bot.py 2025-04-10 16:09:15 -07:00
SpudGunMan
0ec8613d27 Update install.sh
issues raised https://github.com/SpudGunMan/meshing-around/issues/139
2025-04-10 15:57:56 -07:00
SpudGunMan
10dd413ae7 Update config.template 2025-04-10 15:50:12 -07:00
SpudGunMan
09ac7525b3 Update mesh_bot.py 2025-04-10 15:48:47 -07:00
SpudGunMan
aac497dfa0 config.ini Scheduler enhancments
request from https://github.com/SpudGunMan/meshing-around/issues/141

enhances with a basic announcement from config.ini
2025-04-10 15:46:36 -07:00
SpudGunMan
6f652230b0 fixQRZ formatting
and enhance saving names without info packet
2025-04-04 12:00:09 -07:00
SpudGunMan
6f1c44e62a Update mesh_bot.py
enhance llm error
2025-04-02 19:36:12 -07:00
SpudGunMan
837d049acb Update locationdata.py 2025-03-30 14:00:14 -07:00
SpudGunMan
2463407ade Update system.py 2025-03-30 13:49:40 -07:00
SpudGunMan
af2bc7be0c enhance sysinfo 2025-03-30 13:45:22 -07:00
SpudGunMan
38654213e8 fix script run 2025-03-30 13:44:57 -07:00
SpudGunMan
a06819dbda enhance bbsack 2025-03-30 11:49:54 -07:00
SpudGunMan
9818cccbbf fix BBSLink for open mode
fix for issue raised https://github.com/SpudGunMan/meshing-around/discussions/142
2025-03-30 11:29:00 -07:00
SpudGunMan
239dbb8be0 Update config.template
typo
2025-03-28 10:46:29 -07:00
SpudGunMan
872a9601d0 Update system.py 2025-03-27 20:31:53 -07:00
SpudGunMan
2b6dc726e1 valert for USGS Volcano Data 2025-03-27 19:44:02 -07:00
SpudGunMan
ef27ddff84 Update locationdata.py 2025-03-27 19:32:54 -07:00
SpudGunMan
8a8ad961d5 USGS Alerts 2025-03-27 19:31:56 -07:00
SpudGunMan
a8b4362d3c enhance VolcanoAlert
prevent stale records from being rebroadcast
2025-03-27 18:22:13 -07:00
SpudGunMan
dc731ae237 USGS Volcano Alerts 2025-03-27 16:11:21 -07:00
SpudGunMan
d0d024d770 Update system.py 2025-03-27 09:43:36 -07:00
SpudGunMan
9b633502e6 Update mesh_bot.py 2025-03-20 12:14:26 -07:00
Kelly
ac1a007ba4 Merge pull request #140 from todd2982/patch-2
Update .gitignore
2025-03-17 16:03:41 -07:00
todd2982
09cf6f585c Update .gitignore
Ignore rotated logs, install notes, and qrz db.
2025-03-17 02:07:01 -05:00
SpudGunMan
916719f1c5 Update mesh_bot.py 2025-03-15 17:31:51 -07:00
SpudGunMan
11a6dc3cf0 UTF-8-4-Windows
Co-Authored-By: dj505 <dj505@users.noreply.github.com>
2025-03-15 17:31:44 -07:00
SpudGunMan
c160678e79 Update locationdata.py 2025-03-07 17:55:56 -08:00
SpudGunMan
0c9fd919ab Update system.py 2025-03-07 17:53:17 -08:00
SpudGunMan
e17dc79896 🐞Bugs
issue https://github.com/SpudGunMan/meshing-around/issues/138
2025-03-04 12:46:16 -08:00
SpudGunMan
06d6855d92 cmd bang
this will solve all the worlds problems
2025-02-26 20:16:53 -08:00
SpudGunMan
66f937a645 expand BBS Block
ignore node who is cantankerous from all commands
2025-02-25 19:18:56 -08:00
SpudGunMan
f4985b744a Update README.md
Co-Authored-By: mikecarper <135079168+mikecarper@users.noreply.github.com>
2025-02-23 20:33:26 -08:00
SpudGunMan
7ae6174f96 Update hamtest.py 2025-02-23 20:06:08 -08:00
SpudGunMan
d44fdd4462 Update hamtest.py 2025-02-23 20:03:12 -08:00
SpudGunMan
3dd6da4684 Update hamtest.py 2025-02-23 20:02:15 -08:00
SpudGunMan
a229b57964 Update README.md 2025-02-23 19:25:15 -08:00
SpudGunMan
5e045b6447 Update README.md 2025-02-23 19:24:52 -08:00
SpudGunMan
1e328d4f4d Update README.md 2025-02-23 19:20:21 -08:00
SpudGunMan
879d141844 Update mesh_bot.py 2025-02-23 19:14:32 -08:00
SpudGunMan
7daf8c4c33 Update README.md 2025-02-23 18:57:50 -08:00
SpudGunMan
3e6d1f5c6f Merge branch 'main' of https://github.com/SpudGunMan/meshing-around 2025-02-23 18:40:44 -08:00
SpudGunMan
32deea9e3b hamtest
a game of the FCC/ARRL Question Pools
2025-02-23 18:40:41 -08:00
Kelly
793fabcdb8 Merge pull request #136 from NomDeTom/main
change maxBuffer to 200
2025-02-23 13:41:02 -08:00
SpudGunMan
a7a710208a Update send-environment-metrics.py 2025-02-22 17:29:42 -08:00
Tom
41efbc6189 Update config.template
change maxBuffer to 200 by default, as this is the longest that the recent firmware allows.
2025-02-23 01:28:17 +00:00
SpudGunMan
f399190d3c hangman 2025-02-21 21:48:12 -08:00
SpudGunMan
5760c10534 enhanceHangmen
is it hang man or hang men.
2025-02-21 21:31:16 -08:00
SpudGunMan
9deb4a9436 Update hangman.py 2025-02-21 19:04:56 -08:00
SpudGunMan
1f348d963d Update hangman.py 2025-02-21 18:56:07 -08:00
Kelly
b35edf13c8 Merge pull request #134 from dadecoza/main
Hangman!
2025-02-21 18:54:03 -08:00
Johannes le Roux
37185b9f8b Update hangman.py 2025-02-20 23:45:16 +02:00
Johannes le Roux
4e25535ede party face 2025-02-20 23:22:53 +02:00
Johannes le Roux
4de2a36099 added hangman 2025-02-20 22:53:28 +02:00
Kelly
6c0d6fd343 Merge pull request #133 from SpudGunMan/lab
Lab Enhancments
2025-02-19 18:32:06 -08:00
SpudGunMan
abd865c918 ignoreListFema 2025-02-19 18:29:22 -08:00
SpudGunMan
82222addbe Update log.py
enhance with more windows compatibility

Co-Authored-By: dj505 <7433694+dj505@users.noreply.github.com>
2025-02-19 18:21:43 -08:00
SpudGunMan
7750ce468b Update README.md 2025-02-19 17:31:22 -08:00
SpudGunMan
135778d511 winPython
Thanks Discord dj505 request for windows support
2025-02-19 17:30:06 -08:00
SpudGunMan
c54df673c3 refactorValue 2025-02-17 19:40:30 -08:00
SpudGunMan
2fec08060f FEMAIgnore Enhancment 2025-02-17 19:37:19 -08:00
SpudGunMan
ce9af3c0d3 Update locationdata.py 2025-02-17 14:11:06 -08:00
SpudGunMan
217cd01d0a Update locationdata.py 2025-02-17 14:00:40 -08:00
SpudGunMan
8a6057995b Update locationdata.py 2025-02-17 14:00:05 -08:00
SpudGunMan
47e21dbaab Chunker Improvement
Adjusted how packets are split, ignoring .?! which can confound things. @NomDeTom
2025-02-17 10:21:38 -08:00
SpudGunMan
267f50c591 Update locationdata.py 2025-02-16 11:04:37 -08:00
SpudGunMan
0013a7bb74 Update locationdata.py 2025-02-16 11:01:47 -08:00
SpudGunMan
73fe8be432 Update locationdata.py 2025-02-16 11:00:54 -08:00
SpudGunMan
3d45195ae9 refactor NOAA forecast to the API from bScrape
I cleaned up the config.ini noaaforecastduration you may want to set yours to `noaaforecastduration = 3` not like it was before that was a goof
2025-02-16 10:53:04 -08:00
SpudGunMan
ff390cf470 fixLog
reference https://github.com/SpudGunMan/meshing-around/discussions/125
2025-02-05 19:09:16 -08:00
SpudGunMan
17d8cd1067 enhance 2025-02-05 18:09:44 -08:00
SpudGunMan
b9348c906d enhance
better path handling
setting for IP Address

per https://github.com/SpudGunMan/meshing-around/issues/126

Co-Authored-By: mikecarper <135079168+mikecarper@users.noreply.github.com>
2025-02-05 18:09:08 -08:00
SpudGunMan
6ba3508cc5 outsideUSA rlist fix
@g7kse thanks for help on this

resolving https://github.com/SpudGunMan/meshing-around/issues/123
2025-02-04 19:04:08 -08:00
SpudGunMan
1c78f154da fixGameDisable Issue
from @PiHiker thanks for pointing out!
https://github.com/SpudGunMan/meshing-around/issues/124

closed issue
2025-02-04 18:37:50 -08:00
SpudGunMan
e0a3d0f94e Update system.py 2025-02-01 11:34:31 -08:00
SpudGunMan
066211e9f2 Update mesh_bot.py 2025-02-01 11:29:28 -08:00
SpudGunMan
5701cd108b Update qrz.py 2025-02-01 10:11:40 -08:00
SpudGunMan
b877a294ac Update install.sh 2025-02-01 09:19:49 -08:00
SpudGunMan
2aedcfc46e Update system.py 2025-02-01 09:04:48 -08:00
SpudGunMan
12147db5d0 Update mesh_bot.py 2025-01-31 22:06:31 -08:00
SpudGunMan
cef37b574b Update mesh_bot.py 2025-01-31 22:05:53 -08:00
SpudGunMan
6f121b7aac enhance QRZ
default to training mode, a new mode
2025-01-31 22:04:03 -08:00
SpudGunMan
9e31b7f47e deepseek compatibility
deepseek
2025-01-29 20:01:28 -08:00
SpudGunMan
f3103984ef Update README.md 2025-01-28 20:38:58 -08:00
SpudGunMan
9c8b3f0a54 Update CONTRIBUTING.md 2025-01-28 20:32:51 -08:00
SpudGunMan
f88cbf210e Update README.md 2025-01-28 20:30:30 -08:00
SpudGunMan
9909113beb Update README.md 2025-01-28 20:24:35 -08:00
SpudGunMan
c1b783b1cd Create README.md 2025-01-28 20:21:39 -08:00
SpudGunMan
9b3b6a5d3d Update README.md 2025-01-28 19:53:26 -08:00
SpudGunMan
cffdb3c089 Update README.md 2025-01-28 19:48:19 -08:00
SpudGunMan
7bb9c9ac55 Update README.md 2025-01-28 19:46:25 -08:00
SpudGunMan
830ec95080 🐛 2025-01-23 20:50:03 -08:00
SpudGunMan
0ea575ac70 Update README.md 2025-01-23 20:36:24 -08:00
SpudGunMan
d836255716 Update globalalert.py 2025-01-23 17:49:04 -08:00
SpudGunMan
4f115c9c21 Update pong_bot.py 2025-01-22 22:02:42 -08:00
SpudGunMan
63bd5b836d HELP
H
E
L
P
2025-01-22 22:00:56 -08:00
SpudGunMan
5ad9b9a261 Update mesh_bot.py 2025-01-22 21:51:04 -08:00
SpudGunMan
7a024b681f Create send-environment-metrics.py 2025-01-22 21:21:35 -08:00
SpudGunMan
75df5a695b Update mesh_bot.py 2025-01-21 21:39:17 -08:00
Kelly
0ef8cffd56 Merge pull request #119 from SpudGunMan/lab
LabCleanup
2025-01-21 20:26:09 -08:00
SpudGunMan
73e8e063d2 Update mesh_bot.py 2025-01-21 20:22:25 -08:00
SpudGunMan
82880677f4 Update mesh_bot.py 2025-01-21 20:21:32 -08:00
SpudGunMan
fe8ba8aaf4 Update mesh_bot.py 2025-01-21 20:10:01 -08:00
SpudGunMan
cea9147745 Update mesh_bot.py 2025-01-21 20:05:11 -08:00
SpudGunMan
c1c68d4c10 Update mesh_bot.py 2025-01-21 20:02:58 -08:00
SpudGunMan
5fcd21680e Update install.sh 2025-01-21 19:29:27 -08:00
SpudGunMan
9e1356172f Update install.sh 2025-01-21 19:23:20 -08:00
SpudGunMan
de7fdfad11 Update install.sh 2025-01-21 19:20:49 -08:00
SpudGunMan
a87055874a Update mesh_bot.py 2025-01-20 21:02:43 -08:00
SpudGunMan
5c7433091d Update mesh_bot.py 2025-01-20 21:00:18 -08:00
SpudGunMan
f0ca818461 Update checklist.py 2025-01-20 11:27:25 -08:00
SpudGunMan
76006dcda7 reverse_in_out 2025-01-20 10:54:51 -08:00
SpudGunMan
33abe646ae Update README.md 2025-01-19 12:09:55 -08:00
SpudGunMan
c47004c47c Update README.md 2025-01-19 12:09:25 -08:00
SpudGunMan
e66d945be7 Update checklist.py 2025-01-19 11:41:15 -08:00
SpudGunMan
10afc128f4 Update checklist.py 2025-01-19 11:35:15 -08:00
SpudGunMan
e6fc794951 Update requirements.txt 2025-01-19 11:07:34 -08:00
SpudGunMan
4839e9ba03 Update requirements.txt 2025-01-18 20:57:26 -08:00
SpudGunMan
bde15e311a Update README.md 2025-01-18 20:55:23 -08:00
SpudGunMan
21c83222e9 Update mesh_bot.py 2025-01-18 20:52:45 -08:00
SpudGunMan
bbcdd6656a Update README.md 2025-01-18 20:38:50 -08:00
SpudGunMan
7f61b86252 Update README.md 2025-01-18 20:10:19 -08:00
SpudGunMan
25ae27a162 Update system.py 2025-01-18 20:10:16 -08:00
SpudGunMan
a04133e82f Update README.md 2025-01-18 19:59:07 -08:00
SpudGunMan
2a9dfc90ee Update checklist.py 2025-01-18 18:09:42 -08:00
SpudGunMan
f1bf84f6f0 enhance 2025-01-18 18:08:36 -08:00
SpudGunMan
4b91ef10b4 Update README.md 2025-01-18 16:59:08 -08:00
SpudGunMan
cd4497b129 Update config.template 2025-01-18 16:28:52 -08:00
SpudGunMan
01374a8307 Update config.template 2025-01-18 16:28:40 -08:00
SpudGunMan
46c115b783 Update README.md 2025-01-18 16:27:04 -08:00
SpudGunMan
eec7230a84 fix 2025-01-18 16:24:04 -08:00
SpudGunMan
9394fd6ca9 qrzHello
says hello to new seen nodes
2025-01-18 16:22:35 -08:00
SpudGunMan
c6653da1f3 fixQRZ 2025-01-18 16:17:29 -08:00
SpudGunMan
9f47958a03 Update checklist.py 2025-01-18 16:14:00 -08:00
SpudGunMan
78e51b7be1 Update qrz.py 2025-01-18 16:06:40 -08:00
SpudGunMan
26fcf6fc02 enhance 2025-01-18 15:54:27 -08:00
SpudGunMan
c2336850fe Update checklist.py 2025-01-18 15:35:03 -08:00
SpudGunMan
54e0d17e70 Update checklist.py 2025-01-18 15:17:18 -08:00
SpudGunMan
7a6d1f7b29 Update checklist.py 2025-01-18 15:13:34 -08:00
SpudGunMan
7e26d3f0e5 Update README.md 2025-01-18 15:07:16 -08:00
35 changed files with 18815 additions and 373 deletions

6
.gitignore vendored
View File

@@ -8,7 +8,8 @@ config.ini
venv/
# logs
logs/*.log
logs/
install_notes.txt
# modified .service files
etc/*.service
@@ -18,3 +19,6 @@ __pycache__/
# rag data
data/rag/*
# qrz db
data/qrz.db

View File

@@ -1 +1,2 @@
currently operating under "Agile software development" aka rolling code; no major structure. meshing about .. get it..
currently operating under "Agile software development" aka rolling code; no major structure. meshing about .. get it..
there is some ideas for adding code in modules/README.md

115
README.md
View File

@@ -11,13 +11,14 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
- **Automated Responses**: The bot detects keywords like "ping" and responds with "pong" in direct messages (DMs) or group channels.
- **Customizable Triggers**: Monitor group channels for specific keywords and set custom responses.
- **Emergency Response**: Monitor channels for keywords indicating emergencies and alert a wider audience.
- **New Node Hello**: Greet new nodes on the mesh with a hello message
### Network Tools
- **Build, Test Local Mesh**: Ping allow for message delivery testing with more realistic packets vs. telemetry
- **Test Node Hardware**: `test` will send incremental sized data into the radio buffer for overall length of message testing
### Dual Radio/Node Support
- **Simultaneous Monitoring**: Monitor two networks at the same time.
### Multi Radio/Node Support
- **Simultaneous Monitoring**: Monitor up to nine networks at the same time.
- **Flexible Messaging**: send mail and messages, between networks.
### Advanced Messaging Capabilities
@@ -26,7 +27,8 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
- **Store and Forward**: Replay messages with the `messages` command, and log messages locally to disk.
- **Send Mail**: Send mail to nodes using `bbspost @nodeNumber #message` or `bbspost @nodeShortName #message`.
- **BBS Linking**: Combine multiple bots to expand BBS reach.
- **E-Mail/SMS**: Send mesh-messages to E-Mail or SMS expanding visability.
- **E-Mail/SMS**: Send mesh-messages to E-Mail or SMS(Email) expanding visability.
- **New Node Hello**: Send a hello to any new node seen in text message.
### Interactive AI and Data Lookup
- **NOAA location Data**: Get localized weather(alerts), River Flow, and Tide information. Open-Meteo is used for wx only outside NOAA coverage.
@@ -36,12 +38,14 @@ 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
### CheckList / Check In Out
- **Asset Tracking**: Maintain a list of node/asset checkin and checkout. Usefull for accountability of people, assets. Radio-Net, FEMA, Trailhead.
### Fun and Games
- **Built-in Games**: Enjoy games like DopeWars, Lemonade Stand, BlackJack, and VideoPoker.
- **FCC ARRL QuizBot**: The exam question pool quiz-bot.
- **Command-Based Gameplay**: Issue `games` to display help and start playing.
### Radio Frequency Monitoring
@@ -51,8 +55,9 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
### EAS Alerts
- **FEMA iPAWS/EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from FEMA
- **NOAA EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from NOAA.
- **USGS Volcano Alerts via API**: Use an internet-connected node to message Emergency Alerts from USGS.
- **EAS Alerts over the air**: Utilizing external tools to report EAS alerts offline over mesh.
- **UK.GOV Alerts**: Pulling data form the UK.GOV alert page
- **NINA alerts for Germany**: Emergency Alerts from xrepository.de feed
### File Monitor Alerts
- **File Monitor**: Monitor a flat/text file for changes, broadcast the contents of the message to the mesh channel.
@@ -65,7 +70,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
- **Message Chunking**: Automatically chunk messages over 160 characters to ensure higher delivery success across hops.
## Getting Started
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware. For pico or low-powered devices, see projects for embedding, [buildroot](https://github.com/buildroot-meshtastic/buildroot-meshtastic), there is also [femtofox](https://github.com/noon92/femtofox). 🥔 Please use responsibly and follow local rulings for such equipment. This project captures packets, logs them, and handles over the air communications which can include PII such as GPS locations.
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware. For pico or low-powered devices, see projects for embedding, [buildroot](https://github.com/buildroot-meshtastic/buildroot-meshtastic), also see [femtofox](https://github.com/noon92/femtofox). 🥔 Please use responsibly and follow local rulings for such equipment. This project captures packets, logs them, and handles over the air communications which can include PII such as GPS locations.
### Installation
@@ -76,14 +81,14 @@ git clone https://github.com/spudgunman/meshing-around
```
The code is under active development, so make sure to pull the latest changes regularly!
#### Automation of setup
#### Quick setup
- **Automated Installation**: `install.sh` will automate optional venv and requirements installation.
- **Launch Script**: `launch.sh` will activate and launch the app in the venv
- **Launch Script**: `launch.sh` only used in a venv install, to launch the bot and the report generator.
#### Docker Installation
If you prefer to use [Docker](script/docker/README.md)
See further info on the [docker.md](script/docker/README.md)
#### Custom Install
#### Manual Install
Install the required dependencies using pip:
```sh
pip install -r requirements.txt
@@ -94,8 +99,10 @@ Copy the configuration template to `config.ini` and edit it to suit your needs:
cp config.template config.ini
```
### Configuration
Copy the [config.template](config.template) to `config.ini` and set the appropriate interface for your method (serial/ble/tcp). While BLE and TCP will work, they are not as reliable as serial connections. There is a watchdog to reconnect TCP if possible. To get the BLE MAC address, use:
### Configuration Guide
The following is documentation for the config.ini file
If you have not done so, or want to 'factory reset', copy the [config.template](config.template) to `config.ini` and set the appropriate interface for your method (serial/ble/tcp). While BLE and TCP will work, they are not as reliable as serial connections. There is a watchdog to reconnect TCP if possible. To get the BLE MAC address, use:
```sh
meshtastic --ble-scan
```
@@ -128,6 +135,8 @@ The following settings determine how the bot responds. By default, the bot will
respond_by_dm_only = True
defaultChannel = 0
ignoreDefaultChannel = False # ignoreDefaultChannel, the bot will ignore the default channel set above
ignoreChannels = # ignoreChannels is a comma separated list of channels to ignore, e.g. 4,5
cmdBang = False # require ! to be the first character in a command
```
### Location Settings
@@ -139,7 +148,6 @@ enabled = True
lat = 48.50
lon = -123.0
UseMeteoWxAPI = True
riverListDefault = # NOAA Hydrology data, unique identifiers, LID or USGS ID
```
### Module Settings
@@ -173,6 +181,8 @@ SentryRadius = 100 # radius in meters to detect someone close to the bot
SentryChannel = 9 # holdoff time multiplied by seconds(20) of the watchdog
SentryHoldoff = 2 # channel to send a message to when the watchdog is triggered
sentryIgnoreList = # list of ignored nodes numbers ex: 2813308004,4258675309
highFlyingAlert = True # HighFlying Node alert
highFlyingAlertAltitude = 2000 # Altitude in meters to trigger the alert
```
### E-Mail / SMS Settings
@@ -203,17 +213,22 @@ alert_interface = 1
### EAS Alerting
To Alert on Mesh with the EAS API you can set the channels and enable, checks every 20min.
#### FEMA iPAWS/EAS and UK.gov
This uses USA: SAME, FIPS, ZIP code to locate the alerts in the feed. By default ignoring Test messages. UK.gov for England
#### FEMA iPAWS/EAS and NINA
This uses USA: SAME, FIPS, to locate the alerts in the feed. By default ignoring Test messages.
```ini
eAlertBroadcastEnabled = False # Goverment IPAWS/CAP Alert Broadcast
eAlertBroadcastCh = 2,3 # Goverment Emergency IPAWS/CAP Alert Broadcast Channels
ignoreFEMAtest = True # Ignore any headline that includes the word Test
# comma separated list of codes (e.g., SAME,FIPS,ZIP) trigger local alert.
# find your SAME https://www.weather.gov/nwr/counties
mySAME = 053029,053073
enableGBalerts = False # use UK.gov for alert source
ignoreFEMAenable = True # Ignore any headline that includes followig word list
ignoreFEMAwords = test,exercise
# comma separated list of FIPS codes to trigger local alert. find your FIPS codes at https://en.wikipedia.org/wiki/Federal_Information_Processing_Standard_state_code
myFIPSList = 57,58,53
# find your SAME https://www.weather.gov/nwr/counties comma separated list of SAME code to further refine local alert.
mySAMEList = 053029,053073
# To use other country services enable only a single optional serivce
enableDEalerts = False # Use DE Alert Broadcast Data see template for filters
myRegionalKeysDE = 110000000000,120510000000
```
#### NOAA EAS
@@ -224,6 +239,22 @@ enableGBalerts = False # use UK.gov for alert source
wxAlertBroadcastEnabled = True
# EAS Alert Broadcast Channels
wxAlertBroadcastCh = 2,4
ignoreEASenable = True # Ignore any headline that includes followig word list
ignoreEASwords = test,advisory
```
#### USGS River flow data and Volcano alerts
Using the USGS water data page locate a water flow device, for example Columbia River at Vancouver, WA - USGS-14144700
Volcano Alerts use lat/long to determine ~1000km radius
```ini
[location]
# USGS Hydrology unique identifiers, LID or USGS ID https://waterdata.usgs.gov
riverListDefault = 14144700
# USGS Volcano alerts Enable USGS Volcano Alert Broadcast
volcanoAlertBroadcastEnabled = False
volcanoAlertBroadcastCh = 2
```
### Repeater Settings
@@ -308,16 +339,34 @@ rtl_fm -f 162425000 -s 22050 | multimon-ng -t raw -a EAS /dev/stdin | python eas
#### Newspaper on mesh
a newspaper could be built by external scripts. could use Ollama to compile text via news web pages and write news.txt
you can also enable the line by line (hint just search for the commented lines with a 🐝) to return a string from the [bee movie](https://courses.cs.washington.edu/courses/cse163/20wi/files/lectures/L04/bee-movie.txt) for example adding it alongside news.txt as bee.txt
### Greet new nodes QRZ module
This isnt QRZ.com this is Q code for who is calling me, this will track new nodes and say hello
```ini
[qrz]
enabled = True # QRZ Hello to new nodes
qrz_hello_string = "send CMD or DM me for more info." # will be sent to all heard nodes once
training = True # Training mode will not send the hello message to new nodes, use this to build up database
```
### Scheduler
In the config.ini enable the module
```ini
[scheduler]
# enable or disable the scheduler module
enabled = True
enabled = False
# interface to send the message to
interface = 1
# channel to send the message to
channel = 2
message = "MeshBot says Hello! DM for more info."
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun
value =
# interval to use when time is not set (e.g. every 2 days)
interval =
# time of day in 24:00 hour format when value is 'day' and interval is not set
time =
```
The actions are via code only at this time. See mesh_bot.py around line [1097](https://github.com/SpudGunMan/meshing-around/blob/e94581936530c76ea43500eebb43f32ba7ed5e19/mesh_bot.py#L1097) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more. Recomend to backup changes so they dont get lost.
The basic brodcast message can be setup in condig.ini. For advanced, See mesh_bot.py around the bottom of file, line [1491](https://github.com/SpudGunMan/meshing-around/blob/e94581936530c76ea43500eebb43f32ba7ed5e19/mesh_bot.py#L1491) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more. Recomend to backup changes so they dont get lost.
```python
#Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
@@ -362,13 +411,14 @@ There is no direct support for MQTT in the code, however, reports from Discord a
### Radio Propagation & Weather Forcasting
| Command | Description | |
|---------|-------------|-------------------
| `ea` and `ealert` | Return FEMA iPAWS/EAS alerts in USA or UK. Headline or expanded details for USA | |
| `ea` and `ealert` | Return FEMA iPAWS/EAS alerts in USA or DE Headline or expanded details for USA | |
| `hfcond` | Returns a table of HF solar conditions | |
| `rlist` | Returns a table of nearby repeaters from RepeaterBook | |
| `riverflow` | Return information from NOAA for river flow info. Example: `riverflow modules/settings.py`| |
| `solar` | Gives an idea of the x-ray flux | |
| `sun` and `moon` | Return info on rise and set local time | ✅ |
| `tide` | Returns the local tides (NOAA data source) |
| `tide` | Returns the local tides (NOAA data source) | |
| `valert` | Returns USGS Volcano Data | |
| `wx` and `wxc` | Return local weather forecast (wxc is metric value), NOAA or Open Meteo for weather forecasting | |
| `wxa` and `wxalert` | Return NOAA alerts. Short title or expanded details | |
@@ -399,9 +449,10 @@ There is no direct support for MQTT in the code, however, reports from Discord a
### CheckList
| Command | Description | |
| `checkin` | Check in the node to the checklist database | ✅ |
| `checkout` | Checkout the node in the checklist database | ✅ |
| `checklist` | Display the checklist database | ✅ |
|---------|-------------|-
| `checkin` | Check in the node to the checklist database, you can add a note like `checkin ICO` or `checkin radio4` | ✅ |
| `checkout` | Checkout the node in the checklist database, checkout all from node | ✅ |
| `checklist` | Display the checklist database, with note | ✅ |
### Games (via DM)
| Command | Description | |
@@ -409,6 +460,8 @@ There is no direct support for MQTT in the code, however, reports from Discord a
| `blackjack` | Plays Blackjack (Casino 21) | ✅ |
| `dopewars` | Plays the classic drug trader game | ✅ |
| `golfsim` | Plays a 9-hole Golf Simulator | ✅ |
| `hamtest` | FCC/ARRL Quiz `hamtest general` or `hamtest extra` and `score` | ✅ |
| `hangman` | Plays the classic word guess game | ✅ |
| `joke` | Tells a joke | ✅ |
| `lemonstand` | Plays the classic Lemonade Stand finance game | ✅ |
| `mastermind` | Plays the classic code-breaking game | ✅ |
@@ -430,6 +483,7 @@ I used ideas and snippets from other responder bots and want to call them out!
- [Video Poker Terminal Game](https://github.com/devtronvarma/Video-Poker-Terminal-Game)
- [Python Mastermind](https://github.com/pwdkramer/pythonMastermind/)
- [Golf](https://github.com/danfriedman30/pythongame)
- ARRL Question Pool Data from https://github.com/russolsen/ham_radio_question_pool
### Special Thanks
- **xdep**: For the reporting tools.
@@ -438,9 +492,12 @@ I used ideas and snippets from other responder bots and want to call them out!
- **[https://github.com/A-c0rN](A-c0rN)**: Assistance with iPAWS and EAS
- **Mike O'Connell/skrrt**: For [eas_alert_parser](etc/eas_alert_parser.py) enhanced by **sheer.cold**
- **PiDiBi**: For looking at test functions and other suggestions like wxc, CPU use, and alerting ideas.
- **WH6GXZ nurse dude**: For bashing on installer
- **WH6GXZ nurse dude**: For bashing on installer, Volcano Alerts 🌋
- **Josh**: For more bashing on installer!
- **Cisien, bitflip, **Woof**, **propstg**, **Josh** and Hailo1999**: For testing and feature ideas on Discord and GitHub.
- **dj505**: trying it on windows!
- **mikecarper**: ideas, and testing. hamtest
- **c.merphy360**: high altitude alerts
- **Cisien, bitflip, **Woof**, **propstg**, **trs2982**, **Josh** and Hailo1999**: For testing and feature ideas on Discord and GitHub.
- **Meshtastic Discord Community**: For tossing out ideas and testing code.
### Tools

View File

@@ -32,6 +32,10 @@ autoPingInChannel = False
defaultChannel = 0
# ignoreDefaultChannel, the bot will ignore the default channel set above
ignoreDefaultChannel = False
# ignoreChannels is a comma separated list of channels to ignore, e.g. 4,5
ignoreChannels =
# require ! to be the first character in a command
cmdBang = False
# motd is reset to this value on boot
motd = Thanks for using MeshBOT! Have a good day!
@@ -56,6 +60,9 @@ ollama = False
# ollamaModel = llama3.1
# server instance to use (defaults to local machine install)
ollamaHostName = http://localhost:11434
# Produce LLM replies to messages that aren't commands?
# If False, the LLM only replies to the "ask:" and "askai" commands.
llmReplyToNonCommands = True
# StoreForward Enabled and Limits
StoreForward = True
@@ -80,6 +87,9 @@ sysloglevel = DEBUG
# Number of log files to keep in days, 0 to keep all
log_backup_count = 32
#Do not retry enabling interface if it fails, just exit to let OS restart the bot
dont_retry_disconnect = False
[emergencyHandler]
# enable or disable the emergency response handler
enabled = False
@@ -99,6 +109,14 @@ SentryChannel = 2
SentryHoldoff = 9
# list of ignored nodes numbers ex: 2813308004,4258675309
sentryIgnoreList =
# HighFlying Node alert
highFlyingAlert = True
# Altitude in meters to trigger the alert
highFlyingAlertAltitude = 2000
# Channel to send Alert when the high flying node is detected
highFlyingAlertChannel = 2
# list of nodes numbers to ignore high flying alert ex: 2813308004,4258675309
highFlyingIgnoreList =
[bbs]
enabled = True
@@ -123,19 +141,22 @@ useMetric = False
# repeaterList lookup location (rbook / artsci)
repeaterLookup = rbook
# NOAA weather forecast days, the first two rows are today and tonight
NOAAforecastDuration = 4
# NOAA weather forecast days
NOAAforecastDuration = 3
# number of weather alerts to display
NOAAalertCount = 2
# use Open-Meteo API for weather data not NOAA useful for non US locations
UseMeteoWxAPI = False
# NOAA Hydrology unique identifiers, LID or USGS ID
# USGS Hydrology unique identifiers, LID or USGS ID https://waterdata.usgs.gov
riverListDefault =
# NOAA EAS Alert Broadcast
wxAlertBroadcastEnabled = False
# Enable Ignore any message that includes following word list
ignoreEASenable = False
ignoreEASwords = test,advisory
# EAS Alert Broadcast Channels
wxAlertBroadcastCh = 2
# Add extra location to the weather alert
@@ -143,18 +164,22 @@ enableExtraLocationWx = False
# Goverment Alert Broadcast defaults to FEMA IPAWS
eAlertBroadcastEnabled = False
# comma separated list of FIPS codes to trigger local alert. find your FIPS codes at https://en.wikipedia.org/wiki/Federal_Information_Processing_Standard_state_code
myFIPSList = 57,58,53
# find your SAME https://www.weather.gov/nwr/counties comma separated list of SAME code to further refine local alert.
mySAMEList = 053029,053073
# Goverment Alert Broadcast Channels
eAlertBroadcastCh = 2
# Enable Ignore, headline that includes following word list
ignoreFEMAenable = True
ignoreFEMAwords = test,exercise
# FEMA Alert Broadcast Settings
# Ignore any headline that includes the word Test
ignoreFEMAtest = True
# comma separated list of codes (e.g., SAME,FIPS,ZIP) trigger local alert.
# find your SAME https://www.weather.gov/nwr/counties
mySAME = 053029,053073
# Use UK Alert Broadcast Data
enableGBalerts = False
# USGS Volcano alerts Enable USGS Volcano Alert Broadcast
volcanoAlertBroadcastEnabled = False
volcanoAlertBroadcastCh = 2
# Enable Ignore any message that includes following word list
ignoreUSGSEnable = False
ignoreUSGSWords = test,advisory
# Use DE Alert Broadcast Data
enableDEalerts = False
@@ -172,6 +197,15 @@ satList = 25544,7530
[checklist]
enabled = False
checklist_db = data/checklist.db
reverse_in_out = False
[qrz]
# QRZ Hello to new nodes with message
enabled = False
qrz_db = data/qrz.db
qrz_hello_string = "MeshBot says Hello! DM for more info."
# Training mode will not send the hello message to new nodes
training = True
# repeater module
[repeater]
@@ -185,6 +219,17 @@ repeater_channels =
[scheduler]
# enable or disable the scheduler module
enabled = False
# interface to send the message to
interface = 1
# channel to send the message to
channel = 2
message = "MeshBot says Hello! DM for more info."
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun
value =
# interval to use when time is not set (e.g. every 2 days)
interval =
# time of day in 24:00 hour format when value is 'day' and interval is not set
time =
[radioMon]
# using Hamlib rig control will monitor and alert on channel use
@@ -247,17 +292,21 @@ blackjack = True
videopoker = True
mastermind = True
golfsim = True
hangman = True
hamtest = True
[messagingSettings]
# delay in seconds for response to avoid message collision
responseDelay = 1.2
# delay in seconds for splits in messages to avoid message collision
splitDelay = 0.0
# delay in seconds for response to avoid message collision /throttling
responseDelay = 2.2
# delay in seconds for splits in messages to avoid message collision /throttling
splitDelay = 2.5
# message chunk size for sending at high success rate, chunkr allows exceeding by 3 characters
MESSAGE_CHUNK_SIZE = 160
# Request Acknowledgement of message OTA
wantAck = False
# Max limit buffer for radio testing. 233 is hard limit 2.5+ firmware
maxBuffer = 220
# Max limit buffer for radio testing
maxBuffer = 200
#Enable Extra logging of Hop count data
enableHopLogs = False

7226
data/hamradio/extra.json Normal file

File diff suppressed because it is too large Load Diff

5126
data/hamradio/general.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
# /etc/systemd/system/mesh_bot.service
# sudo systemctl daemon-reload
# sudo systemctl enable mesh_bot.service
# sudo systemctl start mesh_bot.service
[Unit]

View File

@@ -1,6 +1,7 @@
# /etc/systemd/system/mesh_bot.service
# /etc/systemd/system/mesh_bot_reporting.service
# sudo systemctl daemon-reload
# sudo systemctl start mesh_bot.service
# sudo systemctl enable mesh_bot_reporting.service
# sudo systemctl start mesh_bot_reporting.service
[Unit]
Description=MeshingAround-Reporting

23
etc/mesh_bot_w3.tmp Normal file
View File

@@ -0,0 +1,23 @@
# /etc/systemd/system/mesh_bot_w3.service
# sudo systemctl daemon-reload
# sudo systemctl enable mesh_bot_w3.service
# sudo systemctl start mesh_bot_w3.service
[Unit]
Description=MeshingAround-W3Server
After=network.target
[Service]
Type=simple
User=pi
Group=pi
WorkingDirectory=/dir/
ExecStart=python3 modules/web.py
ExecStop=pkill -f mesh_bot_w3.py
# Disable Python's buffering of STDOUT and STDERR, so that output from the
# service shows up immediately in systemd's logs
Environment=PYTHONUNBUFFERED=1
Restart=on-failure
Type=notify #try simple if any problems

View File

@@ -1,5 +1,6 @@
# /etc/systemd/system/pong_bot.service
# sudo systemctl daemon-reload
# sudo systemctl enable pong_bot.service
# sudo systemctl start pong_bot.service
[Unit]

View File

@@ -350,7 +350,8 @@ def get_database_info():
os.path.join(base_dir, 'mmind_hs.pkl'),
os.path.join(base_dir, 'golfsim_hs.pkl'),
os.path.join(base_dir, 'bbsdb.pkl'),
os.path.join(base_dir, 'bbsdm.pkl')]
os.path.join(base_dir, 'bbsdm.pkl'),
os.path.join(base_dir, 'qrz.db')]
for file in databaseFiles:
try:
@@ -371,6 +372,16 @@ def get_database_info():
bbsdb = pickle.load(f)
elif 'bbsdm' in file:
bbsdm = pickle.load(f)
elif 'qrz.db' in file:
# open the qrz.db sqllite file
import sqlite3
conn = sqlite3.connect(file)
cursor = conn.cursor()
cursor.execute("SELECT * FROM qrz")
qrz_db = cursor.fetchall()
# convert to a list of strings
qrz_db = [f"{row[0]}: {row[1]} {row[2]}" for row in qrz_db]
conn.close()
except Exception as e:
print(f"Warning issue reading database file: {str(e)}")
if 'lemonstand' in file:
@@ -425,7 +436,8 @@ def get_database_info():
'golfsim_score': golfsim_score,
'banList': banList,
'adminList': adminList,
'sentryIgnoreList': sentryIgnoreList
'sentryIgnoreList': sentryIgnoreList,
'qrz_db': qrz_db if 'qrz_db' in locals() else "no data"
}
def generate_main_html(log_data, system_info):
@@ -913,6 +925,11 @@ def generate_database_html(database_info):
<tr><td>Mastermind</td><td>${mmind_score}</td></tr>
<tr><td>Golf Simulator</td><td>${golfsim_score}</td></tr>
</table>
<h1>QRZ Database</h1>
<p>QRZ Database holds heard nodeID and Shortname</p>
<table>
<tr><td>${qrz_db}</td></tr>
</table>
</body>
</html>
"""

View File

@@ -359,7 +359,8 @@ def get_database_info():
os.path.join(base_dir, 'mmind_hs.pkl'),
os.path.join(base_dir, 'golfsim_hs.pkl'),
os.path.join(base_dir, 'bbsdb.pkl'),
os.path.join(base_dir, 'bbsdm.pkl')]
os.path.join(base_dir, 'bbsdm.pkl'),
os.path.join(base_dir, 'qrz.db')]
for file in databaseFiles:
try:
@@ -380,6 +381,16 @@ def get_database_info():
bbsdb = pickle.load(f)
elif 'bbsdm' in file:
bbsdm = pickle.load(f)
elif 'qrz.db' in file:
#open the qrz.db sqllite file
import sqlite3
conn = sqlite3.connect(file)
cursor = conn.cursor()
cursor.execute("SELECT * FROM qrz")
qrz_db = cursor.fetchall()
# convert to a list of strings
qrz_db = [f"{row[0]}: {row[1]} {row[2]}" for row in qrz_db]
conn.close()
except Exception as e:
print(f"Warning issue reading database file: {str(e)}")
if 'lemonstand' in file:
@@ -434,7 +445,8 @@ def get_database_info():
'golfsim_score': golfsim_score,
'banList': banList,
'adminList': adminList,
'sentryIgnoreList': sentryIgnoreList
'sentryIgnoreList': sentryIgnoreList,
'qrz_db': qrz_db if 'qrz_db' in locals() else "no data"
}
def generate_main_html(log_data, system_info):
@@ -1207,6 +1219,11 @@ def generate_database_html(database_info):
<tr><td>Mastermind</td><td>${mmind_score}</td></tr>
<tr><td>Golf Simulator</td><td>${golfsim_score}</td></tr>
</table>
<h1>QRZ Database</h1>
<p>QRZ Database holds heard nodeID and Shortname</p>
<table>
<tr><td>${qrz_db}</td></tr>
</table>
</body>
</html>
"""

View File

@@ -4,6 +4,7 @@
# install.sh
cd "$(dirname "$0")"
program_path=$(pwd)
chronjob="0 1 * * * /usr/bin/python3 $program_path/etc/report_generator5.py"
printf "\n########################"
printf "\nMeshing Around Installer\n"
printf "########################\n"
@@ -80,6 +81,7 @@ sudo usermod -a -G bluetooth $USER
cp etc/pong_bot.tmp etc/pong_bot.service
cp etc/mesh_bot.tmp etc/mesh_bot.service
cp etc/mesh_bot_reporting.tmp etc/mesh_bot_reporting.service
cp etc/mesh_bot_w3.tmp etc/mesh_bot_w3.service
# generate config file, check if it exists
if [[ -f config.ini ]]; then
@@ -158,10 +160,10 @@ else
fi
# if $1 is passed
if [[ $1 == "mesh" ]]; then
bot="mesh"
elif [[ $1 == "pong" ]]; then
if [[ $1 == "pong" ]]; then
bot="pong"
elif [[ $1 == "mesh" ]] || [[ $(echo "${embedded}" | grep -i "^y") ]]; then
bot="mesh"
else
printf "\n\n"
echo "Which bot do you want to install as a service? Pong Mesh or None? (pong/mesh/n)"
@@ -176,6 +178,7 @@ replace="s|/dir/|$program_path/|g"
sed -i $replace etc/pong_bot.service
sed -i $replace etc/mesh_bot.service
sed -i $replace etc/mesh_bot_reporting.service
sed -i $replace etc/mesh_bot_w3.service
# set the correct user in the service file?
#ask if we should add a user for the bot
@@ -191,26 +194,30 @@ if [[ $(echo "${meshbotservice}" | grep -i "^y") ]] || [[ $(echo "${embedded}" |
sudo usermod -a -G meshbot meshbot
whoami="meshbot"
echo "Added user meshbot with no home directory"
sudo usermod -a -G dialout $whoami
sudo usermod -a -G tty $whoami
sudo usermod -a -G bluetooth $whoami
echo "Added meshbot to dialout, tty, and bluetooth groups"
sudo chown -R $whoami:$whoami $program_path/logs
sudo chown -R $whoami:$whoami $program_path/data
echo "Permissions set for meshbot on logs and data directories"
else
whoami=$(whoami)
fi
# set basic permissions for the bot user
sudo usermod -a -G dialout $whoami
sudo usermod -a -G tty $whoami
sudo usermod -a -G bluetooth $whoami
echo "Added user $whoami to dialout, tty, and bluetooth groups"
sudo chown -R $whoami:$whoami $program_path/logs
sudo chown -R $whoami:$whoami $program_path/data
echo "Permissions set for meshbot on logs and data directories"
# set the correct user in the service file
replace="s|User=pi|User=$whoami|g"
sed -i $replace etc/pong_bot.service
sed -i $replace etc/mesh_bot.service
sed -i $replace etc/mesh_bot_reporting.service
sed -i $replace etc/mesh_bot_w3.service
replace="s|Group=pi|Group=$whoami|g"
sed -i $replace etc/pong_bot.service
sed -i $replace etc/mesh_bot.service
sed -i $replace etc/mesh_bot_reporting.service
sed -i $replace etc/mesh_bot_w3.service
printf "\n service files updated\n"
if [[ $(echo "${bot}" | grep -i "^p") ]]; then
@@ -260,19 +267,20 @@ if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
fi
fi
if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
# document the service install
printf "To install the %s service and keep notes, reference following commands:\n\n" "$service" > install_notes.txt
printf "sudo cp %s/etc/%s.service /etc/systemd/system/etc/%s.service\n" "$program_path" "$service" "$service" >> install_notes.txt
printf "sudo systemctl daemon-reload\n" >> install_notes.txt
printf "sudo systemctl enable %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl start %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl status %s.service\n\n" "$service" >> install_notes.txt
printf "To see logs and stop the service:\n" >> install_notes.txt
printf "sudo journalctl -u %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl stop %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
fi
# document the service install
printf "To install the %s service and keep notes, reference following commands:\n\n" "$service" > install_notes.txt
printf "sudo cp %s/etc/%s.service /etc/systemd/system/etc/%s.service\n" "$program_path" "$service" "$service" >> install_notes.txt
printf "sudo systemctl daemon-reload\n" >> install_notes.txt
printf "sudo systemctl enable %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl start %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl status %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl restart %s.service\n\n" "$service" >> install_notes.txt
printf "To see logs and stop the service:\n" >> install_notes.txt
printf "sudo journalctl -u %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl stop %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
printf "Reporting chron job added to run report_generator5.py\n" >> install_notes.txt
printf "chronjob: %s\n" "$chronjob" >> install_notes.txt
if [[ $(echo "${venv}" | grep -i "^y") ]]; then
printf "\nFor running on venv, virtual launch bot with './launch.sh mesh' in path $program_path\n" >> install_notes.txt
@@ -294,14 +302,26 @@ else
replace="s|# hostname = meshtastic.local|hostname = localhost|g"
sed -i "$replace" config.ini
printf "\nConfig file updated for embedded\n"
# add service dependency for meshtasticd into service file
#replace="s|After=network.target|After=network.target meshtasticd.service|g"
# Set up the meshing around service
sudo cp /opt/meshing-around/etc/$service.service /etc/systemd/system/$service.service
sudo systemctl daemon-reload
sudo systemctl enable $service.service
sudo systemctl start $service.service
# check if the cron job already exists
if ! crontab -l | grep -q "$chronjob"; then
# add the cron job to run the report_generator5.py script
(crontab -l 2>/dev/null; echo "$chronjob") | crontab -
printf "\nAdded cron job to run report_generator5.py\n"
else
printf "\nCron job already exists, skipping\n"
fi
printf "Reference following commands:\n\n" "$service" > install_notes.txt
printf "sudo systemctl status %s.service\n\n" "$service" >> install_notes.txt
printf "sudo systemctl status %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl start %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl restart %s.service\n\n" "$service" >> install_notes.txt
printf "To see logs and stop the service:\n" >> install_notes.txt
printf "sudo journalctl -u %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl stop %s.service\n" "$service" >> install_notes.txt
@@ -321,7 +341,7 @@ exit 0
# sudo systemctl stop mesh_bot_reporting
# sudo systemctl disable mesh_bot_reporting
# sudo rm /etc/systemd/system/mesh_bot.service
# sudo rm /etc/systemd/system/mesh_bot_reporting.service
# sudo rm /etc/systemd/system/mesh_bot_w3.service
# sudo rm /etc/systemd/system/pong_bot.service
# sudo systemctl daemon-reload
# sudo systemctl reset-failed

View File

@@ -4,6 +4,7 @@ Logs will collect here. Give a day of logs or a bunch of messages to have good r
## Reporting Note
Reporting is via [../etc/report_generator5.py](../etc/report_generator5.py). The report_generator5 has newer feel and HTML5 coding. The index.html output is published in [../etc/www](../etc/www) there is a .cfg file created on first run for configuring values as needed (like moving web root)
- Make sure to have `SyslogToFile = True` and default of DEBUG log level to fully enable reporting! ‼️
- If you are in a venv and using launch.sh you can `launch.sh html5`
![reportView](../etc/reporting.jpg)
@@ -23,4 +24,18 @@ log_backup_count = 32
## Web Reporting WebServer
There is a web-server module. You can run `python3 modules/web.py` from the project root directory and it will serve up the web content.
find it at. http://localhost:8420
find it at. http://localhost:8420
If you have linux-native running and errors such as..
```bash
File "/usr/lib/python3.11/http/server.py", line 136, in server_bind
socketserver.TCPServer.server_bind(self)
File "/usr/lib/python3.11/socketserver.py", line 472, in server_bind
self.socket.bind(self.server_address)
```
modify the modules/web.py to use a real IP address, meshtasticD-native is binding to 127.0.0.1
```python
# Set the desired IP address
server_ip = '127.0.0.1'
```

View File

@@ -1,6 +1,6 @@
#!/usr/bin/python3
# Meshtastic Autoresponder MESH Bot
# K7MHI Kelly Keeton 2024
# K7MHI Kelly Keeton 2025
try:
from pubsub import pub
@@ -15,12 +15,11 @@ from modules.log import *
from modules.system import *
# list of commands to remove from the default list for DM only
restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golfsim", "mastermind"]
restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golfsim", "mastermind", "hangman", "hamtest"]
restrictedResponse = "🤖only available in a Direct Message📵" # "" for none
# Global Variables
DEBUGpacket = False # Debug print the packet rx
DEBUGhops = False # Debug print hop info and bad hop count packets
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
global cmdHistory
@@ -43,10 +42,10 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
"bbsread": lambda: handle_bbsread(message),
"blackjack": lambda: handleBlackJack(message, message_from_id, deviceID),
"checkin": lambda: handle_checklist(message, message_from_id, deviceID),
"checkout": lambda: handle_checklist(message, message_from_id, deviceID),
"checklist": lambda: handle_checklist(message, message_from_id, deviceID),
"checkout": lambda: handle_checklist(message, message_from_id, deviceID),
"clearsms": lambda: handle_sms(message_from_id, message),
"cmd": lambda: help_message,
"cmd": lambda: handle_cmd(message, message_from_id, deviceID),
"cq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"cqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"cqcqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
@@ -57,6 +56,8 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
"games": lambda: gamesCmdList,
"globalthermonuclearwar": lambda: handle_gTnW(),
"golfsim": lambda: handleGolf(message, message_from_id, deviceID),
"hamtest": lambda: handleHamtest(message, message_from_id, deviceID),
"hangman": lambda: handleHangman(message, message_from_id, deviceID),
"hfcond": hf_band_conditions,
"history": lambda: handle_history(message, message_from_id, deviceID, isDM),
"joke": lambda: tell_joke(message_from_id),
@@ -83,6 +84,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
"test": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"testing": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"tide": lambda: handle_tide(message_from_id, deviceID, channel_number),
"valert": lambda: get_volcano_usgs(),
"videopoker": lambda: handleVideoPoker(message, message_from_id, deviceID),
"whereami": lambda: handle_whereami(message_from_id, deviceID, channel_number),
"whoami": lambda: handle_whoami(message_from_id, deviceID, hop, snr, rssi, pkiStatus),
@@ -113,6 +115,10 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
# check the message for commands words list, processed after system.messageTrap
for key in command_handler:
word = message_lower.split(' ')
if cmdBang:
# strip the !
if word[0].startswith("!"):
word[0] = word[0][1:]
if key in word:
# append all the commands found in the message to the cmds list
cmds.append({'cmd': key, 'index': message_lower.index(key)})
@@ -140,6 +146,13 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
time.sleep(responseDelay)
return bot_response
def handle_cmd(message, message_from_id, deviceID):
# why CMD? its just a command list. a terminal would normally use "Help"
# I didnt want to invoke the word "help" in Meshtastic due to its possible emergency use
if " " in message and message.split(" ")[1] in trap_list:
return "🤖 just use the commands directly in chat"
return help_message
def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number):
global multiPing
myNodeNum = globals().get(f'myNodeNum{deviceID}', 777)
@@ -341,8 +354,9 @@ def handle_satpass(message_from_id, deviceID, channel_number, message):
return passes
def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel):
global llmRunCounter, llmLocationTable, llmTotalRuntime, cmdHistory
global llmRunCounter, llmLocationTable, llmTotalRuntime, cmdHistory, seenNodes
location_name = 'no location provided'
msg = ''
if location_enabled:
# if message_from_id is is the llmLocationTable use the location from the list to save on API calls
@@ -368,17 +382,20 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
# consider this a command use for the cmdHistory list
cmdHistory.append({'nodeID': message_from_id, 'cmd': 'llm-use', 'time': time.time()})
# if the message_from_id is not in the llmLocationTable send the welcome message
for i in range(0, len(llmLocationTable)):
if not any(d['nodeID'] == message_from_id for d in llmLocationTable):
if (channel_number == publicChannel and antiSpam) or useDMForResponse:
# send via DM
send_message(welcome_message, channel_number, message_from_id, deviceID)
time.sleep(responseDelay)
else:
# send via channel
send_message(welcome_message, channel_number, 0, deviceID)
time.sleep(responseDelay)
# check for a welcome message (is this redundant?)
if not any(node['nodeID'] == message_from_id and node['welcome'] == True for node in seenNodes):
if (channel_number == publicChannel and antiSpam) or useDMForResponse:
# send via DM
send_message(welcome_message, channel_number, message_from_id, deviceID)
time.sleep(responseDelay)
else:
# send via channel
send_message(welcome_message, channel_number, 0, deviceID)
time.sleep(responseDelay)
# mark the node as welcomed
for node in seenNodes:
if node['nodeID'] == message_from_id:
node['welcome'] = True
# update the llmLocationTable for future use
for i in range(0, len(llmLocationTable)):
@@ -397,26 +414,18 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
# information for the user on how long the query will take on average
if llmRunCounter > 0:
averageRuntime = sum(llmTotalRuntime) / len(llmTotalRuntime)
if averageRuntime > 25:
msg = f"Please wait, average query time is: {int(averageRuntime)} seconds"
if (channel_number == publicChannel and antiSpam) or useDMForResponse:
# send via DM
send_message(msg, channel_number, message_from_id, deviceID)
time.sleep(responseDelay)
else:
# send via channel
send_message(msg, channel_number, 0, deviceID)
time.sleep(responseDelay)
msg = f"Average query time is: {int(averageRuntime)} seconds" if averageRuntime > 25 else ''
else:
msg = "Please wait, response could take 30+ seconds. Fund the SysOp's GPU budget!"
if msg != '':
if (channel_number == publicChannel and antiSpam) or useDMForResponse:
# send via DM
send_message(msg, channel_number, message_from_id, deviceID)
time.sleep(responseDelay)
else:
# send via channel
send_message(msg, channel_number, 0, deviceID)
time.sleep(responseDelay)
time.sleep(responseDelay)
start = time.time()
@@ -655,6 +664,69 @@ def handleGolf(message, nodeID, deviceID):
time.sleep(responseDelay + 1)
return msg
def handleHangman(message, nodeID, deviceID):
global hangmanTracker
index = 0
msg = ''
for i in range(len(hangmanTracker)):
if hangmanTracker[i]['nodeID'] == nodeID:
hangmanTracker[i]["last_played"] = time.time()
index = i+1
break
if index and "end" in message.lower():
hangman.end(nodeID)
hangmanTracker.pop(index-1)
return "Thanks for hanging out🤙"
if not index:
hangmanTracker.append(
{
"nodeID": nodeID,
"last_played": time.time()
}
)
msg = "🧩Hangman🤖 'end' to cut rope🪢\n"
msg += hangman.play(nodeID, message)
time.sleep(responseDelay + 1)
return msg
def handleHamtest(message, nodeID, deviceID):
global hamtestTracker
index = 0
msg = ''
response = message.split(' ')
for i in range(len(hamtestTracker)):
if hamtestTracker[i]['nodeID'] == nodeID:
hamtestTracker[i]["last_played"] = time.time()
index = i+1
break
if not index:
hamtestTracker.append({"nodeID": nodeID,"last_played": time.time()})
if "end" in response[0].lower():
msg = hamtest.endGame(nodeID)
elif "score" in response[0].lower():
msg = hamtest.getScore(nodeID)
if "hamtest" in response[0].lower():
if len(response) > 1:
if "gen" in response[1].lower():
msg = hamtest.newGame(nodeID, 'general')
elif "ex" in response[1].lower():
msg = hamtest.newGame(nodeID, 'extra')
else:
msg = hamtest.newGame(nodeID, 'technician')
# 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 handle_riverFlow(message, message_from_id, deviceID):
location = get_node_location(message_from_id, deviceID)
userRiver = message.lower()
@@ -700,9 +772,6 @@ def handle_emergency_alerts(message, message_from_id, deviceID):
if enableDEalerts:
# nina Alerts
return get_nina_alerts()
if enableGBalerts:
# UK Alerts
return get_govUK_alerts(str(location[0]), str(location[1]))
if message.lower().startswith("ealert"):
# Detailed alert FEMA
return getIpawsAlert(str(location[0]), str(location[1]))
@@ -735,7 +804,7 @@ def handle_bbspost(message, message_from_id, deviceID):
toNode = int(toNode.strip("!"),16)
except ValueError as e:
toNode = 0
elif toNode.isalpha() or not toNode.isnumeric():
elif toNode.isalpha() or not toNode.isnumeric() or len(toNode) < 5:
# try short name
toNode = get_num_from_short_name(toNode, deviceID)
@@ -786,8 +855,14 @@ def sysinfo(message, message_from_id, deviceID):
return "sysinfo command returns system information."
else:
if enable_runShellCmd and file_monitor_enabled:
shellData = call_external_script(None, "script/sysEnv.sh").rstrip()
return get_sysinfo(message_from_id, deviceID) + "\n" + shellData
# get the system information from the shell script
# this is an example of how to run a shell script and return the data
shellData = call_external_script(None, "script/sysEnv.sh")
# check if the script returned data
if shellData == "" or shellData == None:
# no data returned from the script
shellData = "shell script data missing"
return get_sysinfo(message_from_id, deviceID) + "\n" + shellData.rstrip()
else:
return get_sysinfo(message_from_id, deviceID)
@@ -828,7 +903,7 @@ def handle_history(message, nodeid, deviceID, isDM, lheard=False):
prettyTime = getPrettyTime(cmdTime)
# history display output
if nodeid in bbs_admin_list and cmdHistory[i]['nodeID'] not in lheardCmdIgnoreNode:
if str(nodeid) in bbs_admin_list and cmdHistory[i]['nodeID'] not in lheardCmdIgnoreNode:
buffer.append((get_name_from_number(cmdHistory[i]['nodeID'], 'short', deviceID), cmdHistory[i]['cmd'], prettyTime))
elif cmdHistory[i]['nodeID'] == nodeid and cmdHistory[i]['nodeID'] not in lheardCmdIgnoreNode:
buffer.append((get_name_from_number(nodeid, 'short', deviceID), cmdHistory[i]['cmd'], prettyTime))
@@ -976,13 +1051,16 @@ def checkPlayingGame(message_from_id, message_string, rxNode, channel_number):
game = "None"
trackers = [
(dwPlayerTracker, "DopeWars", handleDopeWars),
(lemonadeTracker, "LemonadeStand", handleLemonade),
(vpTracker, "VideoPoker", handleVideoPoker),
(jackTracker, "BlackJack", handleBlackJack),
(mindTracker, "MasterMind", handleMmind),
(golfTracker, "GolfSim", handleGolf),
(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,
]
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)
@@ -1005,6 +1083,7 @@ def onReceive(packet, interface):
replyIDset = False
emojiSeen = False
isDM = False
playingGame = False
if DEBUGpacket:
# Debug print the interface object
@@ -1028,16 +1107,15 @@ def onReceive(packet, interface):
if rxType == 'TCPInterface':
rxHost = interface.__dict__.get('hostname', 'unknown')
if hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
elif multiple_interface and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
elif multiple_interface and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
elif multiple_interface and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
elif multiple_interface and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
elif multiple_interface and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
elif multiple_interface and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
elif multiple_interface and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
elif multiple_interface and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
if rxHost and hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
elif multiple_interface and rxHost and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
elif multiple_interface and rxHost and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
elif multiple_interface and rxHost and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
elif multiple_interface and rxHost and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
elif multiple_interface and rxHost and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
elif multiple_interface and rxHost and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
elif multiple_interface and rxHost and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
elif multiple_interface and rxHost and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
if rxType == 'BLEInterface':
if interface1_type == 'ble': rxNode = 1
elif multiple_interface and interface2_type == 'ble': rxNode = 2
@@ -1114,7 +1192,7 @@ def onReceive(packet, interface):
else:
hop_start = 0
if DEBUGhops:
if enableHopLogs:
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start}")
if hop_away == 0 and hop_limit == 0 and hop_start == 0:
logger.debug(f"System: Packet HopDebugger: No hop count found in PACKET {packet} END PACKET")
@@ -1146,7 +1224,7 @@ def onReceive(packet, interface):
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 the message log
# 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 +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
# respond with DM
@@ -1164,7 +1242,7 @@ def onReceive(packet, interface):
playingGame = False
if not playingGame:
if llm_enabled:
if llm_enabled and llmReplyToNonCommands:
# respond with LLM
llm = handle_llm(message_from_id, channel_number, rxNode, message_string, publicChannel)
send_message(llm, channel_number, message_from_id, rxNode)
@@ -1193,14 +1271,22 @@ def onReceive(packet, interface):
time.sleep(responseDelay)
# log the message to the message log
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
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-'))
else:
# message is on a channel
if messageTrap(message_string):
# message is for us to respond to, or is it...
if ignoreDefaultChannel and channel_number == publicChannel:
logger.debug(f"System: ignoreDefaultChannel CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)}")
logger.debug(f"System: Ignoring CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Default Channel:{channel_number}")
elif str(message_from_id) in bbs_ban_list:
logger.debug(f"System: Ignoring CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Cantankerous Node")
elif str(channel_number) in ignoreChannels:
logger.debug(f"System: Ignoring CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Ignored Channel:{channel_number}")
elif 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")
else:
# message is for bot to respond to
# 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 +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
if useDMForResponse:
@@ -1219,7 +1305,7 @@ def onReceive(packet, interface):
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, 0, rxNode)
else:
# message is not for bot to respond to
# message is not for us to respond to
# ignore the message but add it to the message history list
if zuluTime:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
@@ -1254,11 +1340,18 @@ def onReceive(packet, interface):
# if QRZ enabled check if we have said hello
if qrz_hello_enabled:
if never_seen_before(message_from_id):
# add to qrz_hello list
hello(message_from_id, get_name_from_number(message_from_id, 'short', rxNode))
# send a hello message
send_message(f"Hello {get_name_from_number(message_from_id, 'short', rxNode)}", channel_number, message_from_id, rxNode)
time.sleep(responseDelay)
name = get_name_from_number(message_from_id, 'short', rxNode)
if isinstance(name, str) and name.startswith("!") and len(name) == 9:
# we didnt get a info packet yet so wait and ingore this go around
logger.debug(f"System: QRZ Hello ignored, no info packet yet")
else:
# add to qrz_hello list
hello(message_from_id, name)
# send a hello message as a DM
if not train_qrz:
time.sleep(responseDelay)
send_message(f"Hello {name} {qrz_hello_string}", channel_number, message_from_id, rxNode)
time.sleep(responseDelay)
else:
# Evaluate non TEXT_MESSAGE_APP packets
consumeMetadata(packet, rxNode)
@@ -1281,8 +1374,9 @@ async def start_rx():
if llm_enabled:
logger.debug(f"System: Ollama LLM Enabled, loading model {llmModel} please wait")
llm_query(" ")
logger.debug(f"System: LLM model {llmModel} loaded")
llmLoad = llm_query(" ")
if "trouble" not in llmLoad:
logger.debug(f"System: LLM Model {llmModel} loaded")
if log_messages_to_file:
logger.debug("System: Logging Messages to disk")
@@ -1312,6 +1406,8 @@ async def start_rx():
logger.debug(f"System: MOTD Enabled using {MOTD}")
if sentry_enabled:
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel}")
if highfly_enabled:
logger.debug(f"System: HighFly Enabled using {highfly_altitude}m limit reporting to channel:{highfly_channel}")
if store_forward_enabled:
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
if useDMForResponse:
@@ -1331,30 +1427,87 @@ async def start_rx():
if wxAlertBroadcastEnabled:
logger.debug(f"System: Weather Alert Broadcast Enabled on channels {wxAlertBroadcastChannel}")
if emergencyAlertBrodcastEnabled:
logger.debug(f"System: Emergency Alert Broadcast Enabled on channels {emergencyAlertBroadcastCh}")
logger.debug(f"System: Emergency Alert Broadcast Enabled on channels {emergencyAlertBroadcastCh} for FIPS codes {myStateFIPSList}")
# check if the FIPS codes are set
if myStateFIPSList == ['']:
logger.warning(f"System: No FIPS codes set for iPAWS Alerts")
if emergency_responder_enabled:
logger.debug(f"System: Emergency Responder Enabled on channels {emergency_responder_alert_channel} for interface {emergency_responder_alert_interface}")
if qrz_hello_enabled:
logger.debug(f"System: QRZ Hello Enabled")
if volcanoAlertBroadcastEnabled:
logger.debug(f"System: Volcano Alert Broadcast Enabled on channels {volcanoAlertBroadcastChannel}")
if qrz_hello_enabled and train_qrz:
logger.debug(f"System: QRZ Welcome/Hello Enabled with training mode")
if qrz_hello_enabled and not train_qrz:
logger.debug(f"System: QRZ Welcome/Hello Enabled")
if checklist_enabled:
logger.debug(f"System: CheckList Module Enabled")
if ignoreChannels != []:
logger.debug(f"System: Ignoring Channels: {ignoreChannels}")
if enableSMTP:
if enableImap:
logger.debug(f"System: SMTP Email Alerting Enabled using IMAP")
else:
logger.debug(f"System: SMTP Email Alerting Enabled")
if scheduler_enabled:
# Examples of using the scheduler, Times here are in 24hr format
# https://schedule.readthedocs.io/en/stable/
# Reminder Scheduler is enabled every Monday at noon send a log message
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Reminder"))
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Enabled Reminder"))
# basic scheduler
if schedulerValue != '':
logger.debug(f"System: Starting the broadcast scheduler from config.ini")
if schedulerValue.lower() == 'day':
if schedulerTime != '':
# Send a message every day at the time set in schedulerTime
schedule.every().day.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
else:
# Send a message every day at the time set in schedulerInterval
schedule.every(int(schedulerInterval)).days.do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
elif 'mon' in schedulerValue.lower() and schedulerTime != '':
# Send a message every Monday at the time set in schedulerTime
schedule.every().monday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
elif 'tue' in schedulerValue.lower() and schedulerTime != '':
# Send a message every Tuesday at the time set in schedulerTime
schedule.every().tuesday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
elif 'wed' in schedulerValue.lower() and schedulerTime != '':
# Send a message every Wednesday at the time set in schedulerTime
schedule.every().wednesday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
elif 'thu' in schedulerValue.lower() and schedulerTime != '':
# Send a message every Thursday at the time set in schedulerTime
schedule.every().thursday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
elif 'fri' in schedulerValue.lower() and schedulerTime != '':
# Send a message every Friday at the time set in schedulerTime
schedule.every().friday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
elif 'sat' in schedulerValue.lower() and schedulerTime != '':
# Send a message every Saturday at the time set in schedulerTime
schedule.every().saturday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
elif 'sun' in schedulerValue.lower() and schedulerTime != '':
# Send a message every Sunday at the time set in schedulerTime
schedule.every().sunday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
elif 'hour' in schedulerValue.lower():
# Send a message every hour at the time set in schedulerTime
schedule.every(int(schedulerInterval)).hours.do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
elif 'min' in schedulerValue.lower():
# Send a message every minute at the time set in schedulerTime
schedule.every(int(schedulerInterval)).minutes.do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
else:
logger.debug(f"System: Starting the broadcast scheduler")
# Enhanced Examples of using the scheduler, Times here are in 24hr format
# https://schedule.readthedocs.io/en/stable/
# Good Morning Every day at 09:00 using send_message function to channel 2 on device 1
#schedule.every().day.at("09:00").do(lambda: send_message("Good Morning", 2, 0, 1))
# Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
#schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'), 2, 0, 1))
# Send Weather Channel Notice Wed. Noon on channel 2, device 1
#schedule.every().wednesday.at("12:00").do(lambda: send_message("Weather alerts available on 'Alerts' channel with default 'AQ==' key.", 2, 0, 1))
# Send config URL for Medium Fast Network Use every other day at 10:00 to default channel 2 on device 1
#schedule.every(2).days.at("10:00").do(lambda: send_message("Join us on Medium Fast https://meshtastic.org/e/#CgcSAQE6AggNEg4IARAEOAFAA0gBUB5oAQ", 2, 0, 1))
# Send a Net Starting Now Message Every Wednesday at 19:00 using send_message function to channel 2 on device 1
#schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
@@ -1376,7 +1529,6 @@ async def start_rx():
# Send bbslink looking for peers every other day at 10:00 using send_message function to channel 3 on device 1
#schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", 3, 0, 1))
logger.debug("System: Starting the broadcast scheduler")
await BroadcastScheduler()
# here we go loopty loo
@@ -1401,10 +1553,11 @@ async def main():
await asyncio.sleep(0.01)
try:
if __name__ == "__main__":
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
exit_handler()
pass
except KeyboardInterrupt:
exit_handler()
except SystemExit:
pass
# EOF

54
modules/README.md Normal file
View File

@@ -0,0 +1,54 @@
# Modules and Adding stuff
To help with code testing see `etc/simulator.py` to simulate a bot. I also enjoy meshtasticd(linux-native) in noradio with MQTT server and client to just emulate a mesh.
## By following these steps, you can add a new bbs option to the bot.
1. **Define the Command Handler**:
Add a new function in mesh_bot.py to handle the new command. For example, if you want to add a command `newcommand`:
```python
def handle_newcommand(message, message_from_id, deviceID):
return "This is a response from the new command."
```
Additionally you can add a whole new module.py, I recommend doing this if you need to import more stuff, try and wedge it into similar spots if you can. You will need to import the file as well, look further at `modules/system.py` for more.
2. **Add the Command to the Auto Response**:
Update the auto_response function in mesh_bot.py to include the new command:
```python
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
#...
"newcommand": lambda: handle_newcommand(message, message_from_id, deviceID),
#...
```
3. **Update the Trap List and Help**:
A quick way to do this is to edit the line 16/17 in `modules/system.py` to include the new command:
```python
#...
trap_list = ("cmd", "cmd?", "newcommand") # default trap list, with the new command added
help_message = "Bot CMD?:newcommand, "
#...
```
**If looking to merge** the prefered way would be to update `modules/system.py` Adding this block below `ping` which ends around line 28:
```python
# newcommand Configuration
newcommand_enabled = True # settings.py handles the config.ini values; this is a placeholder
if newcommand_enabled:
trap_list_newcommand = ("newcommand",)
trap_list = trap_list + trap_list_newcommand
help_message = help_message + ", newcommand"
```
5. **Test the New Command**:
Run MeshBot and test the new command by sending a message with the command `newcommand` to ensure it responds correctly.
### Running a Shell command
Using the above example and enabling the filemon module, you can make a command which calls a bash file to do things on the system.
```python
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
#...
"switchON": lambda: call_external_script(message)
```
This would call the default script located in script/runShell.sh and return its output.

View File

@@ -167,7 +167,7 @@ def bbs_sync_posts(input, peerNode, RxNode):
messageID = 0
# check if the bbs link is enabled
if bbs_link_whitelist is not None:
if bbs_link_whitelist != ['']:
if str(peerNode) not in bbs_link_whitelist:
logger.warning(f"System: BBS Link is disabled for node {peerNode}.")
return "System: BBS Link is disabled for your node."
@@ -185,11 +185,17 @@ def bbs_sync_posts(input, peerNode, RxNode):
return f"bbsack {messageID}"
elif "bbsack" in input.lower():
# increment the messageID
ack = int(input.split(" ")[1])
messageID = int(ack) + 1
if len(input.split(" ")) > 1:
try:
messageID = int(input.split(" ")[1]) + 1
except:
return "link error"
else:
return "link error"
# send message with delay to keep chutil happy
if messageID < len(bbs_messages):
logger.debug(f"System: Sending bbslink message {messageID} to peer " + str(peerNode))
time.sleep(5 + responseDelay)
# every 5 messages add extra delay
if messageID % 5 == 0:

View File

@@ -28,6 +28,8 @@ def checkin(name, date, time, location, notes):
c = conn.cursor()
try:
c.execute("INSERT INTO checkin (checkin_name, checkin_date, checkin_time, location, checkin_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time, location, notes))
# # remove any checkouts that are older than the checkin
# c.execute("DELETE FROM checkout WHERE checkout_date < ? OR (checkout_date = ? AND checkout_time < ?)", (date, date, time))
except sqlite3.OperationalError as e:
if "no such table" in str(e):
initialize_checklist_database()
@@ -36,7 +38,10 @@ def checkin(name, date, time, location, notes):
raise
conn.commit()
conn.close()
return "Checked in: " + str(name)
if reverse_in_out:
return "Checked✅Out: " + str(name)
else:
return "Checked✅In: " + str(name)
def delete_checkin(checkin_id):
# delete a checkin
@@ -47,22 +52,50 @@ def delete_checkin(checkin_id):
conn.close()
return "Checkin deleted." + str(checkin_id)
def checkout(name, date, time, location, notes):
def checkout(name, date, time_str, location, notes):
location = ", ".join(map(str, location))
# checkout a user
conn = sqlite3.connect(checklist_db)
c = conn.cursor()
try:
c.execute("INSERT INTO checkout (checkout_name, checkout_date, checkout_time, location, checkout_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time, location, notes))
# Check if the user has a checkin before checking out
c.execute("""
SELECT checkin_id FROM checkin
WHERE checkin_name = ?
AND NOT EXISTS (
SELECT 1 FROM checkout
WHERE checkout_name = checkin_name
AND (checkout_date > checkin_date OR (checkout_date = checkin_date AND checkout_time > checkin_time))
)
ORDER BY checkin_date DESC, checkin_time DESC
LIMIT 1
""", (name,))
checkin_record = c.fetchone()
if checkin_record:
c.execute("INSERT INTO checkout (checkout_name, checkout_date, checkout_time, location, checkout_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time_str, location, notes))
# calculate length of time checked in
c.execute("SELECT checkin_time FROM checkin WHERE checkin_id = ?", (checkin_record[0],))
checkin_time = c.fetchone()[0]
checkin_datetime = time.strptime(date + " " + checkin_time, "%Y-%m-%d %H:%M:%S")
time_checked_in_seconds = time.time() - time.mktime(checkin_datetime)
timeCheckedIn = time.strftime("%H:%M:%S", time.gmtime(time_checked_in_seconds))
# # remove the checkin record older than the checkout
# c.execute("DELETE FROM checkin WHERE checkin_date < ? OR (checkin_date = ? AND checkin_time < ?)", (date, date, time_str))
except sqlite3.OperationalError as e:
if "no such table" in str(e):
initialize_checklist_database()
c.execute("INSERT INTO checkout (checkout_name, checkout_date, checkout_time, location, checkout_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time, location, notes))
c.execute("INSERT INTO checkout (checkout_name, checkout_date, checkout_time, location, checkout_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time_str, location, notes))
else:
raise
conn.commit()
conn.close()
return "Checked out: " + str(name)
if checkin_record:
if reverse_in_out:
return "Checked⌛In: " + str(name) + " duration " + timeCheckedIn
else:
return "Checked⌛Out: " + str(name) + " duration " + timeCheckedIn
else:
return "None found for " + str(name)
def delete_checkout(checkout_id):
# delete a checkout
@@ -86,9 +119,16 @@ def list_checkin():
""")
rows = c.fetchall()
conn.close()
timeCheckedIn = ""
checkin_list = ""
for row in rows:
checkin_list += "Checkin ID: " + row[1] + " Date: " + row[2] + " Time: " + row[3] + " Notes: " + row[5] + "\n"
#calculate length of time checked in
timeCheckedIn = time.strftime("%H:%M:%S", time.gmtime(time.time() - time.mktime(time.strptime(row[2] + " " + row[3], "%Y-%m-%d %H:%M:%S"))))
checkin_list += "ID: " + row[1] + " checked-In for " + timeCheckedIn
if row[5] != "":
checkin_list += "📝" + row[5]
if row != rows[-1]:
checkin_list += "\n"
# if empty list
if checkin_list == "":
return "No data to display."
@@ -97,14 +137,18 @@ def list_checkin():
def process_checklist_command(nodeID, message, name="none", location="none"):
current_date = time.strftime("%Y-%m-%d")
current_time = time.strftime("%H:%M:%S")
# if user on bbs_ban_list reject command
if str(nodeID) in bbs_ban_list:
logger.warning("System: Checklist attempt from the ban list")
return "unable to process command"
try:
comment = message.split(" ", 1)[1]
except IndexError:
comment = ""
# handle checklist commands
if "checkin" in message.lower():
if ("checkin" in message.lower() and not reverse_in_out) or ("checkout" in message.lower() and reverse_in_out):
return checkin(name, current_date, current_time, location, comment)
elif "checkout" in message.lower():
elif ("checkout" in message.lower() and not reverse_in_out) or ("checkin" in message.lower() and reverse_in_out):
return checkout(name, current_date, current_time, location, comment)
elif "purgein" in message.lower():
return delete_checkin(nodeID)

View File

@@ -17,12 +17,12 @@ def read_file(file_monitor_file_path, random_line_only=False):
return "🐝buzz 💐buzz buzz🍯"
if random_line_only:
# read a random line from the file
with open(file_monitor_file_path, 'r') as f:
with open(file_monitor_file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
return random.choice(lines)
else:
# read the whole file
with open(file_monitor_file_path, 'r') as f:
with open(file_monitor_file_path, 'r', encoding='utf-8') as f:
content = f.read()
return content
except Exception as e:
@@ -31,13 +31,13 @@ def read_file(file_monitor_file_path, random_line_only=False):
def read_news():
# read the news file on demand
return read_file(news_file_path, read_news_enabled)
return read_file(news_file_path, news_random_line_only)
def write_news(content, append=False):
# write the news file on demand
try:
with open(news_file_path, 'a' if append else 'w') as f:
with open(news_file_path, 'a' if append else 'w', encoding='utf-8') as f:
f.write(content)
logger.info(f"FileMon: Updated {news_file_path}")
return True
@@ -76,7 +76,7 @@ def call_external_script(message, script="script/runShell.sh"):
logger.warning(f"FileMon: Script not found: {script_path}")
return "sorry I can't do that"
output = os.popen(f"bash {script_path} {message}").read()
output = os.popen(f"bash {script_path} {message}").read().encode('utf-8').decode('utf-8')
return output
except Exception as e:
logger.warning(f"FileMon: Error calling external script: {e}")

142
modules/games/hamtest.py Normal file
View File

@@ -0,0 +1,142 @@
# hamradio test module for meshbot DE K7MHI 2025
# depends on the JSON question data files from https://github.com/russolsen/ham_radio_question_pool
# data files which are expected to be in ../../data/hamradio/ similar to the following:
# https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/refs/heads/master/technician-2022-2026/technician.json
# https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/refs/heads/master/general-2023-2027/general.json
# https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/refs/heads/master/extra-2024-2028/extra.json
import json
import random
import os
from modules.log import *
class HamTest:
def __init__(self):
self.questions = {}
self.load_questions()
self.game = {}
def load_questions(self):
for level in ['technician', 'general', 'extra']:
try:
with open(f'{os.path.dirname(__file__)}/../../data/hamradio/{level}.json', encoding='utf-8') as f:
self.questions[level] = json.load(f)
except FileNotFoundError:
logger.error(f"File not found: ../../data/hamradio/{level}.json")
self.questions[level] = []
except json.JSONDecodeError:
logger.error(f"Error decoding JSON from file: ../../data/hamradio/{level}.json")
self.questions[level] = []
def newGame(self, id, level='technician'):
msg = f"📻New {level} quiz started, 'end' to exit."
if id in self.game:
level = self.game[id]['level']
self.game[id] = {
'level': level,
'score': 0,
'total': 0,
'errors': [],
'qId': None,
'question': None,
'answers': None,
'correct': None
}
# set the pool needed for the game
if self.game[id]['level'] == 'extra':
self.game[id]['total'] = 50
else:
self.game[id]['total'] = 35
# randomize the questions
random.shuffle(self.questions[level])
msg += f"\n{self.nextQuestion(id)}"
return msg
def nextQuestion(self, id):
level = self.game[id]['level']
# if question has the word figure in it, skip it
question = random.choice(self.questions[level])
while 'figure' in question['question'].lower():
question = random.choice(self.questions[level])
self.game[id]['question'] = question['question']
self.game[id]['answers'] = question['answers']
self.game[id]['correct'] = question['correct']
self.game[id]['qId'] = question['id']
self.game[id]['total'] -= 1
if self.game[id]['total'] == 0:
return self.endGame(id)
# ask the question and return answers in A, B, C, D format
msg = f"{self.game[id]['question']}\n"
for i, answer in enumerate(self.game[id]['answers']):
msg += f"{chr(65+i)}. {answer}\n"
return msg
def answer(self, id, answer):
if id not in self.game:
return "No game in progress"
if self.game[id]['correct'] == ord(answer.upper()) - 65:
self.game[id]['score'] += 1
return f"Correct👍\n" + self.nextQuestion(id)
else:
# record the section of the question for study aid
section = self.game[id]['qId'][:3]
self.game[id]['errors'].append(section)
# provide the correct answer
answer = [self.game[id]['correct']]
return f"Wrong.⛔️ Correct is {chr(65+self.game[id]['correct'])}\n" + self.nextQuestion(id)
def getScore(self, id):
if id not in self.game:
return "No game in progress"
score = self.game[id]['score']
total = self.game[id]['total']
level = self.game[id]['level']
if self.game[id]['errors']:
areaofstudy = max(set(self.game[id]['errors']), key = self.game[id]['errors'].count)
else:
areaofstudy = "None"
if level == 'extra':
pool = 50
else:
pool = 35
return f"Score: {score}/{pool}\nQuestions left: {total}\nArea of study: {areaofstudy}"
def endGame(self, id):
if id not in self.game:
return "No game in progress"
score = self.game[id]['score']
level = self.game[id]['level']
if level == 'extra':
# passing score for extra is 37 out of 50
passing = 37
else:
# passing score for technician and general is 26 out of 35
passing = 26
if score >= passing:
msg = f"Game over. Score: {score} 73! 🎉You passed the {level} exam."
else:
# find the most common section of the questions missed
if self.game[id]['errors']:
areaofstudy = max(set(self.game[id]['errors']), key = self.game[id]['errors'].count)
else:
areaofstudy = "None"
msg = f"Game over. Score: {score} 73! 😿You did not pass the {level} exam. \nYou may want to study {areaofstudy}."
# remove the game[id] from the list
del self.game[id]
return msg
hamtestTracker = []
hamtest = HamTest()

203
modules/games/hangman.py Normal file
View File

@@ -0,0 +1,203 @@
# Written for Meshtastic mesh-bot by ZR1RF Johannes le Roux 2025
import random
class Hangman:
WORDS = [
"ability","able","about","above","accept","according","account","across",
"act","action","activity","actually","add","address","administration","admit",
"adult","affect","after","again","against","age","agency","agent","ago",
"agree","agreement","ahead","air","all","allow","almost","alone","along",
"already","also","although","always","American","among","amount","analysis",
"and","animal","another","answer","any","anyone","anything","appear","apply",
"approach","area","argue","arm","around","arrive","art","article","artist",
"as","ask","assume","at","attack","attention","attorney","audience","author",
"authority","available","avoid","away","baby","back","bad","bag","ball",
"bank","bar","base","be","beat","beautiful","because","become","bed","before",
"begin","behavior","behind","believe","benefit","best","better","between",
"beyond","big","bill","billion","bit","black","blood","blue","board","body",
"book","born","both","box","boy","break","bring","brother","budget","build",
"building","business","but","buy","by","call","camera","campaign","can",
"cancer","candidate","capital","car","card","care","career","carry","case",
"catch","cause","cell","center","central","century","certain","certainly",
"chair","challenge","chance","change","character","charge","check","child",
"choice","choose","church","citizen","city","civil","claim","class","clear",
"clearly","close","coach","cold","collection","college","color","come",
"commercial","common","community","company","compare","computer","concern",
"condition","conference","Congress","consider","consumer","contain","continue",
"control","cost","could","country","couple","course","court","cover","create",
"crime","cultural","culture","cup","current","customer","cut","dark","data",
"daughter","day","dead","deal","death","debate","decade","decide","decision",
"deep","defense","degree","democrat","democratic","describe","design",
"despite","detail","determine","develop","development","die","difference",
"different","difficult","dinner","direction","director","discover","discuss",
"discussion","disease","do","doctor","dog","door","down","draw","dream","drive",
"drop","drug","during","each","early","east","easy","eat","economic","economy",
"edge","education","effect","effort","eight","either","election","else",
"employee","end","energy","enjoy","enough","enter","entire","environment",
"environmental","especially","establish","even","evening","event","ever",
"every","everybody","everyone","everything","evidence","exactly","example",
"executive","exist","expect","experience","expert","explain","eye","face",
"fact","factor","fail","fall","family","far","fast","father","fear","federal",
"feel","feeling","few","field","fight","figure","fill","film","final","finally",
"financial","find","fine","finger","finish","fire","firm","first","fish","five",
"floor","fly","focus","follow","food","foot","for","force","foreign","forget",
"form","former","forward","four","free","friend","from","front","full","fund",
"future","game","garden","gas","general","generation","get","girl","give",
"glass","go","goal","good","government","great","green","ground","group","grow",
"growth","guess","gun","guy","hair","half","hand","hang","happen","happy",
"hard","have","he","head","health","hear","heart","heat","heavy","help","her",
"here","herself","high","him","himself","his","history","hit","hold","home",
"hope","hospital","hot","hotel","hour","house","how","however","huge","human",
"hundred","husband","I","idea","identify","if","image","imagine","impact",
"important","improve","in","include","including","increase","indeed","indicate",
"individual","industry","information","inside","instead","institution","interest",
"interesting","international","interview","into","investment","involve","issue",
"it","item","its","itself","job","join","just","keep","key","kid","kill","kind",
"kitchen","know","knowledge","land","language","large","last","late","later",
"laugh","law","lawyer","lay","lead","leader","learn","least","leave","left",
"leg","legal","less","let","letter","level","lie","life","light","like","likely",
"line","list","listen","little","live","local","long","look","lose","loss",
"lot","love","low","machine","magazine","main","maintain","major","majority",
"make","man","manage","management","manager","many","market","marriage",
"material","matter","may","maybe","me","mean","measure","media","medical","meet",
"meeting","member","memory","mention","message","method","middle","might",
"military","million","mind","minute","miss","mission","model","modern","moment",
"money","month","more","morning","most","mother","mouth","move","movement",
"movie","Mr","Mrs","much","music","must","my","myself","name","nation",
"national","natural","nature","near","nearly","necessary","need","network",
"never","new","news","newspaper","next","nice","night","no","none","nor",
"north","not","note","nothing","notice","now","number","occur","of","off",
"offer","office","officer","official","often","oh","oil","ok","old","on",
"once","one","only","onto","open","operation","opportunity","option","or",
"order","organization","other","others","our","out","outside","over","own",
"owner","page","pain","painting","paper","parent","part","participant",
"particular","particularly","partner","party","pass","past","patient","pattern",
"pay","peace","people","per","perform","performance","perhaps","period",
"person","personal","phone","physical","pick","picture","piece","place","plan",
"plant","play","player","point","police","policy","political","politics",
"poor","popular","population","position","positive","possible","power",
"practice","prepare","present","president","pressure","pretty","prevent","price",
"private","probably","problem","process","produce","product","production",
"professional","professor","program","project","property","protect","prove",
"provide","public","pull","purpose","push","put","quality","question","quickly",
"quite","race","radio","raise","range","rate","rather","reach","read","ready",
"real","reality","realize","really","reason","receive","recent","recently",
"recognize","record","red","reduce","reflect","region","relate","relationship",
"religious","remain","remember","remove","report","represent","republican",
"require","research","resource","respond","response","responsibility","rest",
"result","return","reveal","rich","right","rise","risk","road","rock","role",
"room","rule","run","safe","same","save","say","scene","school","science",
"scientist","score","sea","season","seat","second","section","security","see",
"seek","seem","sell","send","senior","sense","series","serious","serve",
"service","set","seven","several","shake","share","she","shoot","short","shot",
"should","shoulder","show","side","sign","significant","similar","simple",
"simply","since","sing","single","sister","sit","site","situation","six","size",
"skill","skin","small","smile","so","social","society","soldier","some",
"somebody","someone","something","sometimes","son","song","soon","sort","sound",
"source","south","southern","space","speak","special","specific","speech",
"spend","sport","spring","staff","stage","stand","standard","star","start",
"state","statement","station","stay","step","still","stock","stop","store",
"story","strategy","street","strong","structure","student","study","stuff",
"style","subject","success","successful","such","suddenly","suffer","suggest",
"summer","support","sure","surface","system","table","take","talk","task","tax",
"teach","teacher","team","technology","television","tell","ten","tend","term",
"test","than","thank","that","the","their","them","themselves","then","theory",
"there","these","they","thing","think","third","this","those","though","thought",
"thousand","threat","three","through","throughout","throw","thus","time","to",
"today","together","tonight","too","top","total","tough","toward","town","trade",
"traditional","training","travel","treat","treatment","tree","trial","trip",
"trouble","true","truth","try","turn","TV","two","type","under","understand",
"unit","until","up","upon","us","use","usually","value","various","very",
"victim","view","violence","visit","voice","vote","wait","walk","wall","want",
"war","watch","water","way","we","weapon","wear","week","weight","well","west",
"western","what","whatever","when","where","whether","which","while","white",
"who","whole","whom","whose","why","wide","wife","will","win","wind","window",
"wish","with","within","without","woman","wonder","word","work","worker","world",
"worry","would","write","writer","wrong","yard","yeah","year","yes","yet","you",
"young","your","yourself","meshtastic","node","lora","mesh"]
def __init__(self):
self.game = {}
def new_game(self, id):
games = won = 0
ret = ""
if id in self.game:
games = self.game[id]["games"]
won = self.game[id]["won"]
ret += f"Total Games: {games}, Won: {won}\n"
self.game[id] = {
"word": self.random_word(),
"guesses": [],
"games": games+1,
"won": won
}
ret += self.game_continue(id)
return ret
def guess(self, id, input):
g = self.game[id]
if not input:
return
letter = input[0].lower()
if letter.isalpha() and letter not in g["guesses"]:
g["guesses"].append(letter)
def wrong_guesses(self, id):
g = self.game[id]
wrong = 0
for letter in g["guesses"]:
if letter not in g["word"]:
wrong += 1
return wrong
def won(self, id):
g = self.game[id]
for letter in g["word"]:
if letter not in g["guesses"]:
return False
return True
def mask(self, id):
g = self.game[id]
return " ".join([a if a in g["guesses"] else "_" for a in g["word"]])
def game_board(self, id):
g = self.game[id]
emotions = "😀🙂😐😑😕😔💀"
wrong = self.wrong_guesses(id)
ret = ""
if self.won(id):
ret += "🥳" + "\n"
g["won"] += 1
else:
ret += emotions[wrong] + "\n"
ret += hangman.mask(id) + "\n"
if g["guesses"]:
ret += ",".join(g["guesses"]) + "\n"
return ret
def game_continue(self, id):
return self.game_board(id) + "Guess a letter"
def game_over(self, id):
return self.game_board(id) + "Game over, the word was " + self.game[id]["word"]
def play(self, id, input):
if id not in self.game:
return self.new_game(id)
self.guess(id, input)
wrong = self.wrong_guesses(id)
if wrong >= 6 or self.won(id):
return self.game_over(id) + "\n" + self.new_game(id)
return self.game_continue(id)
def end(self, id):
del self.game[id]
def random_word(self):
return random.choice(self.WORDS)
hangmanTracker = []
hangman = Hangman()

View File

@@ -164,7 +164,7 @@ class PlayerVP:
return self.show_hand()
except Exception as e:
pass
return "ex:1,3,4 deals them new, and keeps 2,5 or (N)o to keep current (H)and"
# Method for scoring hand, calculating winnings, and outputting message
@@ -390,7 +390,7 @@ def playVideoPoker(nodeID, message):
else:
if drawCount <= 1:
msg = player.redraw(deck, message)
if msg.startswith("Send Card"):
if msg.startswith("ex:"):
# if returned error message, return it
return msg
drawCount += 1
@@ -403,7 +403,7 @@ def playVideoPoker(nodeID, message):
if drawCount == 2:
# this is the last draw will carry on to endGame for scoring
msg = player.redraw(deck, message) + f"\n"
if msg.startswith("Send Card"):
if msg.startswith("ex:"):
# if returned error message, return it
return msg
# redraw done

View File

@@ -12,7 +12,7 @@ from modules.log import *
trap_list_location_eu = ("ukalert", "ukwx", "ukflood")
trap_list_location_de = ("dealert", "dewx", "deflood")
def get_govUK_alerts(shortAlerts=False):
def get_govUK_alerts(lat, lon):
try:
# get UK.gov alerts
url = 'https://www.gov.uk/alerts'

View File

@@ -146,6 +146,12 @@ def llm_query(input, nodeID=0, location_name=None):
googleResults = []
if not location_name:
location_name = "no location provided "
# remove askai: and ask: from the input
for trap in trap_list_llm:
if input.lower().startswith(trap):
input = input[len(trap):].strip()
break
# add the naughty list here to stop the function before we continue
# add a list of allowed nodes only to use the function
@@ -211,6 +217,10 @@ def llm_query(input, nodeID=0, location_name=None):
if result.status_code == 200:
result_json = result.json()
result = result_json.get("response", "")
# deepseek-r1 has added <think> </think> tags to the response
if "<think>" in result:
result = result.split("</think>")[1]
else:
raise Exception(f"HTTP Error: {result.status_code}")

View File

@@ -9,7 +9,7 @@ import bs4 as bs # pip install beautifulsoup4
import xml.dom.minidom
from modules.log import *
trap_list_location = ("whereami", "tide", "wx", "wxc", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow")
trap_list_location = ("whereami", "tide", "wx", "wxc", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow","valert")
def where_am_i(lat=0, lon=0, short=False, zip=False):
whereIam = ""
@@ -71,7 +71,16 @@ def where_am_i(lat=0, lon=0, short=False, zip=False):
def getRepeaterBook(lat=0, lon=0):
grid = mh.to_maiden(float(lat), float(lon))
data = []
repeater_url = f"https://www.repeaterbook.com/repeaters/prox_result.php?city={grid}&lat=&long=&distance=50&Dunit=m&band%5B%5D=4&band%5B%5D=16&freq=&call=&mode%5B%5D=1&mode%5B%5D=2&mode%5B%5D=4&mode%5B%5D=64&status_id=1&use=%25&use=OPEN&order=distance_calc%2C+state_id+ASC"
# check if in the US or not
usapi ="https://www.repeaterbook.com/repeaters/prox_result.php?"
elsewhereapi = "https://www.repeaterbook.com/row_repeaters/prox2_result.php?"
if grid[:2] in ['CN', 'DN', 'EN', 'FN', 'CM', 'DM', 'EM', 'FM', 'DL', 'EL', 'FL']:
repeater_url = usapi
else:
repeater_url = elsewhereapi
repeater_url += f"city={grid}&lat=&long=&distance=50&Dunit=m&band%5B%5D=4&band%5B%5D=16&freq=&call=&mode%5B%5D=1&mode%5B%5D=2&mode%5B%5D=4&mode%5B%5D=64&status_id=1&use=%25&use=OPEN&order=distance_calc%2C+state_id+ASC"
try:
msg = ''
response = requests.get(repeater_url)
@@ -95,10 +104,8 @@ def getRepeaterBook(lat=0, lon=0):
'direction': cells[i + 9].text.strip() if i + 9 < len(cells) else 'N/A'
}
data.append(repeater)
else:
msg = "bug?Not enough columns"
else:
msg = "bug?Table not found"
msg = "No Data for your Region"
except Exception as e:
msg = "No repeaters found 😔"
# Limit the output to the first 4 repeaters
@@ -227,38 +234,39 @@ def get_NOAAweather(lat=0, lon=0, unit=0):
# get weather data from NOAA units for metric unit = 1 is metric
if use_metric:
unit = 1
logger.debug("Location: new API metric units not implemented yet")
weather_url = "https://forecast.weather.gov/MapClick.php?FcstType=text&lat=" + str(lat) + "&lon=" + str(lon)
if unit == 1:
weather_url += "&unit=1"
weather_api = "https://api.weather.gov/points/" + str(lat) + "," + str(lon)
# extract the "forecast": property from the JSON response
try:
weather_data = requests.get(weather_url, timeout=urlTimeoutSeconds)
weather_data = requests.get(weather_api, timeout=urlTimeoutSeconds)
if not weather_data.ok:
logger.error("Location:Error fetching weather data from NOAA")
logger.warning("Location:Error fetching weather data from NOAA for location")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.error("Location:Error fetching weather data from NOAA")
logger.warning("Location:Error fetching weather data from NOAA for location")
return ERROR_FETCHING_DATA
# get the forecast URL from the JSON response
weather_json = weather_data.json()
forecast_url = weather_json['properties']['forecast']
try:
forecast_data = requests.get(forecast_url, timeout=urlTimeoutSeconds)
if not forecast_data.ok:
logger.warning("Location:Error fetching weather forecast from NOAA")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.warning("Location:Error fetching weather forecast from NOAA")
return ERROR_FETCHING_DATA
soup = bs.BeautifulSoup(weather_data.text, 'html.parser')
table = soup.find('div', id="detailed-forecast-body")
# from periods, get the detailedForecast from number of days in NOAAforecastDuration
forecast_json = forecast_data.json()
forecast = forecast_json['properties']['periods']
for day in forecast[:forecastDuration]:
# abreviate the forecast
if table is None:
logger.error("Location:Bad weather data from NOAA")
return ERROR_FETCHING_DATA
else:
# get rows
rows = table.find_all('div', class_="row")
# extract data from rows
for row in rows:
# shrink the text
line = abbreviate_noaa(row.text)
# only grab a few days of weather
if len(weather.split("\n")) < forecastDuration:
weather += line + "\n"
# trim off last newline
weather += abbreviate_noaa(day['name']) + ": " + abbreviate_noaa(day['detailedForecast']) + "\n"
# remove last newline
weather = weather[:-1]
# get any alerts and return the count
@@ -280,20 +288,13 @@ def get_NOAAweather(lat=0, lon=0, unit=0):
def abbreviate_noaa(row):
# replace long strings with shorter ones for display
replacements = {
"monday": "Mon ",
"tuesday": "Tue ",
"wednesday": "Wed ",
"thursday": "Thu ",
"friday": "Fri ",
"saturday": "Sat ",
"sunday": "Sun ",
"today": "Today ",
"night": "Night ",
"tonight": "Tonight ",
"tomorrow": "Tomorrow ",
"day": "Day ",
"this afternoon": "Afternoon ",
"overnight": "Overnight ",
"monday": "Mon",
"tuesday": "Tue",
"wednesday": "Wed",
"thursday": "Thu",
"friday": "Fri",
"saturday": "Sat",
"sunday": "Sun",
"northwest": "NW",
"northeast": "NE",
"southwest": "SW",
@@ -323,6 +324,9 @@ def abbreviate_noaa(row):
"degrees": "°",
"percent": "%",
"department": "Dept.",
"amounts less than a tenth of an inch possible.": "< 0.1in",
"temperatures": "temps.",
"temperature": "temp.",
}
line = row
@@ -393,6 +397,12 @@ def alertBrodcastNOAA():
elif currentAlert == NO_ALERTS:
wxAlertCacheNOAA = ""
return False
if ignoreEASenable:
# check if the alert is in the ignoreEAS list
for word in ignoreEASwords:
if word.lower() in currentAlert[0].lower():
logger.debug(f"Location:Ignoring NOAA Alert: {currentAlert[0]} containing {word}")
return False
# broadcast the alerts send to wxBrodcastCh
elif currentAlert[0] not in wxAlertCacheNOAA:
# Check if the current alert is not in the weather alert cache
@@ -457,12 +467,11 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
# get the latest IPAWS alert from FEMA
alert = ''
alerts = []
linked_data = ''
# set the API URL for IPAWS
namespace = "urn:oasis:names:tc:emergency:cap:1.2"
alert_url = "https://apps.fema.gov/IPAWSOPEN_EAS_SERVICE/rest/feed"
if ipawsPIN != "000000":
alert_url += "?pin=" + ipawsPIN
# get the alerts from FEMA
try:
@@ -480,23 +489,49 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
# extract alerts from main feed
for entry in alertxml.getElementsByTagName("entry"):
link = entry.getElementsByTagName("link")[0].getAttribute("href")
## state FIPS
## This logic is being added to reduce load on FEMA server.
stateFips = None
for cat in entry.getElementsByTagName("category"):
if cat.getAttribute("label") == "statefips":
stateFips = cat.getAttribute("term")
break
if stateFips is None:
# no stateFIPS found — skip
continue
# check if it matches your list
if stateFips not in myStateFIPSList:
#logger.debug(f"Skipping FEMA record link {link} with stateFIPS code of: {stateFips} because it doesn't match our StateFIPSList {myStateFIPSList}")
continue # skip to next entry
try:
#pin check
if ipawsPIN != "000000":
link += "?pin=" + ipawsPIN
# get the linked alert data from FEMA
linked_data = requests.get(link, timeout=urlTimeoutSeconds)
if not linked_data.ok:
if not linked_data.ok or not linked_data.text.strip():
# if the linked data is not ok, skip this alert
#logger.warning(f"System: iPAWS Error fetching linked alert data from {link}")
continue
else:
linked_xml = xml.dom.minidom.parseString(linked_data.text)
# this alert is a full CAP alert
except (requests.exceptions.RequestException):
logger.warning(f"System: iPAWS Error fetching embedded alert data from {link}")
continue
# this alert is a full CAP alert
linked_xml = xml.dom.minidom.parseString(linked_data.text)
except xml.parsers.expat.ExpatError:
logger.warning(f"System: iPAWS Error parsing XML from {link}")
continue
except Exception as e:
logger.debug(f"System: iPAWS Error processing alert data from {link}: {e}")
continue
for info in linked_xml.getElementsByTagName("info"):
# only get en-US language alerts (alternative is es-US)
language_nodes = info.getElementsByTagName("language")
if not any(node.firstChild and node.firstChild.nodeValue.strip() == "en-US" for node in language_nodes):
continue # skip if not en-US
# extract values from XML
sameVal = "NONE"
geocode_value = "NONE"
@@ -510,31 +545,35 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
if info.getElementsByTagName("description") and info.getElementsByTagName("description")[0].childNodes:
description = info.getElementsByTagName("description")[0].childNodes[0].nodeValue
else:
logger.debug(f"System: report this to discord - iPAWS No description for alert: {headline}")
description = headline
area_table = info.getElementsByTagName("area")[0]
areaDesc = area_table.getElementsByTagName("areaDesc")[0].childNodes[0].nodeValue
geocode_table = area_table.getElementsByTagName("geocode")[0]
geocode_type = geocode_table.getElementsByTagName("valueName")[0].childNodes[0].nodeValue
geocode_value = geocode_table.getElementsByTagName("value")[0].childNodes[0].nodeValue
if geocode_type == "SAME":
sameVal = geocode_value
except Exception as e:
logger.debug(f"System: iPAWS Error extracting alert data: {link}")
#print(f"DEBUG: {info.toprettyxml()}")
continue
# check if the alert is for the current location, if wanted keep alert
if (sameVal in mySAME) or (geocode_value in mySAME):
# check if the alert is for the SAME location, if wanted keep alert
if (sameVal in mySAMEList) or (geocode_value in mySAMEList) or mySAMEList == ['']:
# ignore the FEMA test alerts
if ignoreFEMAtest:
if "Test" in headline:
logger.debug(f"System: Ignoring FEMA Test Alert: {headline} for {areaDesc}")
continue
if ignoreFEMAenable:
ignore_alert = False
for word in ignoreFEMAwords:
if word.lower() in headline.lower():
logger.debug(f"System: Filtering FEMA Alert by WORD: {headline} containing {word} at {areaDesc}")
ignore_alert = True
break
if ignore_alert:
continue
# add to alerts list
# add to alert list
alerts.append({
'alertType': alertType,
'alertCode': alertCode,
@@ -544,10 +583,10 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
'geocode_value': geocode_value,
'description': description
})
# else:
# # these are discarded some day but logged for debugging currently
# logger.debug(f"Debug iPAWS: Type:{alertType} Code:{alertCode} Desc:{areaDesc} GeoType:{geocode_type} GeoVal:{geocode_value}, Headline:{headline}")
else:
logger.debug(f"System: iPAWS Alert not in SAME List: {sameVal} or {geocode_value} for {headline} at {areaDesc}")
continue
# return the numWxAlerts of alerts
if len(alerts) > 0:
for alertItem in alerts[:numWxAlerts]:
@@ -612,3 +651,51 @@ def get_flood_noaa(lat=0, lon=0, uid=0):
return flood_data
def get_volcano_usgs(lat=0, lon=0):
alerts = ''
if lat == 0 and lon == 0:
lat = latitudeValue
lon = longitudeValue
# get the latest volcano alert from USGS from CAP feed
usgs_volcano_url = "https://volcanoes.usgs.gov/hans-public/api/volcano/getCapElevated"
try:
volcano_data = requests.get(usgs_volcano_url, timeout=urlTimeoutSeconds)
if not volcano_data.ok:
logger.warning("System: Issue with fetching volcano alerts from USGS")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.warning("System: Issue with fetching volcano alerts from USGS")
return ERROR_FETCHING_DATA
volcano_json = volcano_data.json()
# extract alerts from main feed
if volcano_json and isinstance(volcano_json, list):
for alert in volcano_json:
# check ignore list
if ignoreUSGSEnable:
for word in ignoreUSGSwords:
if word.lower() in alert['volcano_name_appended'].lower():
logger.debug(f"System: Ignoring USGS Alert: {alert['volcano_name_appended']} containing {word}")
continue
# check if the alert lat long is within the range of bot latitudeValue and longitudeValue
if (alert['latitude'] >= latitudeValue - 10 and alert['latitude'] <= latitudeValue + 10) and (alert['longitude'] >= longitudeValue - 10 and alert['longitude'] <= longitudeValue + 10):
volcano_name = alert['volcano_name_appended']
alert_level = alert['alert_level']
color_code = alert['color_code']
cap_severity = alert['cap_severity']
synopsis = alert['synopsis']
# format Alert
alerts += f"🌋🚨: {volcano_name}, {alert_level} {color_code}, {cap_severity}.\n{synopsis}\n"
else:
#logger.debug(f"System: USGS volcano alert not in range: {alert['volcano_name_appended']}")
continue
else:
logger.debug("Location:Error fetching volcano data from USGS")
return NO_ALERTS
if alerts == "":
return NO_ALERTS
# trim off last newline
if alerts[-1] == "\n":
alerts = alerts[:-1]
# return the alerts
alerts = abbreviate_noaa(alerts)
return alerts

View File

@@ -69,32 +69,28 @@ logger.addHandler(stdout_handler)
if syslog_to_file:
# Create file handler for logging to a file
file_handler_sys = TimedRotatingFileHandler('logs/meshbot.log', when='midnight', backupCount=log_backup_count)
file_handler_sys = TimedRotatingFileHandler('logs/meshbot.log', when='midnight', backupCount=log_backup_count, encoding='utf-8')
file_handler_sys.setLevel(LOGGING_LEVEL) # DEBUG used by default for system logs to disk
file_handler_sys.setFormatter(plainFormatter(logFormat))
logger.addHandler(file_handler_sys)
if log_messages_to_file:
# Create file handler for logging to a file
file_handler = TimedRotatingFileHandler('logs/messages.log', when='midnight', backupCount=log_backup_count)
file_handler = TimedRotatingFileHandler('logs/messages.log', when='midnight', backupCount=log_backup_count, encoding='utf-8')
file_handler.setLevel(logging.INFO) # INFO used for messages to disk
file_handler.setFormatter(logging.Formatter(msgLogFormat))
msgLogger.addHandler(file_handler)
# Pretty Timestamp
def getPrettyTime(seconds):
# convert unix time to minutes, hours, or days, or years for simple display
designator = "s"
if seconds > 0:
seconds = round(seconds / 60)
designator = "m"
if seconds > 60:
seconds = round(seconds / 60)
designator = "h"
if seconds > 24:
seconds = round(seconds / 24)
designator = "d"
if seconds > 365:
seconds = round(seconds / 365)
designator = "y"
return str(seconds) + designator
# convert unix time to minutes, hours, days, or years for simple display
if seconds < 60:
return f"{int(seconds)}s"
elif seconds < 3600:
return f"{int(round(seconds / 60))}m"
elif seconds < 86400:
return f"{int(round(seconds / 3600))}h"
elif seconds < 31536000:
return f"{int(round(seconds / 86400))}d"
else:
return f"{int(round(seconds / 31536000))}y"

View File

@@ -18,19 +18,37 @@ def never_seen_before(nodeID):
# check if we have seen this node before and sent a hello message
conn = sqlite3.connect(qrz_db)
c = conn.cursor()
c.execute("SELECT * FROM qrz WHERE qrz_call = ?", (nodeID,))
row = c.fetchone()
conn.close()
if row is None:
return True
else:
return False
try:
c.execute("SELECT * FROM qrz WHERE qrz_call = ?", (nodeID,))
row = c.fetchone()
conn.close()
if row is None:
# we have not seen this node before
return True
else:
# we have seen this node before
return False
except sqlite3.OperationalError as e:
if "no such table" in str(e):
initalize_qrz_database()
logger.warning("QRZ database table not found, created new table")
# we have not seen this node before
return True
else:
raise
def hello(nodeID, name, qth, notes):
def hello(nodeID, name):
# send a hello message
conn = sqlite3.connect(qrz_db)
c = conn.cursor()
c.execute("INSERT INTO qrz (qrz_call, qrz_name, qrz_qth, qrz_notes) VALUES (?, ?, ?, ?)", (nodeID, name, qth, notes))
try:
c.execute("INSERT INTO qrz (qrz_call, qrz_name) VALUES (?, ?)", (nodeID, str(name)))
except sqlite3.OperationalError as e:
if "no such table" in str(e):
initalize_qrz_database()
c.execute("INSERT INTO qrz (qrz_call, qrz_name) VALUES (?, ?)", (nodeID, str(name)))
else:
raise
conn.commit()
conn.close()
return True

View File

@@ -21,12 +21,10 @@ ping_enabled = True # ping feature to respond to pings, ack's etc.
sitrep_enabled = True # sitrep feature to respond to sitreps
lastHamLibAlert = 0 # last alert from hamlib
lastFileAlert = 0 # last alert from file monitor
max_retry_count1 = 4 # max retry count for interface 1
max_retry_count2 = 4 # max retry count for interface 2
max_retry_count1 = max_retry_count2 = max_retry_count3 = max_retry_count4 = max_retry_count5 = max_retry_count6 = max_retry_count7 = max_retry_count8 = max_retry_count9 = 4 # default retry count for interfaces
retry_int1 = False
retry_int2 = False
wiki_return_limit = 3 # limit the number of sentences returned off the first paragraph first hit
playingGame = False
GAMEDELAY = 28800 # 8 hours in seconds for game mode holdoff
cmdHistory = [] # list to hold the last commands
seenNodes = [] # list to hold the last seen nodes
@@ -36,7 +34,7 @@ config = configparser.ConfigParser()
config_file = "config.ini"
try:
config.read(config_file)
config.read(config_file, encoding='utf-8')
except Exception as e:
print(f"System: Error reading config file: {e}")
@@ -96,7 +94,7 @@ if 'checklist' not in config:
config.write(open(config_file, 'w'))
if 'qrz' not in config:
config['qrz'] = {'enabled': 'False', 'qrz_db': 'data/qrz.db'}
config['qrz'] = {'enabled': 'False', 'qrz_db': 'data/qrz.db', 'qrz_hello_string': 'send CMD or DM me for more info.'}
config.write(open(config_file, 'w'))
# interface1 settings
@@ -196,7 +194,9 @@ try:
# general
useDMForResponse = config['general'].getboolean('respond_by_dm_only', True)
publicChannel = config['general'].getint('defaultChannel', 0) # the meshtastic public channel
ignoreChannels = config['general'].get('ignoreChannels', '').split(',') # ignore these channels
ignoreDefaultChannel = config['general'].getboolean('ignoreDefaultChannel', False)
cmdBang = config['general'].getboolean('cmdBang', False) # default off
zuluTime = config['general'].getboolean('zuluTime', False) # aka 24 hour time
log_messages_to_file = config['general'].getboolean('LogMessagesToFile', False) # default off
log_backup_count = config['general'].getint('LogBackupCount', 32) # default 32 days
@@ -221,6 +221,8 @@ try:
llm_enabled = config['general'].getboolean('ollama', False) # https://ollama.com
llmModel = config['general'].get('ollamaModel', 'gemma2:2b') # default gemma2:2b
ollamaHostName = config['general'].get('ollamaHostName', 'http://localhost:11434') # default localhost
llmReplyToNonCommands = config['general'].getboolean('llmReplyToNonCommands', True)
dont_retry_disconnect = config['general'].getboolean('dont_retry_disconnect', False) # default False, retry on disconnect
# emergency response
emergency_responder_enabled = config['emergencyHandler'].getboolean('enabled', False)
emergency_responder_alert_channel = config['emergencyHandler'].getint('alert_channel', 2) # default 2
@@ -234,6 +236,10 @@ try:
sentryIgnoreList = config['sentry'].get('sentryIgnoreList', '').split(',')
sentry_radius = config['sentry'].getint('SentryRadius', 100) # default 100 meters
email_sentry_alerts = config['sentry'].getboolean('emailSentryAlerts', False) # default False
highfly_enabled = config['sentry'].getboolean('highFlyingAlert', True) # default True
highfly_altitude = config['sentry'].getint('highFlyingAlertAltitude', 2000) # default 2000 meters
highfly_channel = config['sentry'].getint('highFlyingAlertChannel', 2) # default 2
highfly_ignoreList = config['sentry'].get('highFlyingIgnoreList', '').split(',') # default empty
# location
location_enabled = config['location'].getboolean('enabled', True)
@@ -251,15 +257,22 @@ try:
enableGBalerts = config['location'].getboolean('enableGBalerts', False) # default False
enableDEalerts = config['location'].getboolean('enableDEalerts', False) # default False
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True
mySAME = config['location'].get('mySAME', '').split(',') # default empty
ignoreEASenable = config['location'].getboolean('ignoreEASenable', False) # default False
ignoreEASwords = config['location'].get('ignoreEASwords', 'test,advisory').split(',') # default test,advisory
myRegionalKeysDE = config['location'].get('myRegionalKeysDE', '110000000000').split(',') # default city Berlin
forecastDuration = config['location'].getint('NOAAforecastDuration', 4) # NOAA forcast days
numWxAlerts = config['location'].getint('NOAAalertCount', 2) # default 2 alerts
enableExtraLocationWx = config['location'].getboolean('enableExtraLocationWx', False) # default False
ipawsPIN = config['location'].get('ipawsPIN', '000000') # default 000000
ignoreFEMAtest = config['location'].getboolean('ignoreFEMAtest', True) # default True
myStateFIPSList = config['location'].get('myFIPSList', '').split(',') # default empty
mySAMEList = config['location'].get('mySAMEList', '').split(',') # default empty
ignoreFEMAenable = config['location'].getboolean('ignoreFEMAenable', True) # default True
ignoreFEMAwords = config['location'].get('ignoreFEMAwords', 'test,exercise').split(',') # default test,exercise
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh', '2').split(',') # default Channel 2
emergencyAlertBroadcastCh = config['location'].get('eAlertBroadcastCh', '2').split(',') # default Channel 2
volcanoAlertBroadcastEnabled = config['location'].getboolean('volcanoAlertBroadcastEnabled', False) # default False
volcanoAlertBroadcastChannel = config['location'].get('volcanoAlertBroadcastCh', '2').split(',') # default Channel 2
ignoreUSGSEnable = config['location'].getboolean('ignoreVolcanoEnable', False) # default False
ignoreUSGSWords = config['location'].get('ignoreVolcanoWords', 'test,advisory').split(',') # default test,advisory
# bbs
bbs_enabled = config['bbs'].getboolean('enabled', False)
@@ -272,10 +285,13 @@ try:
# checklist
checklist_enabled = config['checklist'].getboolean('enabled', False)
checklist_db = config['checklist'].get('checklist_db', 'data/checklist.db')
reverse_in_out = config['checklist'].getboolean('reverse_in_out', False)
# qrz hello
qrz_hello_enabled = config['qrz'].getboolean('enabled', False)
qrz_db = config['qrz'].get('qrz_db', 'data/qrz.db')
qrz_hello_string = config['qrz'].get('qrz_hello_string', 'MeshBot says Hello! DM for more info.')
train_qrz = config['qrz'].getboolean('training', True)
# E-Mail Settings
sysopEmails = config['smtp'].get('sysopEmails', '').split(',')
@@ -300,6 +316,12 @@ try:
# scheduler
scheduler_enabled = config['scheduler'].getboolean('enabled', False)
schedulerInterface = config['scheduler'].getint('interface', 1) # default interface 1
schedulerChannel = config['scheduler'].getint('channel', 2) # default channel 2
schedulerMessage = config['scheduler'].get('message', 'Scheduled message') # default message
schedulerInterval = config['scheduler'].get('interval', '') # default empty
schedulerTime = config['scheduler'].get('time', '') # default empty
schedulerValue = config['scheduler'].get('value', '') # default empty
# radio monitoring
radio_detection_enabled = config['radioMon'].getboolean('enabled', False)
@@ -313,7 +335,7 @@ try:
# file monitor
file_monitor_enabled = config['fileMon'].getboolean('filemon_enabled', False)
file_monitor_file_path = config['fileMon'].get('file_path', 'alert.txt') # default alert.txt
file_monitor_broadcastCh = config['fileMon'].getint('broadcastCh', 2) # default 2
file_monitor_broadcastCh = config['fileMon'].get('broadcastCh', '2').split(',') # default Channel 2
read_news_enabled = config['fileMon'].getboolean('enable_read_news', False) # default disabled
news_file_path = config['fileMon'].get('news_file_path', 'news.txt') # default news.txt
news_random_line_only = config['fileMon'].getboolean('news_random_line', False) # default False
@@ -327,6 +349,8 @@ try:
videoPoker_enabled = config['games'].getboolean('videoPoker', True)
mastermind_enabled = config['games'].getboolean('mastermind', True)
golfSim_enabled = config['games'].getboolean('golfSim', True)
hangman_enabled = config['games'].getboolean('hangman', True)
hamtest_enabled = config['games'].getboolean('hamtest', True)
# messaging settings
responseDelay = config['messagingSettings'].getfloat('responseDelay', 0.7) # default 0.7
@@ -334,6 +358,7 @@ try:
MESSAGE_CHUNK_SIZE = config['messagingSettings'].getint('MESSAGE_CHUNK_SIZE', 160) # default 160
wantAck = config['messagingSettings'].getboolean('wantAck', False) # default False
maxBuffer = config['messagingSettings'].getint('maxBuffer', 220) # default 220
enableHopLogs = config['messagingSettings'].getboolean('enableHopLogs', False) # default False
except KeyError as e:
print(f"System: Error reading config file: {e}")

View File

@@ -77,7 +77,7 @@ if location_enabled:
help_message = help_message + ", whereami, wx, wxc, rlist"
if enableGBalerts and not enableDEalerts:
from modules.globalalert import * # from the spudgunman/meshing-around repo
trap_list = trap_list + trap_list_location_eu
logger.warning(f"System: GB Alerts not functional at this time need to find a source API")
#help_message = help_message + ", ukalert, ukwx, ukflood"
if enableDEalerts and not enableGBalerts:
from modules.globalalert import * # from the spudgunman/meshing-around repo
@@ -89,7 +89,14 @@ if location_enabled:
from modules.wx_meteo import * # from the spudgunman/meshing-around repo
else:
# NOAA only features
help_message = help_message + ", wxa, tide, ealert"
help_message = help_message + ", wxa, tide"
# NOAA alerts needs location module
if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled or volcanoAlertBroadcastEnabled:
from modules.locationdata import * # from the spudgunman/meshing-around repo
# limited subset, this should be done better but eh..
trap_list = trap_list + ("wx", "wxc", "wxa", "wxalert", "ea", "ealert", "valert")
help_message = help_message + ", wxalert, ealert, valert"
# BBS Configuration
if bbs_enabled:
@@ -151,7 +158,17 @@ if golfSim_enabled:
from modules.games.golfsim import * # from the spudgunman/meshing-around repo
trap_list = trap_list + ("golfsim",)
games_enabled = True
if hangman_enabled:
from modules.games.hangman import * # from the spudgunman/meshing-around repo
trap_list = trap_list + ("hangman",)
games_enabled = True
if hamtest_enabled:
from modules.games.hamtest import * # from the spudgunman/meshing-around repo
trap_list = trap_list + ("hamtest",)
games_enabled = True
# Games Configuration
if games_enabled is True:
help_message = help_message + ", games"
@@ -172,6 +189,10 @@ if games_enabled is True:
gamesCmdList += "masterMind, "
if golfSim_enabled:
gamesCmdList += "golfSim, "
if hangman_enabled:
gamesCmdList += "hangman, "
if hamtest_enabled:
gamesCmdList += "hamTest, "
gamesCmdList = gamesCmdList[:-2] # remove the last comma
else:
gamesCmdList = ""
@@ -207,7 +228,7 @@ if radio_detection_enabled:
from modules.radio import * # from the spudgunman/meshing-around repo
# File Monitor Configuration
if file_monitor_enabled or read_news_enabled:
if file_monitor_enabled or read_news_enabled or bee_enabled:
from modules.filemon import * # from the spudgunman/meshing-around repo
if read_news_enabled:
trap_list = trap_list + trap_list_filemon # items readnews
@@ -234,6 +255,7 @@ if ble_count > 1:
logger.debug(f"System: Initializing Interfaces")
interface1 = interface2 = interface3 = interface4 = interface5 = interface6 = interface7 = interface8 = interface9 = None
retry_int1 = retry_int2 = retry_int3 = retry_int4 = retry_int5 = retry_int6 = retry_int7 = retry_int8 = retry_int9 = False
max_retry_count1 = max_retry_count2 = max_retry_count3 = max_retry_count4 = max_retry_count5 = max_retry_count6 = max_retry_count7 = max_retry_count8 = max_retry_count9 = 3
for i in range(1, 10):
interface_type = globals().get(f'interface{i}_type')
if not interface_type or interface_type == 'none' or globals().get(f'interface{i}_enabled') == False:
@@ -363,27 +385,27 @@ def get_node_list(nodeInt=1):
return node_list
def get_node_location(number, nodeInt=1, channel=0):
def get_node_location(nodeID, nodeInt=1, channel=0):
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]
lastheard = 0
if interface.nodes:
for node in interface.nodes.values():
if number == node['num']:
if 'position' in node:
if nodeID == node['num']:
if 'position' in node and node['position'] is not {}:
try:
latitude = node['position']['latitude']
longitude = node['position']['longitude']
logger.debug(f"System: location data for {nodeID} is {latitude},{longitude}")
position = [latitude,longitude]
except Exception as e:
logger.warning(f"System: Error getting location data for {number}")
logger.debug(f"System: location data for {number} is {latitude},{longitude}")
position = [latitude,longitude]
logger.debug(f"System: No location data for {nodeID} use default location")
return position
else:
logger.warning(f"System: No location data for {number} using default location")
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}")
@@ -392,7 +414,7 @@ def get_node_location(number, nodeInt=1, channel=0):
# logger.error(f"System: Error requesting location data for {number}. Error: {e}")
return position
else:
logger.warning(f"System: No nodes found")
logger.warning(f"System: Location for NodeID {nodeID} not found in nodeDb")
return position
@@ -439,8 +461,35 @@ def get_closest_nodes(nodeInt=1,returnCount=3):
else:
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
interface = globals()[f'interface{nodeInt}']
myNodeNumber = globals().get(f'myNodeNum{nodeInt}')
if aor:
interface.getNode(myNodeNumber).addFavorite(nodeID)
logger.info(f"System: Added {nodeID} to favorites")
else:
interface.getNode(myNodeNumber).removeFavorite(nodeID)
logger.info(f"System: Removed {nodeID} from favorites")
def getFavoritNodes(nodeInt=1):
interface = globals()[f'interface{nodeInt}']
myNodeNumber = globals().get(f'myNodeNum{nodeInt}')
favList = []
for node in interface.getNode(myNodeNumber).favorites:
favList.append(node)
return favList
def handleSentinelIgnore(nodeInt=1, nodeID=0, aor=False):
#aor is add or remove if True add, if False remove
if aor:
sentryIgnoreList.append(str(nodeID))
logger.info(f"System: Added {nodeID} to sentry ignore list")
else:
sentryIgnoreList.remove(str(nodeID))
logger.info(f"System: Removed {nodeID} from sentry ignore list")
def messageChunker(message):
message_list = []
if len(message) > MESSAGE_CHUNK_SIZE:
@@ -460,9 +509,9 @@ def messageChunker(message):
sentence = ''
for char in part:
sentence += char
if char in '.!?':
sentences.append(sentence.strip())
sentence = ''
# if char in '.!?':
# sentences.append(sentence.strip())
# sentence = ''
if sentence:
sentences.append(sentence.strip())
@@ -496,8 +545,12 @@ def messageChunker(message):
final_message_list = []
for chunk in message_list:
while len(chunk) > MESSAGE_CHUNK_SIZE:
final_message_list.append(chunk[:MESSAGE_CHUNK_SIZE])
chunk = chunk[MESSAGE_CHUNK_SIZE:]
# Find the last space within the chunk size limit
split_index = chunk.rfind(' ', 0, MESSAGE_CHUNK_SIZE)
if split_index == -1:
split_index = MESSAGE_CHUNK_SIZE
final_message_list.append(chunk[:split_index])
chunk = chunk[split_index:].strip()
if chunk:
final_message_list.append(chunk)
@@ -614,6 +667,8 @@ def messageTrap(msg):
# if word in message is in the trap list, return True
if t.lower() == m.lower():
return True
if cmdBang and m.startswith("!"):
return True
# if no trap words found, run a search for near misses like ping? or cmd?
for m in message_list:
for t in range(len(trap_list)):
@@ -669,12 +724,15 @@ def handleMultiPing(nodeID=0, deviceID=1):
multiPingList.pop(j)
break
priorVolcanoAlert = ""
def handleAlertBroadcast(deviceID=1):
global priorVolcanoAlert
alertUk = NO_ALERTS
alertDe = NO_ALERTS
alertFema = NO_ALERTS
wxAlert = NO_ALERTS
volcanoAlert = NO_ALERTS
alertWx = False
# only allow API call every 20 minutes
# the watchdog will call this function 3 times, seeing possible throttling on the API
clock = datetime.now()
@@ -698,7 +756,7 @@ def handleAlertBroadcast(deviceID=1):
# format alert
if alertWx:
wxAlert = f"🚨 {alertWx[1]} EAS WX ALERT: {alertWx[0]}"
wxAlert = f"🚨 {alertWx[1]} EAS-WX ALERT: {alertWx[0]}"
else:
wxAlert = False
@@ -730,8 +788,8 @@ def handleAlertBroadcast(deviceID=1):
send_message(ukAlert, emergencyAlertBroadcastCh, 0, deviceID)
return True
# pause for 10 seconds
time.sleep(10)
# pause for traffic
time.sleep(5)
if wxAlertBroadcastEnabled:
if wxAlert:
@@ -741,42 +799,27 @@ def handleAlertBroadcast(deviceID=1):
else:
send_message(wxAlert, wxAlertBroadcastChannel, 0, deviceID)
return True
# pause for traffic
time.sleep(5)
if volcanoAlertBroadcastEnabled:
volcanoAlert = get_volcano_usgs(latitudeValue, longitudeValue)
if volcanoAlert and NO_ALERTS not in volcanoAlert and ERROR_FETCHING_DATA not in volcanoAlert:
# check if the alert is different from the last one
if volcanoAlert != priorVolcanoAlert:
priorVolcanoAlert = volcanoAlert
if isinstance(volcanoAlertBroadcastChannel, list):
for channel in volcanoAlertBroadcastChannel:
send_message(volcanoAlert, int(channel), 0, deviceID)
else:
send_message(volcanoAlert, volcanoAlertBroadcastChannel, 0, deviceID)
return True
def onDisconnect(interface):
global retry_int1, retry_int2, retry_int3, retry_int4, retry_int5, retry_int6, retry_int7, retry_int8, retry_int9
rxType = type(interface).__name__
if rxType in ['SerialInterface', 'TCPInterface', 'BLEInterface']:
identifier = interface.__dict__.get('devPath', interface.__dict__.get('hostname', 'BLE'))
logger.critical(f"System: Lost Connection to Device {identifier}")
for i in range(1, 10):
if globals().get(f'interface{i}_enabled'):
if (rxType == 'SerialInterface' and globals().get(f'port{i}') in identifier) or \
(rxType == 'TCPInterface' and globals().get(f'hostname{i}') in identifier) or \
(rxType == 'BLEInterface' and globals().get(f'interface{i}_type') == 'ble'):
globals()[f'retry_int{i}'] = True
break
def exit_handler():
# Close the interface and save the BBS messages
logger.debug(f"System: Closing Autoresponder")
try:
logger.debug(f"System: Closing Interface1")
interface1.close()
if multiple_interface:
for i in range(2, 10):
if globals().get(f'interface{i}_enabled'):
logger.debug(f"System: Closing Interface{i}")
globals()[f'interface{i}'].close()
except Exception as e:
logger.error(f"System: closing: {e}")
if bbs_enabled:
save_bbsdb()
save_bbsdm()
logger.debug(f"System: BBS Messages Saved")
logger.debug(f"System: Exiting")
asyncLoop.stop()
asyncLoop.close()
exit (0)
# Handle disconnection of the interface
logger.warning(f"System: Abrupt Disconnection of Interface detected")
interface.close()
# Telemetry Functions
telemetryData = {}
@@ -905,8 +948,14 @@ def consumeMetadata(packet, rxNode=0):
for key in keys:
positionMetadata[nodeID][key] = position_data.get(key, 0)
# if altitude is over 2000 send a log and message for high-flying nodes and not in highfly_ignoreList
if position_data.get('altitude', 0) > highfly_altitude and highfly_enabled and str(nodeID) not in highfly_ignoreList:
logger.info(f"System: High Altitude {position_data['altitude']}m on Device: {rxNode} NodeID: {nodeID}")
send_message(f"High Altitude {position_data['altitude']}m on Device:{rxNode} Node:{get_name_from_number(nodeID,'short',rxNode)}", highfly_channel, 0, rxNode)
time.sleep(responseDelay)
# Keep the positionMetadata dictionary at 5 records
# Keep the positionMetadata dictionary at a maximum size of 20
if len(positionMetadata) > 20:
# Remove the oldest entry
oldest_nodeID = next(iter(positionMetadata))
@@ -1023,7 +1072,7 @@ async def handleFileWatcher():
# if fileWatchBroadcastCh list contains multiple channels, broadcast to all
if type(file_monitor_broadcastCh) is list:
for ch in file_monitor_broadcastCh:
if antiSpam and ch != publicChannel:
if antiSpam and int(ch) != publicChannel:
send_message(msg, int(ch), 0, 1)
time.sleep(responseDelay)
if multiple_interface:
@@ -1049,21 +1098,26 @@ async def handleFileWatcher():
pass
async def retry_interface(nodeID):
global max_retry_count
global retry_int1, retry_int2, retry_int3, retry_int4, retry_int5, retry_int6, retry_int7, retry_int8, retry_int9
global max_retry_count1, max_retry_count2, max_retry_count3, max_retry_count4, max_retry_count5, max_retry_count6, max_retry_count7, max_retry_count8, max_retry_count9
interface = globals()[f'interface{nodeID}']
retry_int = globals()[f'retry_int{nodeID}']
max_retry_count = globals()[f'max_retry_count{nodeID}']
if dont_retry_disconnect:
logger.critical(f"System: dont_retry_disconnect is set, not retrying interface{nodeID}")
exit_handler()
if interface is not None:
retry_int = True
max_retry_count -= 1
globals()[f'retry_int{nodeID}'] = True
globals()[f'max_retry_count{nodeID}'] -= 1
logger.debug(f"System: Retrying interface{nodeID} {globals()[f'max_retry_count{nodeID}']} attempts left")
try:
interface.close()
logger.debug(f"System: Retrying interface{nodeID} in 15 seconds")
except Exception as e:
logger.error(f"System: closing interface{nodeID}: {e}")
logger.debug(f"System: Retrying interface{nodeID} in 15 seconds")
if max_retry_count == 0:
if globals()[f'max_retry_count{nodeID}'] == 0:
logger.critical(f"System: Max retry count reached for interface{nodeID}")
exit_handler()
@@ -1073,13 +1127,15 @@ async def retry_interface(nodeID):
if retry_int:
interface = None
globals()[f'interface{nodeID}'] = None
logger.debug(f"System: Retrying Interface{nodeID}")
interface_type = globals()[f'interface{nodeID}_type']
if interface_type == 'serial':
logger.debug(f"System: Retrying Interface{nodeID} Serial on port: {globals().get(f'port{nodeID}')}")
globals()[f'interface{nodeID}'] = meshtastic.serial_interface.SerialInterface(globals().get(f'port{nodeID}'))
elif interface_type == 'tcp':
logger.debug(f"System: Retrying Interface{nodeID} TCP on hostname: {globals().get(f'hostname{nodeID}')}")
globals()[f'interface{nodeID}'] = meshtastic.tcp_interface.TCPInterface(globals().get(f'hostname{nodeID}'))
elif interface_type == 'ble':
logger.debug(f"System: Retrying Interface{nodeID} BLE on mac: {globals().get(f'mac{nodeID}')}")
globals()[f'interface{nodeID}'] = meshtastic.ble_interface.BLEInterface(globals().get(f'mac{nodeID}'))
logger.debug(f"System: Interface{nodeID} Opened!")
globals()[f'retry_int{nodeID}'] = False
@@ -1155,7 +1211,7 @@ async def watchdog():
handleMultiPing(0, i)
if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled:
if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled or volcanoAlertBroadcastEnabled:
handleAlertBroadcast(i)
intData = displayNodeTelemetry(0, i)
@@ -1169,3 +1225,24 @@ async def watchdog():
except Exception as e:
logger.error(f"System: retrying interface{i}: {e}")
def exit_handler():
# Close the interface and save the BBS messages
logger.debug(f"System: Closing Autoresponder")
try:
logger.debug(f"System: Closing Interface1")
interface1.close()
if multiple_interface:
for i in range(2, 10):
if globals().get(f'interface{i}_enabled'):
logger.debug(f"System: Closing Interface{i}")
globals()[f'interface{i}'].close()
except Exception as e:
logger.error(f"System: closing: {e}")
if bbs_enabled:
save_bbsdb()
save_bbsdm()
logger.debug(f"System: BBS Messages Saved")
logger.debug(f"System: Exiting")
asyncLoop.stop()
asyncLoop.close()
exit (0)

View File

@@ -7,17 +7,26 @@
import os
import http.server
# Set the desired IP address
server_ip = '127.0.0.1'
# Set the port for the server
PORT = 8420
# set webRoot index.html location
webRoot = "etc/www"
# Generate with: openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes
SSL = False
# Set to True to enable logging sdtout
webServerLogs = False
# Generate with: openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes
SSL = False
# Determine the directory where this script is located.
script_dir = os.path.dirname(os.path.realpath(__file__))
# Go up one level from the modules directory to the project root.
project_root = os.path.abspath(os.path.join(script_dir, ".."))
# Build the absolute path to the webRoot folder; to where index.html is located.
webRoot = os.path.join(project_root, "etc", "www")
if SSL:
import ssl
@@ -31,8 +40,8 @@ class QuietHandler(http.server.SimpleHTTPRequestHandler):
# Change the current working directory to webRoot
os.chdir(webRoot)
# boot up simple HTTP server
httpd = http.server.HTTPServer(('127.0.0.1', PORT), QuietHandler)
# Create the HTTP server instance with the desired IP address
httpd = http.server.HTTPServer((server_ip, PORT), QuietHandler)
if SSL:
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
@@ -42,8 +51,10 @@ if SSL:
print("SSL certificate file not found. Please generate it using the command provided in the comments.")
exit(1)
httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
print(f"Serving reports at https://{server_ip}:{PORT} Press ^C to quit.\n\n")
else:
print(f"Serving reports at http://{server_ip}:{PORT} Press ^C to quit.\n\n")
print(f"Serving reports at http://localhost:{PORT} Press ^C to quit.\n\n")
if not webServerLogs:
print("Server Logs are disabled")
# Serve forever, that is until the user interrupts the process

View File

@@ -25,8 +25,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
command_handler = {
# Command List processes system.trap_list. system.messageTrap() sends any commands to here
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"cmd": lambda: help_message,
"cmd?": lambda: help_message,
"cmd": lambda: handle_cmd(message, message_from_id, deviceID),
"cq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"cqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"cqcqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
@@ -56,6 +55,13 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
return bot_response
def handle_cmd(message, message_from_id, deviceID):
# why CMD? its just a command list. a terminal would normally use "Help"
# I didnt want to invoke the word "help" in Meshtastic due to its possible emergency use
if " " in message and message.split(" ")[1] in trap_list:
return "🤖 just use the commands directly in chat"
return help_message
def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number):
global multiPing
if "?" in message and isDM:
@@ -211,15 +217,15 @@ def onReceive(packet, interface):
if rxType == 'TCPInterface':
rxHost = interface.__dict__.get('hostname', 'unknown')
if hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
elif multiple_interface and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
elif multiple_interface and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
elif multiple_interface and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
elif multiple_interface and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
elif multiple_interface and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
elif multiple_interface and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
elif multiple_interface and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
elif multiple_interface and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
if rxHost and hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
elif multiple_interface and rxHost and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
elif multiple_interface and rxHost and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
elif multiple_interface and rxHost and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
elif multiple_interface and rxHost and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
elif multiple_interface and rxHost and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
elif multiple_interface and rxHost and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
elif multiple_interface and rxHost and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
elif multiple_interface and rxHost and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
if rxType == 'BLEInterface':
if interface1_type == 'ble': rxNode = 1
@@ -304,7 +310,7 @@ def onReceive(packet, interface):
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 the message log
# 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 +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
# respond with DM
@@ -315,7 +321,8 @@ def onReceive(packet, interface):
time.sleep(responseDelay)
# log the message to the message log
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
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-'))
else:
# message is on a channel
if messageTrap(message_string):
@@ -437,10 +444,11 @@ async def main():
await asyncio.sleep(0.01)
try:
if __name__ == "__main__":
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
exit_handler()
pass
except KeyboardInterrupt:
exit_handler()
except SystemExit:
pass
# EOF

View File

@@ -0,0 +1,53 @@
# file name: send-environment-metrics.py
# https://github.com/pdxlocations/Meshtastic-Python-Examples/blob/main/send-environment-metrics.py
from meshtastic.protobuf import portnums_pb2, telemetry_pb2
from meshtastic import BROADCAST_ADDR
import time
# For connection over serial
# import meshtastic.serial_interface
# interface = meshtastic.serial_interface.SerialInterface()
# For connection over TCP
import meshtastic.tcp_interface
interface = meshtastic.tcp_interface.TCPInterface(hostname='127.0.0.1', noProto=False)
# Create a telemetry data object
telemetry_data = telemetry_pb2.Telemetry()
telemetry_data.time = int(time.time())
#telemetry_data.local_stats.upTime = 0
telemetry_data.environment_metrics.temperature = 0
# telemetry_data.environment_metrics.voltage = 0
# telemetry_data.environment_metrics.current = 0
# telemetry_data.environment_metrics.relative_humidity = 0
# telemetry_data.environment_metrics.barometric_pressure = 0
# telemetry_data.environment_metrics.gas_resistance = 0
# telemetry_data.environment_metrics.iaq = 0
# telemetry_data.environment_metrics.distance = 0
# telemetry_data.environment_metrics.lux = 0
# telemetry_data.environment_metrics.white_lux = 0
# telemetry_data.environment_metrics.ir_lux = 0
# telemetry_data.environment_metrics.uv_lux = 0
# telemetry_data.environment_metrics.wind_direction = 0
# telemetry_data.environment_metrics.wind_speed = 0
# telemetry_data.environment_metrics.wind_gust = 0
# telemetry_data.environment_metrics.wind_lull = 0
# telemetry_data.environment_metrics.weight = 0
# Read the uptime
# with open('/proc/uptime', 'r') as uptime:
# telemetry_data.local_stats.upTime = int(float(uptime.readline().split()[0]))
# Read the CPU temperature
with open('/sys/class/thermal/thermal_zone0/temp', 'r') as cpu_temp:
telemetry_data.environment_metrics.temperature = int(cpu_temp.read()) / 1000
interface.sendData(
telemetry_data,
destinationId=BROADCAST_ADDR,
portNum=portnums_pb2.PortNum.TELEMETRY_APP,
wantResponse=False,
)
interface.close()

View File

@@ -24,4 +24,21 @@ else
fi
# print telemetry data rounded to 2 decimal places
printf "Disk:%s RAM:%.2f%% CPU:%.2f%% CPU-T:%.2f°C (%.2f°F)\n" "$free_space" "$ram_usage" "$cpu_usage" "$temp" "$tempf"
printf "Disk:%s RAM:%.2f%% CPU:%.2f%% CPU-T:%.2f°C (%.2f°F)\n" "$free_space" "$ram_usage" "$cpu_usage" "$temp" "$tempf"
# attempt check for updates
if command -v git &> /dev/null
then
if [ -d ../.git ]; then
# check for updates
git fetch --quiet
local_branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$local_branch" != "HEAD" ] && git show-ref --verify --quiet "refs/remotes/origin/$local_branch"; then
local_commit=$(git rev-parse "$local_branch")
remote_commit=$(git rev-parse "origin/$local_branch")
if [ "$local_commit" != "$remote_commit" ]; then
echo "Bot Update Available!"
fi
fi
fi
fi

46
update.sh Normal file
View File

@@ -0,0 +1,46 @@
#!/bin/bash
# MeshBot Update Script
# Usage: bash update.sh or ./update.sh after making it executable with chmod +x update.sh
# Check if the mesh_bot.service or pong_bot.service
if systemctl is-active --quiet mesh_bot.service; then
echo "Stopping mesh_bot.service..."
systemctl stop mesh_bot.service
fi
if systemctl is-active --quiet pong_bot.service; then
echo "Stopping pong_bot.service..."
systemctl stop pong_bot.service
fi
if systemctl is-active --quiet mesh_bot_reporting.service; then
echo "Stopping mesh_bot_reporting.service..."
systemctl stop mesh_bot_reporting.service
fi
if systemctl is-active --quiet mesh_bot_w3.service; then
echo "Stopping mesh_bot_w3.service..."
systemctl stop mesh_bot_w3.service
fi
# Update the local repository
echo "Updating local repository..."
#git fetch --all
#git reset --hard origin/main # Replace 'main' with your branch name if different
git pull origin main --rebase # Fetch and rebase to keep local changes if any
echo "Local repository updated."
# Install or update dependencies
echo "Installing or updating dependencies..."
pip install -r requirements.txt --upgrade
echo "Dependencies installed or updated."
# Restart the services
echo "Restarting services..."
systemctl start mesh_bot.service
systemctl start pong_bot.service
systemctl start mesh_bot_reporting.service
systemctl start mesh_bot_w3.service
echo "Services restarted."
# Print completion message
echo "Update completed successfully?"
exit 0
# End of script