Compare commits

...

160 Commits

Author SHA1 Message Date
SpudGunMan 011bac41f2 lower 2025-10-19 22:32:06 -07:00
SpudGunMan 20467ea886 enhance 2025-10-19 22:30:40 -07:00
SpudGunMan bbfd71f011 "IP-Network" 2025-10-19 22:27:40 -07:00
SpudGunMan e1ff87a197 enhance Ping 2025-10-19 22:26:09 -07:00
SpudGunMan a859f830bb coreFix
Enhance Packet hop and MQTT detection
2025-10-19 22:16:19 -07:00
SpudGunMan d99698e7f3 Update blackjack.py 2025-10-19 20:25:36 -07:00
SpudGunMan 5ecc563e96 newChunker 2025-10-19 20:18:08 -07:00
SpudGunMan eeeb43cacc Update lemonade.py 2025-10-19 20:12:28 -07:00
SpudGunMan 9fdcea56fc Update lemonade.py 2025-10-19 20:10:26 -07:00
SpudGunMan 24a33fe882 Update lemonade.py 2025-10-19 20:08:34 -07:00
SpudGunMan 5710cebf39 Update mmind.py 2025-10-19 20:04:05 -07:00
SpudGunMan b66487863d Update mmind.py 2025-10-19 19:35:41 -07:00
SpudGunMan b3c4d208b7 Update mmind.py 2025-10-19 19:29:34 -07:00
SpudGunMan f41ff2d5f7 Update mmind.py 2025-10-19 19:21:48 -07:00
SpudGunMan 48366bc595 Update mmind.py 2025-10-19 19:15:31 -07:00
SpudGunMan 02dd64382d Update mesh_bot.py 2025-10-19 19:05:23 -07:00
SpudGunMan 731b48ad65 Update mesh_bot.py 2025-10-19 19:04:34 -07:00
SpudGunMan 69a7082669 Update README.md 2025-10-19 18:57:56 -07:00
SpudGunMan fafa7d8a51 Update config.template 2025-10-19 18:57:27 -07:00
Kelly 6e69b5f014 Merge pull request #219 from pdxlocations/allow-port-numbers
Enhance TCP interface initialization to support host:port format
2025-10-19 18:54:54 -07:00
SpudGunMan 03895248cd fixing
sorry if you saw the crashing I had dinner
2025-10-19 18:31:51 -07:00
SpudGunMan a79de8a325 cleanupBadCode 2025-10-19 17:16:53 -07:00
SpudGunMan 740b53f02f Update system.py 2025-10-19 16:25:35 -07:00
SpudGunMan 76e75551c6 Update videopoker.py 2025-10-19 16:15:49 -07:00
SpudGunMan 51752ae896 Update videopoker.py 2025-10-19 16:15:37 -07:00
SpudGunMan d81e773c0c Update blackjack.py 2025-10-19 16:02:24 -07:00
SpudGunMan 1f1ed1ca70 Update blackjack.py 2025-10-19 16:01:04 -07:00
pdxlocations df9f3806a3 Enhance TCP interface initialization to support host:port format 2025-10-19 16:00:45 -07:00
SpudGunMan 081ccd9e2e Update blackjack.py 2025-10-19 15:55:46 -07:00
SpudGunMan d9a7dafe6e Update blackjack.py 2025-10-19 15:54:41 -07:00
SpudGunMan 921225965b Update blackjack.py 2025-10-19 15:52:20 -07:00
SpudGunMan 3659254785 refactor suggestion 2025-10-19 15:42:49 -07:00
SpudGunMan 7c502608f6 remove deps install 2025-10-19 14:51:51 -07:00
SpudGunMan 427c25f80b noHoldsHeld 2025-10-19 14:26:45 -07:00
SpudGunMan c3f15390ea Update system.py 2025-10-19 14:08:21 -07:00
SpudGunMan e1476a44c6 enhance 2025-10-19 13:46:58 -07:00
SpudGunMan 72070fef3e backup data 2025-10-19 13:43:57 -07:00
SpudGunMan b63ea677f6 Update blackjack.py 2025-10-19 13:35:00 -07:00
SpudGunMan f8389500b8 Update mesh_bot.py 2025-10-19 13:28:32 -07:00
SpudGunMan b257625a45 cleanupBlackJack 2025-10-19 13:23:40 -07:00
SpudGunMan a233d8c7b3 Update mesh_bot.py 2025-10-19 12:57:37 -07:00
SpudGunMan 11c9742ebe cleanup 2025-10-19 12:55:16 -07:00
SpudGunMan 5af28c3dc2 Update system.py
kidding
2025-10-19 12:55:08 -07:00
SpudGunMan aebb9e3c20 cleanup 2025-10-19 12:48:19 -07:00
SpudGunMan d5916f4ccc 🐞🧩
thanks meshguy
2025-10-19 12:22:09 -07:00
SpudGunMan 056159a3f3 Update mesh_bot.py 2025-10-19 09:21:25 -07:00
SpudGunMan 2f6049d94b bugfix survey game 2025-10-18 19:20:36 -07:00
SpudGunMan a2d7f664ab Update udp.py 2025-10-18 17:29:21 -07:00
SpudGunMan b26491b646 Update udp.py 2025-10-18 17:26:39 -07:00
SpudGunMan 22e97b0eec Update udp.py 2025-10-18 16:54:26 -07:00
SpudGunMan f540866d08 Update locationdata.py 2025-10-18 15:18:18 -07:00
SpudGunMan c9729c8214 Update locationdata.py 2025-10-18 15:17:34 -07:00
SpudGunMan 49901cbbee Update locationdata.py 2025-10-18 15:14:23 -07:00
SpudGunMan 2aa2b80935 Update locationdata.py 2025-10-18 15:08:40 -07:00
SpudGunMan 95695f4f58 Update locationdata.py 2025-10-18 15:02:33 -07:00
SpudGunMan b641d2b5e8 ok finslly
this looks better
2025-10-18 14:54:45 -07:00
SpudGunMan 51d8faab12 enhance 2025-10-18 14:49:26 -07:00
SpudGunMan 7a1396b99d Update locationdata.py 2025-10-18 14:40:39 -07:00
SpudGunMan 819bbbcaf4 enhance 2025-10-18 14:39:06 -07:00
SpudGunMan 0eeda96670 Update locationdata.py 2025-10-18 14:36:44 -07:00
SpudGunMan 18cca4ffdd Update locationdata.py 2025-10-18 14:35:28 -07:00
SpudGunMan d169fe2dff Update locationdata.py 2025-10-18 14:33:42 -07:00
SpudGunMan 1c732dfe17 Update install.sh 2025-10-18 12:47:17 -07:00
SpudGunMan bdad3927e5 enhance 2025-10-18 10:07:09 -07:00
SpudGunMan 0e0d6416d9 enhance config merge data 2025-10-18 09:38:00 -07:00
SpudGunMan 0da780371a enhance 2025-10-18 09:10:47 -07:00
SpudGunMan 37bf30cbc0 enhance 2025-10-18 09:05:17 -07:00
SpudGunMan 817a8601dd Update system.py 2025-10-18 08:53:30 -07:00
SpudGunMan 47cca409be lab work 2025-10-18 08:52:32 -07:00
SpudGunMan e08a82ec39 Update system.py 2025-10-18 08:42:48 -07:00
SpudGunMan 345541dfb5 Update system.py 2025-10-18 08:41:22 -07:00
SpudGunMan 6e89762f1d bbsCompression
not enabled yet
2025-10-18 08:39:43 -07:00
SpudGunMan 0fb26bc16a Update mesh_bot.py 2025-10-17 19:50:47 -07:00
SpudGunMan f1ad5966af send_raw_bytes 2025-10-17 19:50:41 -07:00
SpudGunMan ac57d4683f Update udp.py 2025-10-17 17:48:24 -07:00
SpudGunMan eab099e5ee channelID 2025-10-17 17:42:07 -07:00
SpudGunMan 685bd3491d Update udp.py 2025-10-17 17:10:44 -07:00
SpudGunMan b8d64f3a9e Update system.py 2025-10-17 13:31:47 -07:00
SpudGunMan 852d491030 Update meshview.ino 2025-10-16 18:57:17 -07:00
SpudGunMan 76565c5546 Update meshview.ino 2025-10-16 18:55:03 -07:00
SpudGunMan af1ec1630e Update udp.py 2025-10-16 16:04:06 -07:00
SpudGunMan 0c2b36a206 refactor handle_messages
@mesb1 give this one a test

https://github.com/SpudGunMan/meshing-around/issues/213
2025-10-16 15:55:12 -07:00
SpudGunMan c0934096f0 Update meshview.ino 2025-10-16 12:07:58 -07:00
SpudGunMan 819bfaba90 Update meshview.ino 2025-10-16 11:52:03 -07:00
SpudGunMan 8041a1296b tinkering
@martinbogo
2025-10-15 20:32:46 -07:00
SpudGunMan 10d93b4fd3 keyFactor 2025-10-15 20:17:44 -07:00
SpudGunMan 19dedef1e6 meshview.ino
I could use help with this I am stuck at the moment
2025-10-15 19:25:21 -07:00
SpudGunMan d4af0c7e8b Update udp.py 2025-10-15 15:58:02 -07:00
SpudGunMan 8730f0fd38 Update udp.py 2025-10-15 15:57:40 -07:00
SpudGunMan 9cda8daf65 Update udp.py 2025-10-15 15:57:24 -07:00
SpudGunMan a9223f1613 Create udp.py 2025-10-15 15:51:30 -07:00
SpudGunMan 04ca4c99b8 Update scheduler.py
sorry for that
2025-10-15 08:24:02 -07:00
SpudGunMan 3072520e63 Merge branch 'main' of https://github.com/SpudGunMan/meshing-around 2025-10-15 08:23:16 -07:00
SpudGunMan bd6603766b Update scheduler.py 2025-10-15 08:23:14 -07:00
Kelly 075a23bd2b LowerBits
https://github.com/SpudGunMan/meshing-around/issues/213
2025-10-14 22:21:21 -07:00
SpudGunMan a8e4f653ed Update radio.py 2025-10-14 21:19:00 -07:00
SpudGunMan 374a44f4a9 Update radio.py 2025-10-14 21:17:36 -07:00
SpudGunMan 3c8d2e646e Update radio.py 2025-10-14 16:32:01 -07:00
SpudGunMan e5df983244 Update mesh_bot.py 2025-10-14 16:23:27 -07:00
SpudGunMan fa5f9250c4 Update llm.py 2025-10-14 16:14:59 -07:00
SpudGunMan 3f7a831690 Update llm.py 2025-10-14 16:12:14 -07:00
SpudGunMan 89aaaddae9 Update llm.py 2025-10-14 16:02:40 -07:00
SpudGunMan e1919616c2 refactoring 2025-10-14 15:55:59 -07:00
SpudGunMan 8b9e637006 Update README.md 2025-10-14 15:08:07 -07:00
SpudGunMan 0df3e32901 Update README.md 2025-10-14 15:07:18 -07:00
SpudGunMan 1c2fa174ea Hey Chirpy
- **Voice/Command Triggers**: The following keywords can be used in messages or via voice (VOX) to trigger bot functions:
  - `joke`: Tells a joke
  - `weather`: Returns local weather forecast
  - `moon`: Returns moonrise/set and phase info
  - `daylight`: Returns sunrise/sunset times
  - `river`: Returns NOAA river flow info
  - `tide`: Returns NOAA tide information
  - `satellite`: Returns satellite pass info
2025-10-14 15:05:39 -07:00
SpudGunMan c97aefcef1 Update radio.py 2025-10-14 14:59:10 -07:00
SpudGunMan dfb94c3993 voxUse 2025-10-14 14:56:45 -07:00
SpudGunMan 7d62f69f12 ... 2025-10-14 14:24:42 -07:00
SpudGunMan cf896767fb Update mesh_bot.py 2025-10-14 13:41:31 -07:00
SpudGunMan 1eb4cf71ed Update mesh_bot.py 2025-10-14 13:40:27 -07:00
SpudGunMan e959124eac voxEnhance 2025-10-14 13:38:26 -07:00
SpudGunMan d787c72812 Update radio.py 2025-10-14 13:32:22 -07:00
SpudGunMan 9f0dd56d43 Update mesh_bot.py 2025-10-14 13:26:37 -07:00
SpudGunMan aa71e6045a Update radio.py
forgot to save a good
2025-10-14 12:45:41 -07:00
SpudGunMan a140ad83cd Update radio.py 2025-10-14 12:34:44 -07:00
SpudGunMan 93c2d731e8 Update radio.py 2025-10-14 12:33:18 -07:00
SpudGunMan d8da553af9 Update radio.py 2025-10-14 12:32:21 -07:00
SpudGunMan 9d9f070908 enhance 2025-10-14 12:24:42 -07:00
SpudGunMan 0f2061af55 chirpy make my lunch 2025-10-14 12:24:29 -07:00
SpudGunMan d8423584d4 Update radio.py 2025-10-14 12:19:13 -07:00
SpudGunMan 843320d268 Update radio.py 2025-10-14 12:18:11 -07:00
SpudGunMan 216128b15a Update radio.py 2025-10-14 12:07:45 -07:00
SpudGunMan f8bc574753 Update radio.py 2025-10-14 11:35:32 -07:00
SpudGunMan 6193c5933f Update radio.py 2025-10-14 11:32:34 -07:00
SpudGunMan b668965bda bufferHandler
tracking https://github.com/SpudGunMan/meshing-around/issues/213
2025-10-14 11:13:40 -07:00
SpudGunMan ae039b5baf Update radio.py 2025-10-14 11:01:24 -07:00
SpudGunMan 824d43f16e Update radio.py 2025-10-14 10:57:39 -07:00
SpudGunMan 2de76e6c5e Update radio.py 2025-10-14 10:56:14 -07:00
SpudGunMan afb02602fd Update radio.py 2025-10-14 10:53:44 -07:00
SpudGunMan 99528c2bcf Update system.py 2025-10-14 10:52:35 -07:00
SpudGunMan b53f5821f3 Update system.py 2025-10-14 10:51:34 -07:00
SpudGunMan 93fc6547b8 Update system.py 2025-10-14 10:46:30 -07:00
SpudGunMan 9a7e321dff Update scheduler.py 2025-10-14 10:25:32 -07:00
SpudGunMan 39257f2d39 Update scheduler.py 2025-10-14 10:25:16 -07:00
SpudGunMan 8c5abecac3 refactor
or custom for module/scheduler.py
2025-10-14 10:09:33 -07:00
SpudGunMan 16dcc96037 consolidate time to wait 2025-10-14 09:38:38 -07:00
SpudGunMan b1d32a7745 refactor MOTD 2025-10-14 09:22:41 -07:00
SpudGunMan 631a2f53ea major refactor to schedule
major thanks to @FJRPiolt
2025-10-14 08:44:59 -07:00
SpudGunMan 32903c97e3 enhance 2025-10-14 08:13:35 -07:00
SpudGunMan 6e61e8122d Update system.py
no its not
2025-10-14 08:07:00 -07:00
SpudGunMan d109803f9d Update system.py 2025-10-14 07:21:05 -07:00
SpudGunMan 09ed4f57cf Update system.py
enhance high fly with block list blocks
2025-10-14 07:18:10 -07:00
SpudGunMan acfb8078a9 Update system.py
to much log
2025-10-14 07:06:46 -07:00
SpudGunMan 84f9693833 Update system.py 2025-10-13 23:50:32 -07:00
SpudGunMan 50fdcf486d Update system.py 2025-10-13 23:21:50 -07:00
SpudGunMan eab5afccc8 Update system.py
helps to hit save
2025-10-13 21:31:10 -07:00
SpudGunMan ea9db47c2d refactor sysinfo local telemetry 2025-10-13 21:29:45 -07:00
SpudGunMan cf3a9c5b43 Update filemon.py 2025-10-13 19:49:43 -07:00
SpudGunMan adedaa092c Update mesh_bot.py
fixLocaStats and sysinfo
2025-10-13 19:49:24 -07:00
SpudGunMan f204237a63 Update mesh_bot.py 2025-10-13 19:27:58 -07:00
SpudGunMan 057a400041 Update mesh_bot.py 2025-10-13 19:26:53 -07:00
SpudGunMan 4cdf68f074 fixLocaStats and sysinfo 2025-10-13 19:24:37 -07:00
SpudGunMan 003a11c557 fixReportingEngine
This data is used by the webReporting engine
2025-10-13 17:57:20 -07:00
SpudGunMan 8d309fa579 Update README.md 2025-10-13 17:42:35 -07:00
SpudGunMan 232f9c24db aaahhhrrg 2025-10-13 17:27:51 -07:00
SpudGunMan 39dccd149b Update mesh_bot.py 2025-10-13 17:26:45 -07:00
SpudGunMan b921c73fa7 Update mesh_bot.py 2025-10-13 17:26:08 -07:00
SpudGunMan f3ec1cbe93 enhance 2025-10-13 17:23:49 -07:00
SpudGunMan a6bcfda0ac enhance 2025-10-13 17:20:56 -07:00
25 changed files with 1721 additions and 750 deletions
+15 -5
View File
@@ -41,7 +41,15 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
### Proximity Alerts
- **Location-Based Alerts**: Get notified when members arrive back at a configured lat/long, perfect for remote locations like campsites.
- **High Flying Alerts**: Get notified when nodes with high altitude are seen on mesh
- **Hey Chirpy**: Voice activate send messages with "hey chirpy"
- **Voice/Command Triggers**: The following keywords can be used via voice (VOX) to trigger bot functions "Hey Chirpy!"
- Say "Hey Chirpy.."
- `joke`: Tells a joke
- `weather`: Returns local weather forecast
- `moon`: Returns moonrise/set and phase info
- `daylight`: Returns sunrise/sunset times
- `river`: Returns NOAA river flow info
- `tide`: Returns NOAA tide information
- `satellite`: Returns satellite pass info
### CheckList / Check In Out
- **Asset Tracking**: Maintain a list of node/asset checkin and checkout. Useful foraccountability of people, assets. Radio-Net, FEMA, Trailhead.
@@ -114,6 +122,7 @@ git clone https://github.com/spudgunman/meshing-around
| `whoami` | Returns details of the node asking, also returned when position exchanged 📍 | ✅ |
| `whois` | Returns details known about node, more data with bbsadmin node | ✅ |
| `echo` | Echo string back, disabled by default | ✅ |
| `bannode` | Admin option to prevent a node from using bot. `bannode list` will load and use the data/bbs_ban_list.txt db | ✅ |
### Radio Propagation & Weather Forecasting
| Command | Description | |
@@ -220,7 +229,7 @@ meshtastic --ble-scan
# config.ini
# type can be serial, tcp, or ble.
# port is the serial port to use; commented out will try to auto-detect
# hostname is the IP address of the device to connect to for TCP type
# hostname is the IP/DNS and port for tcp type default is host:4403
# mac is the MAC address of the device to connect to for BLE type
[interface]
@@ -517,7 +526,7 @@ value = # value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun
interval = # interval to use when time is not set (e.g. every 2 days)
time = # time of day in 24:00 hour format when value is 'day' and interval is not set
```
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.
The basic brodcast message can be setup in condig.ini. For advanced, See the [modules/scheduler.py](modules/scheduler.py) 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
@@ -528,7 +537,7 @@ schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now
```
#### BBS Link
The scheduler also handles the BBS Link Broadcast message, this would be an example of a mesh-admin channel on 8 being used to pass BBS post traffic between two bots as the initiator, one direction pull.
The scheduler also handles the BBS Link Broadcast message, this would be an example of a mesh-admin channel on 8 being used to pass BBS post traffic between two bots as the initiator, one direction pull. The message just needs to have bbslink
```python
# Send bbslink looking for peers every other day at 10:00 using send_message function to channel 8 on device 1
schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", 8, 0, 1))
@@ -586,7 +595,8 @@ I used ideas and snippets from other responder bots and want to call them out!
- **mikecarper**: ideas, and testing. hamtest
- **c.merphy360**: high altitude alerts
- **Iris**: testing and finding 🐞
- **Cisien, bitflip, Woof, propstg, snydermesh, trs2982, FJRPilot, F0X, mesb1, and Hailo1999**: For testing and feature ideas on Discord and GitHub.
- **FJRPiolt**: testing bugs out!!
- **Cisien, bitflip, Woof, propstg, snydermesh, trs2982, F0X, mesb1, and Hailo1999**: For testing and feature ideas on Discord and GitHub.
- **Meshtastic Discord Community**: For tossing out ideas and testing code.
### Tools
+8 -4
View File
@@ -1,7 +1,7 @@
#config.ini
# type can be serial, tcp, or ble
# port is the serial port to use, commented out will try to auto-detect
# hostname is the IP address of the device to connect to for tcp type
# hostname is the IP/DNS and port for tcp type default is host:4403
# mac is the MAC address of the device to connect to for ble type
[interface]
@@ -127,6 +127,7 @@ alert_interface = 1
[sentry]
# detect anyone close to the bot
SentryEnabled = True
reqLocationEnabled = False
emailSentryAlerts = False
# radius in meters to detect someone close to the bot
SentryRadius = 100
@@ -276,7 +277,7 @@ channel = 2
message = "MeshBot says Hello! DM for more info."
# enable overides the above and uses the motd as the message
schedulerMotd = False
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun. or custom for module/scheduler.py
value =
# interval to use when time is not set (e.g. every 2 days)
interval =
@@ -307,6 +308,7 @@ voxLanguage = en-us
voxInputDevice = default
voxOnTrapList = True
voxTrapList = chirpy
voxEnableCmd = True
[fileMon]
@@ -376,8 +378,10 @@ tictactoe = True
# enable or disable the quiz game module questions are in data/quiz.json
quiz = False
# enable or disable the survey game module questions are in data/survey/survey.json
# enable or disable the survey game module questions are in data/survey/*_survey.json
survey = False
# this is the default survey to use when command givcen, from data/survey/example_survey.json
defaultSurvey = example
# Whether to record user ID in responses
surveyRecordID=True
# Whether to record location on start of survey
@@ -392,7 +396,7 @@ splitDelay = 2.5
MESSAGE_CHUNK_SIZE = 160
# Request Acknowledgement of message OTA
wantAck = False
# Max limit buffer for radio testing
# Max limit buffer for radio testing in bytes
maxBuffer = 200
#Enable Extra logging of Hop count data
enableHopLogs = False
+224
View File
@@ -0,0 +1,224 @@
// Example to receive and decode Meshtastic UDP packets
// Make sure to install the meashtastic library and generate the .pb.h and .pb.c files from the Meshtastic .proto definitions
// https://github.com/meshtastic/protobufs/tree/master/meshtastic
// Example to receive and decode Meshtastic UDP packets
#include <WiFi.h>
#include <WiFiUdp.h>
// #include <AESLib.h> // or another AES library
#include "pb_decode.h"
#include "meshtastic/mesh.pb.h" // MeshPacket, Position, etc.
#include "meshtastic/portnums.pb.h" // Port numbers enum
#include "meshtastic/telemetry.pb.h" // Telemetry message
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* default_key = "1PG7OiApB1nwvP+rz05pAQ=="; // Your network key here
uint8_t aes_key[16]; // Buffer for decoded key
const char* MCAST_GRP = "224.0.0.69";
const uint16_t MCAST_PORT = 4403;
unsigned long udpPacketCount = 0;
WiFiUDP udp;
IPAddress multicastIP;
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("Scanning for WiFi networks...");
int n = WiFi.scanNetworks();
if (n == 0) {
Serial.println("No networks found.");
} else {
Serial.print(n);
Serial.println(" networks found:");
for (int i = 0; i < n; ++i) {
Serial.print(i + 1);
Serial.print(": ");
Serial.print(WiFi.SSID(i));
Serial.print(" (RSSI ");
Serial.print(WiFi.RSSI(i));
Serial.print(")");
Serial.println((WiFi.encryptionType(i) == WIFI_AUTH_OPEN) ? " [OPEN]" : " [SECURED]");
delay(10);
}
}
Serial.println("Connecting to WiFi...");
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
unsigned long startAttemptTime = millis();
const unsigned long wifiTimeout = 20000;
while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < wifiTimeout) {
delay(500);
Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi connected.");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
multicastIP.fromString(MCAST_GRP);
if (udp.beginMulticast(multicastIP, MCAST_PORT)) {
Serial.println("UDP multicast listener started.");
} else {
Serial.println("Failed to start UDP multicast listener.");
}
} else {
Serial.print("\nFailed to connect to WiFi. SSID: ");
Serial.println(ssid);
Serial.println("Check SSID, range, and password.");
}
}
void printHex(const uint8_t* buf, size_t len) {
for (size_t i = 0; i < len; i++) {
Serial.printf("%02X ", buf[i]);
}
Serial.println();
}
void printAscii(const uint8_t* buf, size_t len) {
for (size_t i = 0; i < len; i++) {
char c = static_cast<char>(buf[i]);
Serial.print(isprint(c) ? c : '.');
}
Serial.println();
}
void decodeKey() {
// Convert base64 key to raw bytes
// You may need to add a base64 decoding function/library
// Example: decode_base64(default_key, aes_key, sizeof(aes_key));
}
void decryptPayload(const uint8_t* encrypted, size_t len, uint8_t* decrypted) {
// Use AESLib or similar to decrypt
// Example: aes128_dec_single(decrypted, encrypted, aes_key);
}
void loop() {
int packetSize = udp.parsePacket();
if (!packetSize) {
delay(50);
return;
}
udpPacketCount++;
Serial.print("UDP packets seen: ");
Serial.println(udpPacketCount);
uint8_t buffer[512];
int len = udp.read(buffer, sizeof(buffer));
if (len <= 0) {
Serial.println("Failed to read UDP packet.");
delay(50);
return;
}
// Always show raw payload
Serial.print("Raw UDP payload (hex): ");
printHex(buffer, len);
Serial.print("Raw UDP payload (ASCII): ");
printAscii(buffer, len);
// Decode outer MeshPacket
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_zero;
pb_istream_t stream = pb_istream_from_buffer(buffer, len);
if (!pb_decode(&stream, meshtastic_MeshPacket_fields, &pkt)) {
Serial.println("Failed to decode meshtastic_MeshPacket.");
delay(50);
return;
}
// Basic MeshPacket fields
Serial.print("id: "); Serial.println(pkt.id);
Serial.print("rx_time: "); Serial.println(pkt.rx_time);
Serial.print("rx_snr: "); Serial.println(pkt.rx_snr, 2);
Serial.print("rx_rssi: "); Serial.println(pkt.rx_rssi);
Serial.print("hop_limit: "); Serial.println(pkt.hop_limit);
Serial.print("priority: "); Serial.println(pkt.priority);
Serial.print("from: "); Serial.println(pkt.from);
Serial.print("to: "); Serial.println(pkt.to);
Serial.print("channel: "); Serial.println(pkt.channel);
// Only proceed if we have a decoded Data variant
if (pkt.which_payload_variant != meshtastic_MeshPacket_decoded_tag) {
Serial.println("Packet does not contain decoded Data (maybe encrypted or other variant).");
delay(50);
return;
}
const meshtastic_Data& data = pkt.decoded;
Serial.print("Portnum: "); Serial.println(data.portnum);
Serial.print("Payload size: "); Serial.println(data.payload.size);
if (data.payload.size == 0) {
Serial.println("No inner payload bytes.");
delay(50);
return;
}
// Decode by portnum
switch (data.portnum) {
case meshtastic_PortNum_TEXT_MESSAGE_APP: {
// Current schemas do not use a separate user.pb.h. Text payload is plain bytes.
Serial.print("Decoded text message: ");
printAscii(data.payload.bytes, data.payload.size);
break;
}
case meshtastic_PortNum_POSITION_APP: {
meshtastic_Position pos = meshtastic_Position_init_zero;
pb_istream_t ps = pb_istream_from_buffer(data.payload.bytes, data.payload.size);
if (pb_decode(&ps, meshtastic_Position_fields, &pos)) {
Serial.print("Position lat="); Serial.print(pos.latitude_i / 1e7, 7);
Serial.print(" lon="); Serial.print(pos.longitude_i / 1e7, 7);
Serial.print(" alt="); Serial.println(pos.altitude);
} else {
Serial.println("Failed to decode Position payload.");
}
break;
}
case meshtastic_PortNum_TELEMETRY_APP: {
meshtastic_Telemetry tel = meshtastic_Telemetry_init_zero;
pb_istream_t ts = pb_istream_from_buffer(data.payload.bytes, data.payload.size);
if (pb_decode(&ts, meshtastic_Telemetry_fields, &tel)) {
// Print a few common fields if present
if (tel.which_variant == meshtastic_Telemetry_device_metrics_tag) {
const meshtastic_DeviceMetrics& m = tel.variant.device_metrics;
Serial.print("Telemetry battery_level="); Serial.print(m.battery_level);
Serial.print(" voltage="); Serial.print(m.voltage);
Serial.print(" air_util_tx="); Serial.println(m.air_util_tx);
} else {
Serial.println("Telemetry decoded, different variant. Raw bytes:");
printHex(data.payload.bytes, data.payload.size);
}
} else {
Serial.println("Failed to decode Telemetry payload.");
}
break;
}
default: {
Serial.print("Unhandled portnum "); Serial.print((int)data.portnum);
Serial.println(", showing payload as hex:");
printHex(data.payload.bytes, data.payload.size);
break;
}
}
delay(50);
}
+1 -1
View File
@@ -287,7 +287,7 @@ 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 cp %s/etc/%s.service /etc/systemd/system/%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
+291 -290
View File
@@ -81,7 +81,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
"readrss": lambda: get_rss_feed(message),
"riverflow": lambda: handle_riverFlow(message, message_from_id, deviceID),
"rlist": lambda: handle_repeaterQuery(message_from_id, deviceID, channel_number),
"satpass": lambda: handle_satpass(message_from_id, deviceID, channel_number, message),
"satpass": lambda: handle_satpass(message_from_id, deviceID, message),
"setemail": lambda: handle_email(message_from_id, message),
"setsms": lambda: handle_sms( message_from_id, message),
"sitrep": lambda: handle_lheard(message, message_from_id, deviceID, isDM),
@@ -144,16 +144,17 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
cmds = sorted(cmds, key=lambda k: k['index'])
# Check if user is already playing a game
playing, game = isPlayingGame(message_from_id)
playing, game = isPlayingGame(message_from_id)[0], isPlayingGame(message_from_id)[1]
# Block restricted commands if not DM, or if already playing a game
if (cmds[0]['cmd'] in restrictedCommands and not isDM) or (cmds[0]['cmd'] in restrictedCommands and playing):
# Block restricted commands if not DM
if (cmds[0]['cmd'] in restrictedCommands and not isDM) or (cmds[0]['cmd'] in restrictedCommands and playing) or playing:
logger.debug(f"System: Bot restricted Command:{cmds[0]['cmd']} From: {get_name_from_number(message_from_id)} isDM:{isDM} playing:{playing}")
if playing:
bot_response = f"🤖You are already playing {game}, finish that first."
else:
bot_response = restrictedResponse
else:
logger.debug(f"System: Bot detected Commands:{cmds} From: {get_name_from_number(message_from_id)}")
logger.debug(f"System: Bot detected Commands:{cmds} From: {get_name_from_number(message_from_id)} isDM:{isDM} playing:{playing}")
# run the first command after sorting
bot_response = command_handler[cmds[0]['cmd']]()
# append the command to the cmdHistory list for lheard and history
@@ -168,6 +169,59 @@ def handle_cmd(message, message_from_id, deviceID):
if " " in message and message.split(" ")[1] in trap_list:
return "🤖 just use the commands directly in chat"
return help_message
def isPlayingGame(message_from_id):
global gameTrackers
trackers = gameTrackers.copy()
playingGame = False
game = "None"
trackers = [tracker for tracker in trackers if tracker is not None]
for tracker, game_name, handle_game_func in trackers:
for i in range(len(tracker)-1, -1, -1): # iterate backwards for safe removal
id_key = 'userID' if game_name == "DopeWars" else 'nodeID'
id_key = 'id' if game_name == "Survey" else id_key
if tracker[i].get(id_key) == message_from_id:
last_played_key = 'last_played' if 'last_played' in tracker[i] else 'time'
if tracker[i].get(last_played_key, 0) > (time.time() - GAMEDELAY):
playingGame = True
game = game_name
break
if playingGame:
break
return playingGame, game
def checkPlayingGame(message_from_id, message_string, rxNode, channel_number):
global gameTrackers
trackers = gameTrackers.copy()
playingGame = False
game = "None"
trackers = [tracker for tracker in trackers if tracker is not None]
for tracker, game_name, handle_game_func in trackers:
playingGame, game = check_and_play_game(tracker, message_from_id, message_string, rxNode, channel_number, game_name, handle_game_func)
if playingGame:
break
return playingGame
def check_and_play_game(tracker, message_from_id, message_string, rxNode, channel_number, game_name, handle_game_func):
global llm_enabled
for i in range(len(tracker)):
# Use 'userID' for DopeWars, 'nodeID' for others (including Survey)
id_key = 'userID' if game_name == "DopeWars" else 'nodeID'
if tracker[i].get(id_key) == message_from_id:
last_played_key = 'last_played' if 'last_played' in tracker[i] else 'time'
if tracker[i].get(last_played_key) > (time.time() - GAMEDELAY):
if llm_enabled:
logger.debug(f"System: LLM Disabled for {message_from_id} for duration of {game_name}")
send_message(handle_game_func(message_string, message_from_id, rxNode), channel_number, message_from_id, rxNode)
return True, game_name
return False, "None"
def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number):
global multiPing
@@ -240,6 +294,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
if pingCount > 1:
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number, 'startCount': pingCount})
logger.info(f"System: Starting auto-ping of type {type} for {pingCount} pings to {get_name_from_number(message_from_id, 'short', deviceID)}")
if type == "🎙TEST":
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
else:
@@ -271,7 +326,6 @@ def handle_emergency(message_from_id, deviceID, message):
nodeInfo = f"{get_name_from_number(message_from_id, 'short', deviceID)} detected by {get_name_from_number(myNodeNum, 'short', deviceID)} lastGPS {nodeLocation[0]}, {nodeLocation[1]}"
msg = f"🔔🚨Intercepted Possible Emergency Assistance needed for: {nodeInfo}"
# alert the emergency_responder_alert_channel
time.sleep(responseDelay)
send_message(msg, emergency_responder_alert_channel, 0, emergency_responder_alert_interface)
logger.warning(f"System: {message_from_id} Emergency Assistance Requested in {message}")
# send the message out via email/sms
@@ -282,40 +336,39 @@ def handle_emergency(message_from_id, deviceID, message):
def handle_motd(message, message_from_id, isDM):
global MOTD
isAdmin = False
msg = ""
# check if the message_from_id is in the bbs_admin_list
if bbs_admin_list != ['']:
for admin in bbs_admin_list:
if str(message_from_id) == admin:
isAdmin = True
break
else:
isAdmin = True
# admin help via DM
if "?" in message and isDM and isAdmin:
msg = ''
isAdmin = isNodeAdmin(message_from_id)
if "?" in message:
msg = "Message of the day, set with 'motd $ HelloWorld!'"
elif "?" in message and isDM and not isAdmin:
# non-admin help via DM
msg = "Message of the day"
elif "$" in message and isAdmin:
motd = message.split("$")[1]
MOTD = motd.rstrip()
logger.debug(f"System: {message_from_id} changed MOTD: {MOTD}")
logger.debug(f"System: {message_from_id} temporarly changed MOTD: {MOTD}")
msg = "MOTD changed to: " + MOTD
else:
msg = "MOTD: " + MOTD
return msg
def handle_echo(message, message_from_id, deviceID, isDM, channel_number):
echoBinary = False
if echoBinary:
try:
#send_raw_bytes echo the data to the channel with synch word:
port_num = 256
synch_word = b"echo:"
message = message.split("echo ")[1]
raw_bytes = synch_word + message.encode('utf-8')
send_raw_bytes(message_from_id, raw_bytes, nodeInt=deviceID, channel=channel_number, portnum=port_num)
except Exception as e:
logger.error(f"System: Echo Exception {e}")
return f"Sent binary echo message to {message_from_id} to {port_num} on channel {channel_number} device {deviceID}"
if "?" in message.lower():
return "command returns your message back to you. Example:echo Hello World"
elif "echo " in message.lower():
parts = message.lower().split("echo ", 1)
if len(parts) > 1 and parts[1].strip() != "":
echo_msg = parts[1]
if channel_number != echoChannel:
if channel_number != echoChannel and not isDM:
echo_msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + echo_msg
return echo_msg
else:
@@ -389,8 +442,8 @@ def handle_howtall(message, message_from_id, deviceID, isDM):
lat = location[0]
lon = location[1]
if lat == latitudeValue and lon == longitudeValue:
logger.debug(f"System: HowTall: No GPS location for {message_from_id}")
return "No GPS location available"
# add guessing tot he msg
msg += "Guessing:"
if use_metric:
measure = "meters"
else:
@@ -405,7 +458,7 @@ def handle_howtall(message, message_from_id, deviceID, isDM):
return f"Please provide a shadow length in {measure} example: howtall 5.5"
# get data
msg = measureHeight(lat, lon, shadow_length)
msg += measureHeight(lat, lon, shadow_length)
# if data has NO_ALERTS return help
if NO_ALERTS in msg:
@@ -436,8 +489,12 @@ llmRunCounter = 0
llmTotalRuntime = []
llmLocationTable = [{'nodeID': 1234567890, 'location': 'No Location'},]
def handle_satpass(message_from_id, deviceID, channel_number, message):
location = get_node_location(message_from_id, deviceID)
def handle_satpass(message_from_id, deviceID, message='', vox=False):
if vox:
location = (latitudeValue, longitudeValue)
message = 'satpass'
else:
location = get_node_location(message_from_id, deviceID)
passes = ''
satList = satListConfig
message = message.lower()
@@ -498,11 +555,9 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
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:
@@ -536,7 +591,6 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
else:
# send via channel
send_message(msg, channel_number, 0, deviceID)
time.sleep(responseDelay)
start = time.time()
@@ -549,8 +603,10 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
llmTotalRuntime.append(end - start)
return response
def handleDopeWars(message, nodeID, rxNode):
global dwPlayerTracker, dwHighScore
from modules.settings import dwPlayerTracker
global dwHighScore
# Find player in tracker
player = next((p for p in dwPlayerTracker if p.get('userID') == nodeID), None)
@@ -602,7 +658,8 @@ def handle_gTnW(chess = False):
return response[selected_index]
def handleLemonade(message, nodeID, deviceID):
global lemonadeTracker, lemonadeCups, lemonadeLemons, lemonadeSugar, lemonadeWeeks, lemonadeScore, lemon_starting_cash, lemon_total_weeks
from modules.settings import lemonadeTracker
global lemonadeCups, lemonadeLemons, lemonadeSugar, lemonadeWeeks, lemonadeScore, lemon_starting_cash, lemon_total_weeks
msg = ""
def create_player(nodeID):
@@ -649,7 +706,7 @@ def handleLemonade(message, nodeID, deviceID):
return msg
def handleBlackJack(message, nodeID, deviceID):
global jackTracker
from modules.settings import jackTracker
msg = ""
# Find player in tracker
@@ -664,9 +721,24 @@ def handleBlackJack(message, nodeID, deviceID):
# Create new player if not found
if not player and nodeID != 0:
jackTracker.append({'nodeID': nodeID, 'cmd': 'new', 'last_played': time.time()})
msg += "Welcome to 🃏BlackJack!🃏\n"
logger.debug(f"System: BlackJack: New Player {nodeID}")
# create new player
jackTracker.append({
'nodeID': nodeID,
'bet': 0,
'cash': 100, # starting cash
'gameStats': {'p_win': 0, 'd_win': 0, 'draw': 0},
'p_cards': [],
'd_cards': [],
'p_hand': [],
'd_hand': [],
'next_card': [],
'last_played': time.time(),
'cmd': 'new'
})
msg += f"Welcome to 🃏BlackJack🃏!\n (H)it,(S)tand,(F)orfit,(D)ouble,(R)esend,(L)eave table"
# Show high score if available
highScore = 0
highScore = loadHSJack()
if highScore and highScore.get('nodeID', 0) != 0:
nodeName = get_name_from_number(highScore['nodeID'])
@@ -679,12 +751,18 @@ def handleBlackJack(message, nodeID, deviceID):
if player:
player['last_played'] = time.time()
# get player's last command from tracker if not new player
last_cmd = ""
for i in range(len(jackTracker)):
if jackTracker[i]['nodeID'] == nodeID:
last_cmd = jackTracker[i]['cmd']
# Play BlackJack
msg += playBlackJack(nodeID=nodeID, message=message)
msg += playBlackJack(nodeID=nodeID, message=message, last_cmd=last_cmd)
return msg
def handleVideoPoker(message, nodeID, deviceID):
global vpTracker
from modules.settings import vpTracker
msg = ""
# Find player in tracker
@@ -719,7 +797,7 @@ def handleVideoPoker(message, nodeID, deviceID):
return msg
def handleMmind(message, nodeID, deviceID):
global mindTracker
from modules.settings import mindTracker
msg = ''
if "end" in message.lower() or message.lower().startswith("e"):
@@ -763,11 +841,30 @@ def handleMmind(message, nodeID, deviceID):
return msg
def handleGolf(message, nodeID, deviceID):
global golfTracker
from modules.settings import golfTracker
msg = ''
# get player's last command from tracker if not new player
last_cmd = ""
# Ensure player exists in tracker
if not any(entry['nodeID'] == nodeID for entry in golfTracker):
logger.debug("System: GolfSim: New Player: " + str(nodeID))
golfTracker.append({
'nodeID': nodeID,
'last_played': time.time(),
'cmd': 'new',
'hole': 1,
'distance_remaining': 0,
'hole_shots': 0,
'hole_strokes': 0,
'hole_to_par': 0,
'total_strokes': 0,
'total_to_par': 0,
'par': 0,
'hazard': ''
})
# get player's last command from tracker
for i in range(len(golfTracker)):
if golfTracker[i]['nodeID'] == nodeID:
last_cmd = golfTracker[i]['cmd']
@@ -782,18 +879,17 @@ def handleGolf(message, nodeID, deviceID):
logger.debug(f"System: {nodeID} PlayingGame golfsim last_cmd: {last_cmd}")
if last_cmd == "" and nodeID != 0:
if last_cmd == "new" and nodeID != 0:
# create new player
logger.debug("System: GolfSim: New Player: " + str(nodeID))
golfTracker.append({'nodeID': nodeID, 'last_played': time.time(), 'cmd': 'new', 'hole': 1, 'distance_remaining': 0, 'hole_shots': 0, 'hole_strokes': 0, 'hole_to_par': 0, 'total_strokes': 0, 'total_to_par': 0, 'par': 0, 'hazard': ''})
msg = f"Welcome to 🏌️GolfSim⛳️\n"
msg += f"Clubs: (D)river, (L)ow Iron, (M)id Iron, (H)igh Iron, (G)ap Wedge, Lob (W)edge\n"
msg += f"Clubs: (D)river, (L)ow Iron, (M)id Iron, (H)igh Iron, (G)ap Wedge, Lob (W)edge (C)addie\n"
msg += playGolf(nodeID=nodeID, message=message)
msg += playGolf(nodeID=nodeID, message=message, last_cmd=last_cmd)
return msg
def handleHangman(message, nodeID, deviceID):
global hangmanTracker
from modules.settings import hangmanTracker
index = 0
msg = ''
for i in range(len(hangmanTracker)):
@@ -819,7 +915,7 @@ def handleHangman(message, nodeID, deviceID):
return msg
def handleHamtest(message, nodeID, deviceID):
global hamtestTracker
from modules.settings import hamtestTracker
index = 0
msg = ''
response = message.split(' ')
@@ -852,7 +948,7 @@ def handleHamtest(message, nodeID, deviceID):
return msg
def handleTicTacToe(message, nodeID, deviceID):
global tictactoeTracker
from modules.settings import tictactoeTracker
index = 0
msg = ''
@@ -933,7 +1029,6 @@ def quizHandler(message, nodeID, deviceID):
if isinstance(msg, dict) and str(nodeID) in bbs_admin_list and 'message' in msg:
for player_id in quizGamePlayer.players:
send_message(msg['message'], 0, player_id, deviceID)
time.sleep(responseDelay)
msg = f"Message sent to {len(quizGamePlayer.players)} players"
return msg
@@ -941,7 +1036,8 @@ def quizHandler(message, nodeID, deviceID):
return "🧠Please provide an answer or command, or send q: ?"
def surveyHandler(message, nodeID, deviceID):
global surveyTracker
from modules.settings import surveyTracker
user_id = nodeID
location = get_node_location(nodeID, deviceID)
msg = ''
# Normalize and parse the command
@@ -983,8 +1079,13 @@ def surveyHandler(message, nodeID, deviceID):
return msg
def handle_riverFlow(message, message_from_id, deviceID):
location = get_node_location(message_from_id, deviceID)
def handle_riverFlow(message, message_from_id, deviceID, vox=False):
# River Flow from NOAA or Open-Meteo
if vox:
location = (latitudeValue, longitudeValue)
message = "riverflow"
else:
location = get_node_location(message_from_id, deviceID)
msg_lower = message.lower()
if "riverflow " in msg_lower:
user_input = msg_lower.split("riverflow ", 1)[1].strip()
@@ -1010,7 +1111,14 @@ def handle_mwx(message_from_id, deviceID, cmd):
return NO_ALERTS
return get_nws_marine(zone=myCoastalZone, days=coastalForecastDays)
def handle_wxc(message_from_id, deviceID, cmd):
def handle_wxc(message_from_id, deviceID, cmd, vox=False):
# Weather from NOAA or Open-Meteo
if vox:
# return a default message if vox is enabled
if use_meteo_wxApi:
return get_wx_meteo(latitudeValue, longitudeValue)
else:
return get_NOAAweather(latitudeValue, longitudeValue)
location = get_node_location(message_from_id, deviceID)
if use_meteo_wxApi and not "wxc" in cmd and not use_metric:
#logger.debug("System: Bot Returning Open-Meteo API for weather imperial")
@@ -1100,49 +1208,51 @@ def handle_messages(message, deviceID, channel_number, msg_history, publicChanne
if "?" in message and isDM:
return message.split("?")[0].title() + " command returns the last " + str(storeFlimit) + " messages sent on a channel."
else:
response = ""
header = f"📨Messages:\n"
# Calculate safe byte limit (account for header and some overhead)
header_bytes = len(header.encode('utf-8'))
available_bytes = max_bytes - header_bytes
# Filter messages for this device/channel
filtered_msgs = [
msgH for msgH in msg_history
if msgH[4] == deviceID and (msgH[2] == channel_number or msgH[2] == publicChannel)
]
# Reverse the message history to show most recent first
for msgH in reversed(msg_history):
# number of messages to return +1 for the header line
if len(response.split("\n")) >= storeFlimit + 1:
break
# if the message is for this deviceID and channel or publicChannel
if msgH[4] == deviceID:
if msgH[2] == channel_number or msgH[2] == publicChannel:
new_line = f"\n{msgH[0]}: {msgH[1]}"
# Check if adding this line would exceed byte limit
test_response = response + new_line
if len(test_response.encode('utf-8')) > available_bytes:
# Try to add truncated version of the message
msg_text = msgH[1]
truncated = False
while len(msg_text) > 0 and len((response + f"\n{msgH[0]}: {msg_text}").encode('utf-8')) > available_bytes:
# Remove one character at a time from the end
msg_text = msg_text[:-1]
truncated = True
if len(msg_text) > 10: # Only add if we have at least 10 chars left
response += f"\n{msgH[0]}: {msg_text}" + ("..." if truncated else "")
break # Stop adding more messages
else:
response += new_line
# Choose order and slice
# Oldest first, take first N
filtered_msgs = filtered_msgs[-storeFlimit:][::-1]
if reverseSF:
# segassem reverse the order of the messages
response_lines = response.split("\n")
response_lines.reverse()
response = "\n".join(response_lines)
# reverse that
filtered_msgs = filtered_msgs[::-1]
response = ""
header = f"📨Msgs:\n"
for msgH in filtered_msgs:
new_line = f"\n{msgH[0]}: {msgH[1]}"
test_response = response + new_line
if len(test_response.encode('utf-8')) > maxBuffer:
# Truncate message if needed
msg_text = msgH[1]
truncated = False
trunc_marker = "..."
while len(msg_text) > 0 and len((response + f"\n{msgH[0]}: {msg_text}{trunc_marker}").encode('utf-8')) > maxBuffer:
msg_text = msg_text[:-1]
truncated = True
if len(msg_text) > 10:
if truncated:
response += f"\n{msgH[0]}: {msg_text}{trunc_marker}"
else:
response += f"\n{msgH[0]}: {msg_text}"
break
continue
else:
response += new_line
if len(response) > 0:
return header + response
else:
return "No 📭messages in history"
def handle_sun(message_from_id, deviceID, channel_number):
def handle_sun(message_from_id, deviceID, channel_number, vox=False):
if vox:
# return a default message if vox is enabled
return get_sun(str(latitudeValue), str(longitudeValue))
location = get_node_location(message_from_id, deviceID, channel_number)
return get_sun(str(location[0]), str(location[1]))
@@ -1153,7 +1263,7 @@ def sysinfo(message, message_from_id, deviceID):
if enable_runShellCmd and file_monitor_enabled:
# 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")
shellData = call_external_script('', "script/sysEnv.sh")
# check if the script returned data
if shellData == "" or shellData == None:
# no data returned from the script
@@ -1250,11 +1360,15 @@ def handle_repeaterQuery(message_from_id, deviceID, channel_number):
else:
return "Repeater lookup not enabled"
def handle_tide(message_from_id, deviceID, channel_number):
def handle_tide(message_from_id, deviceID, channel_number, vox=False):
if vox:
return get_NOAAtide(str(latitudeValue), str(longitudeValue))
location = get_node_location(message_from_id, deviceID, channel_number)
return get_NOAAtide(str(location[0]), str(location[1]))
def handle_moon(message_from_id, deviceID, channel_number):
def handle_moon(message_from_id, deviceID, channel_number, vox=False):
if vox:
return get_moon(str(latitudeValue), str(longitudeValue))
location = get_node_location(message_from_id, deviceID, channel_number)
return get_moon(str(location[0]), str(location[1]))
@@ -1322,74 +1436,6 @@ def handle_whois(message, deviceID, channel_number, message_from_id):
msg += f"Loc: {where_am_i(str(location[0]), str(location[1]))}"
return msg
def check_and_play_game(tracker, message_from_id, message_string, rxNode, channel_number, game_name, handle_game_func):
global llm_enabled
for i in range(len(tracker)):
# Use 'userID'
id_key = 'userID' if game_name == "DopeWars" else 'nodeID' # DopeWars uses 'userID'
id_key = 'id' if game_name == "Survey" else id_key # Survey uses 'id'
if tracker[i].get(id_key) == message_from_id:
last_played_key = 'last_played' if 'last_played' in tracker[i] else 'time'
if tracker[i].get(last_played_key) > (time.time() - GAMEDELAY):
if llm_enabled:
logger.debug(f"System: LLM Disabled for {message_from_id} for duration of {game_name}")
send_message(handle_game_func(message_string, message_from_id, rxNode), channel_number, message_from_id, rxNode)
return True, game_name
return False, "None"
gameTrackers = [
(dwPlayerTracker, "DopeWars", handleDopeWars) if 'dwPlayerTracker' in globals() else None,
(lemonadeTracker, "LemonadeStand", handleLemonade) if 'lemonadeTracker' in globals() else None,
(vpTracker, "VideoPoker", handleVideoPoker) if 'vpTracker' in globals() else None,
(jackTracker, "BlackJack", handleBlackJack) if 'jackTracker' in globals() else None,
(mindTracker, "MasterMind", handleMmind) if 'mindTracker' in globals() else None,
(golfTracker, "GolfSim", handleGolf) if 'golfTracker' in globals() else None,
(hangmanTracker, "Hangman", handleHangman) if 'hangmanTracker' in globals() else None,
(hamtestTracker, "HamTest", handleHamtest) if 'hamtestTracker' in globals() else None,
(tictactoeTracker, "TicTacToe", handleTicTacToe) if 'tictactoeTracker' in globals() else None,
(surveyTracker, "Survey", surveyHandler) if 'surveyTracker' in globals() else None,
#quiz does not use a tracker (quizGamePlayer) always active
]
def isPlayingGame(message_from_id):
global gameTrackers
trackers = gameTrackers.copy()
playingGame = False
game = "None"
trackers = [tracker for tracker in trackers if tracker is not None]
for tracker, game_name, handle_game_func in trackers:
for i in range(len(tracker)-1, -1, -1): # iterate backwards for safe removal
id_key = 'userID' if game_name == "DopeWars" else 'nodeID'
id_key = 'id' if game_name == "Survey" else id_key
if tracker[i].get(id_key) == message_from_id:
last_played_key = 'last_played' if 'last_played' in tracker[i] else 'time'
if tracker[i].get(last_played_key, 0) > (time.time() - GAMEDELAY):
playingGame = True
game = game_name
break
if playingGame:
break
return playingGame, game
def checkPlayingGame(message_from_id, message_string, rxNode, channel_number):
global gameTrackers
trackers = gameTrackers.copy()
playingGame = False
game = "None"
trackers = [tracker for tracker in trackers if tracker is not None]
for tracker, game_name, handle_game_func in trackers:
playingGame, game = check_and_play_game(tracker, message_from_id, message_string, rxNode, channel_number, game_name, handle_game_func)
if playingGame:
break
return playingGame
def onReceive(packet, interface):
global seenNodes, msg_history, cmdHistory
# Priocess the incoming packet, handles the responses to the packet with auto_response()
@@ -1403,7 +1449,13 @@ def onReceive(packet, interface):
pkiStatus = (False, 'ABC')
replyIDset = False
emojiSeen = False
simulator_flag = False
isDM = False
channel_number = 0
hop_away = 0
hop_start = 0
hop_count = 0
channel_name = "unknown"
playingGame = False
if DEBUGpacket:
@@ -1450,7 +1502,23 @@ def onReceive(packet, interface):
# check if the packet has a channel flag use it
if packet.get('channel'):
channel_number = packet.get('channel', 0)
channel_number = packet.get('channel')
channel_name = "unknown"
# get channel hashes for the interface
device = next((d for d in channel_list if d["interface_id"] == rxNode), None)
if device:
# Find the channel name whose hash matches channel_number
for chan_name, info in device['channels'].items():
if info['hash'] == channel_number:
print(f"Matched channel hash {info['hash']} to channel name {chan_name}")
channel_name = chan_name
break
# check if the packet has a simulator flag
simulator_flag = packet.get('decoded', {}).get('simulator', False)
if isinstance(simulator_flag, dict):
# assume Software Simulator
simulator_flag = True
# set the message_from_id
message_from_id = packet['from']
@@ -1464,8 +1532,6 @@ def onReceive(packet, interface):
msg = bbs_check_dm(message_from_id)
if msg:
# wait a responseDelay to avoid message collision from lora-ack.
time.sleep(responseDelay)
logger.info(f"System: BBS DM Delivery: {msg[1]} For: {get_name_from_number(message_from_id, 'long', rxNode)}")
message = "Mail: " + msg[1] + " From: " + get_name_from_number(msg[2], 'long', rxNode)
bbs_delete_dm(msg[0], msg[1])
@@ -1477,6 +1543,13 @@ def onReceive(packet, interface):
message_bytes = packet['decoded']['payload']
message_string = message_bytes.decode('utf-8')
via_mqtt = packet['decoded'].get('viaMqtt', False)
transport_mechanism = (
packet.get('transport_mechanism')
or packet.get('transportMechanism')
or (packet.get('decoded', {}).get('transport_mechanism'))
or (packet.get('decoded', {}).get('transportMechanism'))
or 'unknown'
)
rx_time = packet['decoded'].get('rxTime', time.time())
# check if the packet is from us
@@ -1503,40 +1576,36 @@ def onReceive(packet, interface):
# check if the packet has a hop count flag use it
if packet.get('hopsAway'):
hop_away = packet.get('hopsAway', 0)
if packet.get('hopStart'):
hop_start = packet.get('hopStart', 0)
if packet.get('hopLimit'):
hop_limit = packet.get('hopLimit', 0)
# calculate hop count
hop = ""
if hop_limit > 0 and hop_start >= hop_limit:
hop_count = hop_away + (hop_start - hop_limit)
elif hop_limit > 0 and hop_start < hop_limit:
hop_count = hop_away + (hop_limit - hop_start)
else:
# if the packet does not have a hop count try other methods
if packet.get('hopLimit'):
hop_limit = packet.get('hopLimit', 0)
else:
hop_limit = 0
if packet.get('hopStart'):
hop_start = packet.get('hopStart', 0)
else:
hop_start = 0
if enableHopLogs:
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start}")
hop_count = hop_away
if hop_away == 0 and hop_limit == 0 and hop_start == 0:
hop = "Last Hop"
hop_count = 0
if hop_start == hop_limit:
hop = "Direct"
hop_count = 0
elif hop_start == 0 and hop_limit > 0 or via_mqtt:
hop = "MQTT"
hop_count = 0
else:
# set hop to Direct if the message was sent directly otherwise set the hop count
if hop_away > 0:
hop_count = hop_away
else:
hop_count = hop_start - hop_limit
#print (f"calculated hop count: {hop_start} - {hop_limit} = {hop_count}")
hop = f"{hop_count} hops"
if hop_start == hop_limit and "lora" in str(transport_mechanism).lower():
hop = "Direct"
if ((hop_start == 0 and hop_limit >= 0) or via_mqtt or ("mqtt" in str(transport_mechanism).lower())):
hop = "MQTT"
if "unknown" in str(transport_mechanism).lower() and (snr == 0 and rssi == 0):
hop = "IP-Network"
if enableHopLogs:
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start} calculated_hop_count:{hop_count} final_hop_value:{hop} via_mqtt:{via_mqtt} transport_mechanism:{transport_mechanism}")
# check with stringSafeChecker if the message is safe
if stringSafeCheck(message_string) is False:
@@ -1566,7 +1635,6 @@ def onReceive(packet, interface):
if games_enabled:
logger.warning(f"Device:{rxNode} Ignoring Request to Play Game: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)} with hop count: {hop}")
send_message(f"Your hop count exceeds safe playable distance at {hop_count} hops", channel_number, message_from_id, rxNode)
time.sleep(responseDelay)
else:
playingGame = False
else:
@@ -1577,7 +1645,6 @@ def onReceive(packet, interface):
# respond with LLM
llm = handle_llm(message_from_id, channel_number, rxNode, message_string, publicChannel)
send_message(llm, channel_number, message_from_id, rxNode)
time.sleep(responseDelay)
else:
# respond with welcome message on DM
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
@@ -1586,7 +1653,6 @@ def onReceive(packet, interface):
if not any(node['nodeID'] == message_from_id and node['welcome'] == True for node in seenNodes):
# send welcome message
send_message(welcome_message, channel_number, message_from_id, rxNode)
time.sleep(responseDelay)
# mark the node as welcomed
for node in seenNodes:
if node['nodeID'] == message_from_id:
@@ -1598,9 +1664,7 @@ def onReceive(packet, interface):
else:
# respond with help message on DM
send_message(help_message, channel_number, message_from_id, rxNode)
time.sleep(responseDelay)
# log the message to the message log
if log_messages_to_file:
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | DM | " + message_string.replace('\n', '-nl-'))
@@ -1685,9 +1749,7 @@ def onReceive(packet, interface):
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, channel_number)
@@ -1750,7 +1812,7 @@ async def start_rx():
if wikipedia_enabled:
if use_kiwix_server:
logger.debug(f"System: Wikipedia search Enabled using Kiwix server at {kiwix_server_address}")
logger.debug(f"System: Wikipedia search Enabled using Kiwix server at {kiwix_url}")
else:
logger.debug("System: Wikipedia search Enabled")
@@ -1761,13 +1823,13 @@ async def start_rx():
logger.debug(f"System: MOTD Enabled using {MOTD} scheduler:{schedulerMotd}")
if sentry_enabled:
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel}")
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel} requestLOC:{reqLocationEnabled}")
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: S&F(messages command) Enabled using limit: {storeFlimit}")
logger.debug(f"System: S&F(messages command) Enabled using limit: {storeFlimit} and reverse queue:{reverseSF}")
if enableEcho:
logger.debug("System: Echo command Enabled")
@@ -1829,95 +1891,34 @@ async def start_rx():
logger.warning("System: SMTP Email Alerting Enabled")
if scheduler_enabled:
# basic scheduler
if schedulerMotd:
schedulerMessage = MOTD
if schedulerValue != '':
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))
logger.debug(f"System: Starting the scheduler to send '{schedulerMessage}' every {schedulerValue} at {schedulerTime} on Device:{schedulerInterface} Channel:{schedulerChannel}")
else:
logger.warning("System: No schedule.Value set edit the .py file to do more. See examples in the code.")
# 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 Enabled Reminder"))
# example scheduler message
logger.debug(f"System: Starting the scheduler to send '{schedulerMessage}' every Monday at noon on Device:{schedulerInterface} Channel:{schedulerChannel}")
# 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))
# Send a Welcome Notice for group on the 15th and 25th of the month at 12:00 using send_message function to channel 2 on device 1
#schedule.every().day.at("12:00").do(lambda: send_message("Welcome to the group", 2, 0, 1)).day(15, 25)
# Send a joke every 6 hours using tell_joke function to channel 2 on device 1
#schedule.every(6).hours.do(lambda: send_message(tell_joke(), 2, 0, 1))
# Send a joke every 2 minutes using tell_joke function to channel 2 on device 1
#schedule.every(2).minutes.do(lambda: send_message(tell_joke(), 2, 0, 1))
# Send the Welcome Message every other day at 08:00 using send_message function to channel 2 on device 1
#schedule.every(2).days.at("08:00").do(lambda: send_message(welcome_message, 2, 0, 1))
# Send the MOTD every day at 13:00 using send_message function to channel 2 on device 1
#schedule.every().day.at("13:00").do(lambda: send_message(MOTD, 2, 0, 1))
# 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))
# show schedual details
await BroadcastScheduler()
# setup the scheduler
from modules.scheduler import setup_scheduler
await setup_scheduler(
schedulerMotd, MOTD, schedulerMessage, schedulerChannel, schedulerInterface,
schedulerValue, schedulerTime, schedulerInterval, logger, BroadcastScheduler
)
# here we go loopty loo
while True:
await asyncio.sleep(0.5)
pass
# Initialize game trackers
gameTrackers = [
(dwPlayerTracker, "DopeWars", handleDopeWars) if 'dwPlayerTracker' in globals() else None,
(lemonadeTracker, "LemonadeStand", handleLemonade) if 'lemonadeTracker' in globals() else None,
(vpTracker, "VideoPoker", handleVideoPoker) if 'vpTracker' in globals() else None,
(jackTracker, "BlackJack", handleBlackJack) if 'jackTracker' in globals() else None,
(mindTracker, "MasterMind", handleMmind) if 'mindTracker' in globals() else None,
(golfTracker, "GolfSim", handleGolf) if 'golfTracker' in globals() else None,
(hangmanTracker, "Hangman", handleHangman) if 'hangmanTracker' in globals() else None,
(hamtestTracker, "HamTest", handleHamtest) if 'hamtestTracker' in globals() else None,
(tictactoeTracker, "TicTacToe", handleTicTacToe) if 'tictactoeTracker' in globals() else None,
(surveyTracker, "Survey", surveyHandler) if 'surveyTracker' in globals() else None,
#quiz does not use a tracker (quizGamePlayer) always active
]
# Hello World
async def main():
tasks = []
+40 -1
View File
@@ -5,12 +5,19 @@ import pickle # pip install pickle
from modules.log import *
import time
useSynchCompression = False
if useSynchCompression:
import zlib
from modules.system import send_raw_bytes
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp", "bbsinfo", "bbslink", "bbsack")
# global message list, later we will use a pickle on disk
bbs_messages = []
bbs_dm = []
def load_bbsdb():
global bbs_messages
# load the bbs messages from the database file
@@ -201,6 +208,32 @@ def bbs_delete_dm(toNode, message):
return "System: cleared mail for" + str(toNode)
return "System: No DM found for node " + str(toNode)
def compress_data(data_to_compress):
# Prepare message as bytes
compressed = zlib.compress(data_to_compress.encode('utf-8'))
return compressed
def decompress_data(data_bytes):
try:
decompressed = zlib.decompress(data_bytes)
msg = decompressed.decode('utf-8')
return msg
except Exception as e:
logger.warning(f"Error decompressing data: {e}")
return False
def bbs_receive_compressed(data_bytes, fromNode, RxNode):
try:
decompressed = zlib.decompress(data_bytes)
msg = decompressed.decode('utf-8')
bbs_sync_posts(msg, fromNode, RxNode)
return msg
except Exception as e:
logger.error(f"Error decompressing BBS message: {e}")
return None
def bbs_sync_posts(input, peerNode, RxNode):
messageID = 0
@@ -245,7 +278,13 @@ def bbs_sync_posts(input, peerNode, RxNode):
if messageID % 5 == 0:
time.sleep(10 + responseDelay)
logger.debug(f"System: Sending bbslink message {messageID} of {len(bbs_messages)} to peer " + str(peerNode))
return f"bbslink {messageID} ${bbs_messages[messageID][1]} #{bbs_messages[messageID][2]} @{fromNodeHex}"
msg = f"bbslink {messageID} ${bbs_messages[messageID][1]} #{bbs_messages[messageID][2]} @{fromNodeHex}"
if useSynchCompression:
compressed = compress_data(msg)
send_raw_bytes(peerNode, compressed)
logger.debug("System: Sent compressed bbslink message to peer " + str(peerNode))
else:
return msg
else:
logger.debug("System: bbslink sync complete with peer " + str(peerNode))
+9 -3
View File
@@ -72,7 +72,6 @@ async def watch_file():
def call_external_script(message, script="script/runShell.sh"):
# Call an external script with the message as an argument this is a example only
try:
# Debugging: Print the current working directory and resolved script path
current_working_directory = os.getcwd()
script_path = os.path.join(current_working_directory, script)
@@ -82,8 +81,15 @@ def call_external_script(message, script="script/runShell.sh"):
if not os.path.exists(script_path):
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().encode('utf-8').decode('utf-8')
# Use subprocess.run for better resource management
result = subprocess.run(
["bash", script_path, message],
capture_output=True,
text=True,
timeout=10
)
output = result.stdout.strip()
return output
except Exception as e:
logger.warning(f"FileMon: Error calling external script: {e}")
+56 -40
View File
@@ -7,8 +7,7 @@ import time
import pickle
jack_starting_cash = 100 # Replace 100 with your desired starting cash value
jackTracker= [{'nodeID': 0, 'cmd': 'new', 'cash': jack_starting_cash,\
'bet': 0, 'gameStats': {'p_win': 0, 'd_win': 0, 'draw': 0}, 'p_cards':[], 'd_cards':[], 'p_hand':[], 'd_hand':[], 'next_card':[],'last_played': time.time()}]
from modules.settings import jackTracker
SUITS = ("♥️", "♦️", "♠️", "♣️")
RANKS = (
@@ -114,22 +113,35 @@ class jackChips:
self.total -= self.bet
self.winnings -= 1
def success_rate(card, obj_h):
""" Calculate Success rate of 'HIT' new cards """
msg = ""
rate = 0
diff = 21 - obj_h.value
if diff != 0:
rate = (VALUES[card[0][1]] / diff) * 100
def success_rate(next_card, player_hand):
# Estimate the chance of a successful 'HIT' (not busting) in blackjack.
if rate < 100:
msg += f"If Hit, chance {int(rate)}% failure, {100-int(rate)}% success."
else:
l_rate = int(rate - (rate - 99)) # Round to 99
if card[0][1] == "A":
l_rate -= 99
msg += f"If Hit, chance {100-l_rate}% failure, and {l_rate}% success"
return msg
# If player already has 21 or more, hitting will always bust
if player_hand.value >= 21:
return "\n🧠 What do you think?"
# Calculate how much more the player can add without busting
max_safe = 21 - player_hand.value
safe_cards = 0
total_cards = 0
for rank in VALUES:
# 4 cards of each rank in a standard deck
count = 4
card_value = VALUES[rank]
# Ace can be 1 or 11, but here we treat it as 1 if 11 would bust
if rank == "A":
card_value = 1 if player_hand.value + 11 > 21 else 11
# Count as safe if it won't bust the player
if card_value <= max_safe:
safe_cards += count
total_cards += count
# Calculate probability
success_chance = int((safe_cards / total_cards) * 100)
fail_chance = 100 - success_chance
return f"\n🧠Hit: {fail_chance}% 👎, {success_chance}% 👍"
def hits(obj_de):
new_card = [obj_de.deal_cards()[0][0]]
@@ -147,12 +159,12 @@ def display_hand(hand):
def show_some(player_cards, dealer_cards, obj_h):
msg = f"Player[{obj_h.value}] {display_hand(player_cards)} "
msg += f"Dealer[{VALUES[dealer_cards[1][1]]}] {dealer_cards[1][1]}{dealer_cards[1][0]} "
msg += f"\nDealer[{VALUES[dealer_cards[1][1]]}] {dealer_cards[1][1]}{dealer_cards[1][0]} "
return msg
def show_all(player_cards, dealer_cards, obj_h, obj_d):
msg = f"Player[{obj_h.value}] {display_hand(player_cards)} "
msg += f"Dealer[{obj_d.value}] {display_hand(dealer_cards)}"
msg += f"\nDealer[{obj_d.value}] {display_hand(dealer_cards)}"
return msg
def player_bust(obj_h, obj_c):
@@ -229,7 +241,7 @@ def loadHSJack():
pickle.dump(highScore, file)
return 0
def playBlackJack(nodeID, message):
def playBlackJack(nodeID, message, last_cmd=None):
# Initalize the Game
msg, last_cmd = '', None
blackJack = False
@@ -267,10 +279,12 @@ def playBlackJack(nodeID, message):
if last_cmd is None:
# create new player if not in tracker
logger.debug(f"System: BlackJack: New Player {nodeID}")
jackTracker.append({'nodeID': nodeID, 'cmd': 'new', 'last_played': time.time(), 'cash': jack_starting_cash,\
'bet': 0, 'gameStats': {'p_win': p_win, 'd_win': d_win, 'draw': draw}, 'p_cards':p_cards, 'd_cards':d_cards, 'p_hand':p_hand.cards, 'd_hand':d_hand.cards, 'next_card':next_card})
return f"Welcome to ♠️♥️BlackJack♣️♦️ you have {p_chips.total} chips. Whats your bet?"
if nodeID != 0:
#logger.debug(f"System: BlackJack: New Player {nodeID}")
jackTracker.append({'nodeID': nodeID, 'cmd': 'new', 'last_played': time.time(), 'cash': jack_starting_cash,\
'bet': 0, 'gameStats': {'p_win': p_win, 'd_win': d_win, 'draw': draw}, 'p_cards':p_cards, 'd_cards':d_cards, 'p_hand':p_hand.cards, 'd_hand':d_hand.cards, 'next_card':next_card})
return f"You have {p_chips.total} chips. Whats your bet?"
return "Error: Player not found."
if getLastCmdJack(nodeID) == "new":
# Place Bet
@@ -283,24 +297,26 @@ def playBlackJack(nodeID, message):
#resend the hand
msg += show_some(p_cards, d_cards, p_hand)
return msg
elif "blackjack" in message.lower():
return f"\nTo place a bet, enter the amount you wish to wager."
else:
try:
bet_money = int(message)
except ValueError:
return "Invalid Bet, please enter a valid number."
return f"\nInvalid Bet, please enter a valid number."
if bet_money <= p_chips.total and bet_money >= 1:
p_chips.bet = bet_money
else:
return f"Invalid Bet, the maximum bet you can place is {p_chips.total} and the minimum bet is 1."
return f"\nInvalid Bet, the maximum bet you can place is {p_chips.total} and the minimum bet is 1."
except ValueError:
return f"Invalid Bet, the maximum bet, {p_chips.total}"
return f"\nInvalid Bet, the maximum bet, {p_chips.total}"
# Show the cards
msg += show_some(p_cards, d_cards, p_hand)
# check for blackjack 21 and only two cards
if p_hand.value == 21 and len(p_hand.cards) == 2:
msg += "Player 🎰 BLAAAACKJACKKKK 💰"
msg += f"\n🎰 BLAAAACKJACKKKK 💰"
p_chips.total += round(p_chips.bet * 1.5)
setLastCmdJack(nodeID, "dealerTurn")
blackJack = True
@@ -317,7 +333,7 @@ def playBlackJack(nodeID, message):
if getLastCmdJack(nodeID) == "betPlaced":
setLastCmdJack(nodeID, "playing")
msg += "(H)it,(S)tand,(F)orfit,(D)ouble,(R)esend,(L)eave table"
msg += f"\n(H)it,(S)tand,(F)orfit,(D)ouble,(R)esend,(L)eave table"
# save the game state
for i in range(len(jackTracker)):
@@ -367,7 +383,7 @@ def playBlackJack(nodeID, message):
# Check if player bust
if player_bust(p_hand, p_chips):
d_win += 1
msg += "💥PlayerBUST💥"
msg += f"\n💥PlayerBUST💥"
setLastCmdJack(nodeID, "dealerTurn")
if getLastCmdJack(nodeID) == "playing":
@@ -419,7 +435,7 @@ def playBlackJack(nodeID, message):
d_hand.add_cards(d_card)
if dealer_bust(d_hand, p_hand, p_chips):
p_win += 1
msg += "💰DealerBUST💥"
msg += f"\n💰DealerBUST💥"
break
# Show all cards
msg += show_all(p_hand.cards, d_hand.cards, p_hand, d_hand)
@@ -427,15 +443,15 @@ def playBlackJack(nodeID, message):
# Check who wins
if push(p_hand, d_hand):
draw += 1
msg += "👌PUSH"
msg += f"\n👌PUSH"
elif player_wins(p_hand, d_hand, p_chips):
p_win += 1
msg += "🎉PLAYER WINS🎰"
msg += f"\n🎉PLAYER WINS🎰"
elif dealer_wins(p_hand, d_hand, p_chips):
d_win += 1
msg += "👎DEALER WINS"
msg += f"\n👎DEALER WINS"
else:
msg += "👎DEALER WINS"
msg += f"\n👎DEALER WINS"
# Display the Game Stats
msg += gameStats(str(p_win), str(d_win), str(draw))
@@ -443,20 +459,20 @@ def playBlackJack(nodeID, message):
# Display the chips left
if p_chips.total < 1:
if p_chips.total > 0:
msg += "🪙Keep the change you filthy animal!"
msg += f"\n🪙Keep the change you filthy animal!"
else:
msg += "💸NO MORE CHIPS!🏧💳"
msg += f"\n💸NO MORE CHIPS!🏧💳"
p_chips.total = jack_starting_cash
else:
# check high score
highScore = loadHSJack()
if highScore != 0 and p_chips.total > highScore['highScore']:
msg += f"💰HighScore💰{p_chips.total} "
msg += f"\n💰HighScore💰{p_chips.total} "
saveHSJack(nodeID, p_chips.total)
else:
msg += f"💰You have {p_chips.total} chips "
msg += f"\n💰You have {p_chips.total} chips "
msg += " Bet or Leave?"
msg += f"\nBet or Leave?"
# Reset the game
setLastCmdJack(nodeID, "new")
+1 -1
View File
@@ -14,7 +14,7 @@ dwInventoryDb = [{'userID': 1234567890, 'inventory': 0, 'priceList': [], 'amount
dwCashDb = [{'userID': 1234567890, 'cash': starting_cash},]
dwGameDayDb = [{'userID': 1234567890, 'day': 0},]
dwLocationDb = [{'userID': 1234567890, 'location': 'USA', 'loc_choice': 0},]
dwPlayerTracker = [{'userID': 1234567890, 'last_played': time.time(), 'cmd': 'start'},]
from modules.settings import dwPlayerTracker
# high score is saved in a pickle file
dwHighScore = {}
+12 -11
View File
@@ -26,7 +26,7 @@ par4_5_range = par4_range + par5_range
# Player setup
playingHole = False
golfTracker = [{'nodeID': 0, 'last_played': time.time(), 'cmd': '', 'hole': 0, 'distance_remaining': 0, 'hole_shots': 0, 'hole_strokes': 0, 'hole_to_par': 0, 'total_strokes': 0, 'total_to_par': 0, 'par': 0, 'hazard': ''}]
from modules.settings import golfTracker
# Club functions
def hit_driver():
@@ -122,9 +122,8 @@ def getHighScoreGolf(nodeID, strokes, par):
return 0
# Main game loop
def playGolf(nodeID, message, finishedHole=False):
def playGolf(nodeID, message, finishedHole=False, last_cmd=''):
msg = ''
global golfTracker
# Course setup
par3_count = 0
par4_count = 0
@@ -150,8 +149,8 @@ def playGolf(nodeID, message, finishedHole=False):
for i in range(len(golfTracker)):
if golfTracker[i]['nodeID'] == nodeID:
golfTracker[i]['last_played'] = time.time()
if last_cmd == "" or last_cmd == "new":
if last_cmd == "new":
# Start a new hole
if hole <= 9:
# Set up hole count restrictions on par
@@ -198,17 +197,19 @@ def playGolf(nodeID, message, finishedHole=False):
# Set initial parameters before starting a hole
distance_remaining = hole_length
hole_shots = 0
last_cmd = 'stroking'
# save player's current game state
for i in range(len(golfTracker)):
if golfTracker[i]['nodeID'] == nodeID:
golfTracker[i]['cmd'] = last_cmd
golfTracker[i]['hole'] = hole
golfTracker[i]['distance_remaining'] = distance_remaining
golfTracker[i]['cmd'] = 'stroking'
golfTracker[i]['par'] = par
golfTracker[i]['total_strokes'] = total_strokes
golfTracker[i]['total_to_par'] = total_to_par
golfTracker[i]['hazard'] = hazard
golfTracker[i]['hole'] = hole
golfTracker[i]['last_played'] = time.time()
golfTracker[i]['hole_shots'] = hole_shots
@@ -325,8 +326,8 @@ def playGolf(nodeID, message, finishedHole=False):
else:
last_cmd = 'stroking'
else:
msg += "\nYou have " + str(distance_remaining) + "yd. ⛳️"
msg += "\nClub?[D, L, M, H, G, W]🏌️"
msg += f"\nYou have " + str(distance_remaining) + "yd. ⛳️"
msg += f"\nClub?[D, L, M, H, G, W]🏌️"
# save player's current game state, keep stroking
@@ -370,7 +371,7 @@ def playGolf(nodeID, message, finishedHole=False):
if hole not in [1, 10]:
# Show player total scoring info for the round, except hole 1 and 10
msg += "\nYou've hit a total of " + str(total_strokes) + " strokes today, for"
msg += f"\nYou've hit a total of " + str(total_strokes) + " strokes today, for"
msg += getScorecardGolf(total_to_par)
# Move to next hole
@@ -408,7 +409,7 @@ def playGolf(nodeID, message, finishedHole=False):
logger.debug("System: GolfSim: Player " + str(nodeID) + " has finished their round.")
else:
# Show player the next hole
msg += playGolf(nodeID, 'new', True)
msg += "\n🏌️[D, L, M, H, G, W, End]🏌️"
msg += playGolf(nodeID, '', True, last_cmd='new')
msg += f"\n🏌️[D, L, M, H, G, W, End]🏌️"
return msg
+2 -2
View File
@@ -168,10 +168,10 @@ def sendWithEmoji(message):
i += 1
return ' '.join(words)
def tell_joke(nodeID=0):
def tell_joke(nodeID=0, vox=False):
dadjoke = Dadjoke()
try:
if dad_jokes_emojiJokes:
if dad_jokes_emojiJokes or vox:
renderedLaugh = sendWithEmoji(dadjoke.joke)
else:
renderedLaugh = dadjoke.joke
+21 -21
View File
@@ -23,6 +23,7 @@ lemonadeLemons = [{'nodeID': 0, 'cost': 4.00, 'count': 8, 'min': 2.00, 'unit': 0
lemonadeSugar = [{'nodeID': 0, 'cost': 3.00, 'count': 15, 'min': 1.50, 'unit': 0.00}]
lemonadeWeeks = [{'nodeID': 0, 'current': 1, 'total': lemon_total_weeks, 'sales': 99, 'potential': 0, 'unit': 0.00, 'price': 0.00, 'total_sales': 0}]
lemonadeScore = [{'nodeID': 0, 'value': 0.00, 'total': 0.00}]
from modules.settings import lemonadeTracker
def get_sales_amount(potential, unit, price):
"""Gets the sales amount.
@@ -258,7 +259,7 @@ def playLemonstand(nodeID, message, celsius=False, newgame=False):
buffer += ". " + \
formatted + temperature.units + " " + \
forecastd[list(forecastd)[temperature.forecast]][2] + \
" " + glyph
" " + glyph + f"\n"
# Calculate the potential sales as a percentage of the maximum value
# (lower temperature = fewer sales, severe weather = fewer sales)
@@ -287,23 +288,23 @@ def playLemonstand(nodeID, message, celsius=False, newgame=False):
# Calculate the unit cost and display the estimated sales from the forecast potential
unit = max(0.01, min(cups.unit + lemons.unit + sugar.unit, 4.0)) # limit the unit cost between $0.01 and $4.00
buffer += " SupplyCost" + locale.currency(round(unit, 2), grouping=True) + " a cup."
buffer += " Sales Potential:" + str(potential) + " cups."
buffer += f"\nSupplyCost" + locale.currency(round(unit, 2), grouping=True) + " a cup."
buffer += f"\nSales Potential:" + str(potential) + " cups."
# Display the current inventory
buffer += " Inventory:"
buffer += f"\nInventory:"
buffer += "🥤:" + str(inventory.cups)
buffer += "🍋:" + str(inventory.lemons)
buffer += "🍚:" + str(inventory.sugar)
# Display the updated item prices
buffer += f"\nPrices: "
buffer += "🥤:" + locale.currency(round(cups.cost, 2), grouping=True) + " 📦 of " + str(cups.count) + "."
buffer += " 🍋:" + locale.currency(round(lemons.cost, 2), grouping=True) + " 🧺 of " + str(lemons.count) + "."
buffer += " 🍚:" + locale.currency(round(sugar.cost, 2), grouping=True) + " bag for " + str(sugar.count) + "🥤."
buffer += f"\nPrices:\n"
buffer += f"\n🥤:" + locale.currency(round(cups.cost, 2), grouping=True) + " 📦 of " + str(cups.count) + "."
buffer += f"\n🍋:" + locale.currency(round(lemons.cost, 2), grouping=True) + " 🧺 of " + str(lemons.count) + "."
buffer += f"\n🍚:" + locale.currency(round(sugar.cost, 2), grouping=True) + " bag for " + str(sugar.count) + "🥤."
# Display the current cash
gainloss = inventory.cash - inventory.start
buffer += " 💵:" + locale.currency(round(inventory.cash, 2), grouping=True)
buffer += f"\n💵:" + locale.currency(round(inventory.cash, 2), grouping=True)
# if the player is in the red
@@ -314,7 +315,7 @@ def playLemonstand(nodeID, message, celsius=False, newgame=False):
else:
buffer += "📊P&L📈" + pnl
buffer += f"\n🥤 to buy? Have {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
buffer += f"\n🥤 to buy?\nHave {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
return buffer
@@ -338,7 +339,7 @@ def playLemonstand(nodeID, message, celsius=False, newgame=False):
except Exception as e:
return "invalid input, enter the number of 🥤 to purchase or (N)one"
msg += f"\n 🍋 to buy? Have {inventory.lemons}🥤 of 🍋 Cost {locale.currency(lemons.cost, grouping=True)} a 🧺 for {str(lemons.count)}🥤"
msg += f"\n 🍋 to buy?\nHave {inventory.lemons}🥤 of 🍋 Cost {locale.currency(lemons.cost, grouping=True)} a 🧺 for {str(lemons.count)}🥤"
# set the last command to lemons in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
@@ -368,7 +369,7 @@ def playLemonstand(nodeID, message, celsius=False, newgame=False):
newlemons = -1
return "⛔️invalid input, enter the number of 🍋 to purchase"
msg += f"\n 🍚 to buy? You have {inventory.sugar}🥤 of 🍚, Cost {locale.currency(sugar.cost, grouping=True)} a bag for {str(sugar.count)}🥤"
msg += f"\n 🍚 to buy?\nYou have {inventory.sugar}🥤 of 🍚, Cost {locale.currency(sugar.cost, grouping=True)} a bag for {str(sugar.count)}🥤"
# set the last command to sugar in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
@@ -414,7 +415,7 @@ def playLemonstand(nodeID, message, celsius=False, newgame=False):
lemonadeTracker[i]['cmd'] = "sales"
if "g" in message.lower():
lemonadeTracker[i]['cmd'] = "cups"
msg = f"#of🥤 to buy? Have {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
msg = f"#of🥤\nto buy? Have {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
return msg
else:
lemonsLastCmd = "sales"
@@ -467,7 +468,7 @@ def playLemonstand(nodeID, message, celsius=False, newgame=False):
msg += " N.Profit:" + locale.currency(net, grouping=True)
# Display the updated inventory levels
msg += "\nRemaining"
msg += f"\nRemaining"
msg += " 🥤:" + str(inventory.cups)
msg += " 🍋:" + str(inventory.lemons)
msg += " 🍚:" + str(inventory.sugar)
@@ -484,7 +485,7 @@ def playLemonstand(nodeID, message, celsius=False, newgame=False):
pad_week = len(str(weeks.total))
pad_sale = len(str(weeks.sales))
total = 0
msg += "\nWeekly📊"
msg += f"\nWeekly📊"
for i in range(len(weeks.summary)):
msg += "#" + str(weeks.current).rjust(pad_week) + ". " + str(weeks.summary[i]['sales']).rjust(pad_sale) + \
" sold x " + locale.currency(weeks.summary[i]['price'], grouping=True) + "ea. "
@@ -524,7 +525,7 @@ def playLemonstand(nodeID, message, celsius=False, newgame=False):
if (inventory.sugar <= 0):
msg += " You ran out of sugar.🍚"
else:
msg += "\nCongratulations 🍋🍋 your sales were perfect!🎉"
msg += f"\nCongratulations 🍋🍋 your sales were perfect!🎉"
# Increment the score counters
score.value = score.value + minnet
@@ -535,27 +536,26 @@ def playLemonstand(nodeID, message, celsius=False, newgame=False):
if (weeks.current == weeks.total):
# end of the game
success = round((score.value / score.total) * 100)
msg += "\nYou've made " + locale.currency(score.value, grouping=True) + " out of a possible " + \
msg += f"\nYou've made " + locale.currency(score.value, grouping=True) + " out of a possible " + \
locale.currency(score.total, grouping=True) + " for a score of " + str(success) + "% "
msg += "You've sold " + str(weeks.total_sales) + " total 🥤🍋"
msg += f"\nYou've sold " + str(weeks.total_sales) + " total 🥤🍋"
# check for high score
high_score = getHighScoreLemon()
if (inventory.cash > int(high_score['cash'])):
msg += "\nCongratulations! You've set a new high score!🎉💰🍋"
msg += f"\nCongratulations! You've set a new high score!🎉💰🍋"
high_score['cash'] = inventory.cash
high_score['success'] = success
high_score['userID'] = nodeID
with open('data/lemonstand.pkl', 'wb') as file:
pickle.dump(high_score, file)
endGame(nodeID)
else:
# keep playing
weeks.current = weeks.current + 1
msg += f"Play another week🥤? or (E)nd Game"
msg += f"\nPlay another week🥤? or (E)nd Game"
# set the last command to new in the inventory db
for i in range(len(lemonadeTracker)):
if lemonadeTracker[i]['nodeID'] == nodeID:
+73 -105
View File
@@ -5,9 +5,7 @@ import random
import time
import pickle
from modules.log import *
mindTracker = [{'nodeID': 0, 'last_played': time.time(), 'cmd': '', 'secret_code': '', 'diff': 'n', 'turns': 1}]
from modules.settings import mindTracker
def chooseDifficultyMMind(message):
usrInput = message.lower()
msg = ''
@@ -62,98 +60,63 @@ def makeCodeMMind(diff):
return secret_code
#get guess from user
def getGuessMMind(diff, guess):
msg = ''
if diff == "n":
valid_colorsMMind = "RYGB"
elif diff == "h":
valid_colorsMMind = "RYGBOP"
elif diff == "x":
valid_colorsMMind = "RYGBOPWK"
user_guess = guess.upper()
valid_guess = True
if len(user_guess) != 4:
valid_guess = False
for i in range(len(user_guess)):
if user_guess[i] not in valid_colorsMMind:
valid_guess = False
if valid_guess == False:
user_guess = "XXXX"
def getGuessMMind(diff, guess, nodeID):
valid_colors = {
"n": "RYGB",
"h": "RYGBOP",
"x": "RYGBOPWK"
}
user_guess = guess.strip().upper()
if len(user_guess) != 4 or any(c not in valid_colors.get(diff, "RYGB") for c in user_guess):
return "XXXX"
#increase the turn count and store in tracker
for i in range(len(mindTracker)):
if mindTracker[i]['nodeID'] == nodeID:
mindTracker[i]['turns'] += 1
mindTracker[i]['last_played'] = time.time()
mindTracker[i]['diff'] = diff
return user_guess
def getHighScoreMMind(nodeID, turns, diff):
# check if player is in high score list and pick the lowest score
try:
with open('mmind_hs.pkl', 'rb') as f:
mindHighScore = pickle.load(f)
except:
logger.debug("System: MasterMind: High Score file not found.")
mindHighScore = [{'nodeID': nodeID, 'turns': turns, 'diff': diff}]
with open('mmind_hs.pkl', 'wb') as f:
pickle.dump(mindHighScore, f)
import os
hs_file = 'data/mmind_hs.pkl'
# Try to load existing high scores
if os.path.exists(hs_file):
try:
with open(hs_file, 'rb') as f:
mindHighScore = pickle.load(f)
except Exception as e:
logger.debug(f"System: MasterMind: Error loading high score file: {e}")
mindHighScore = []
else:
mindHighScore = []
# If nodeID==0, just return 0
if nodeID == 0:
# just return the high score
mindHighScore = [{'nodeID': 0, 'turns': 0, 'diff': 'n'}]
return mindHighScore
# calculate lowest score
lowest_score = mindHighScore[0]['turns']
# If no high score, add this one
if not mindHighScore:
mindHighScore = [{'nodeID': nodeID, 'turns': turns, 'diff': diff}]
with open(hs_file, 'wb') as f:
pickle.dump(mindHighScore, f)
return mindHighScore
if mindHighScore[0]['diff'] == "n" and diff == "n":
if lowest_score > turns:
# update the high score for normal if new score is lower
mindHighScore[0]['nodeID'] = nodeID
mindHighScore[0]['turns'] = turns
mindHighScore[0]['diff'] = diff
# write new high score to file
with open('mmind_hs.pkl', 'wb') as f:
# If the diff matches, compare and update if better
if mindHighScore[0]['diff'] == diff:
if turns < mindHighScore[0]['turns']:
mindHighScore[0] = {'nodeID': nodeID, 'turns': turns, 'diff': diff}
with open(hs_file, 'wb') as f:
pickle.dump(mindHighScore, f)
return mindHighScore
elif mindHighScore[0]['diff'] == "n" and diff == "h":
# update the high score for hard if normal is the only high score
mindHighScore[0]['nodeID'] = nodeID
mindHighScore[0]['turns'] = turns
mindHighScore[0]['diff'] = diff
# write new high score to file
with open('mmind_hs.pkl', 'wb') as f:
pickle.dump(mindHighScore, f)
return mindHighScore
elif mindHighScore[0]['diff'] == "h" and diff == "h":
if lowest_score > turns:
# update the high score for hard if new score is lower
mindHighScore[0]['nodeID'] = nodeID
mindHighScore[0]['turns'] = turns
mindHighScore[0]['diff'] = diff
# write new high score to file
with open('mmind_hs.pkl', 'wb') as f:
pickle.dump(mindHighScore, f)
return mindHighScore
elif mindHighScore[0]['diff'] == "n" or mindHighScore[0]['diff'] == "h" and diff == "x":
# update the high score for expert if normal or high is the only high score
mindHighScore[0]['nodeID'] = nodeID
mindHighScore[0]['turns'] = turns
mindHighScore[0]['diff'] = diff
# write new high score to file
with open('mmind_hs.pkl', 'wb') as f:
pickle.dump(mindHighScore, f)
return mindHighScore
elif mindHighScore[0]['diff'] == "x" and diff == "x":
if lowest_score > turns:
# update the high score for expert if new score is lower
mindHighScore[0]['nodeID'] = nodeID
mindHighScore[0]['turns'] = turns
mindHighScore[0]['diff'] = diff
# write new high score to file
with open('mmind_hs.pkl', 'wb') as f:
pickle.dump(mindHighScore, f)
return mindHighScore
return 0
# If the diff is different, replace with new high score for new diff
mindHighScore[0] = {'nodeID': nodeID, 'turns': turns, 'diff': diff}
with open(hs_file, 'wb') as f:
pickle.dump(mindHighScore, f)
return mindHighScore
def getEmojiMMind(secret_code):
@@ -182,7 +145,7 @@ def getEmojiMMind(secret_code):
return secret_code_emoji
#compare userGuess with secret code and provide feedback
def compareCodeMMind(secret_code, user_guess):
def compareCodeMMind(secret_code, user_guess, nodeID):
game_won = False
perfect_pins = 0
wrong_position = 0
@@ -210,9 +173,26 @@ def compareCodeMMind(secret_code, user_guess):
temp_code.remove(guess) # Remove the first occurrence of the matched color
# display feedback
if game_won:
msg += f"Correct{getEmojiMMind(user_guess)}\n"
msg += f"\n🏆Correct{getEmojiMMind(user_guess)}\nYou are the master mind!🤯"
# get turn count from tracker
for i in range(len(mindTracker)):
if mindTracker[i]['nodeID'] == nodeID:
turns = mindTracker[i]['turns'] - 2 # subtract 2 to account for increment after last guess and starting at 1
diff = mindTracker[i]['diff']
# get high score
high_score = getHighScoreMMind(nodeID, turns, diff)
if high_score[0]['turns'] != 0:
msg += f"\n🏆 High Score:{turns} turns, Difficulty:{diff}"
# reset turn count in tracker
msg += f"\nWould you like to play again? (N)ormal, (H)ard, or e(X)pert?"
# reset turn count in tracker
for i in range(len(mindTracker)):
if mindTracker[i]['nodeID'] == nodeID:
mindTracker[i]['turns'] = 0
mindTracker[i]['secret_code'] = ''
mindTracker[i]['cmd'] = 'new'
else:
msg += f"Guess{getEmojiMMind(user_guess)}\n"
msg += f"\nGuess{getEmojiMMind(user_guess)}\n"
if perfect_pins > 0 and game_won == False:
msg += "✅ color ✅ position: {}".format(perfect_pins)
@@ -231,11 +211,11 @@ def playGameMMind(diff, secret_code, turn_count, nodeID, message):
msg = ''
won = False
if turn_count <= 10:
user_guess = getGuessMMind(diff, message)
user_guess = getGuessMMind(diff, message, nodeID)
if user_guess == "XXXX":
msg += f"⛔️Invalid guess. Please enter 4 valid colors letters.\n🔴🟢🔵🔴 is RGBR"
return msg
check_guess = compareCodeMMind(secret_code, user_guess)
check_guess = compareCodeMMind(secret_code, user_guess, nodeID)
# display turn count and feedback
msg += "Turn {}:".format(turn_count)
@@ -245,18 +225,6 @@ def playGameMMind(diff, secret_code, turn_count, nodeID, message):
if won == True:
msg += f"\n🎉🧠 you win 🥷🤯"
# get high score
high_score = getHighScoreMMind(nodeID, turn_count, diff)
if high_score != 0:
msg += f"\n🏆 High Score:{high_score[0]['turns']} turns, Difficulty:{high_score[0]['diff'].upper()}"
msg += "\nWould you like to play again?\n(N)ormal, (H)ard, e(X)pert (E)nd?"
# reset turn count in tracker
for i in range(len(mindTracker)):
if mindTracker[i]['nodeID'] == nodeID:
mindTracker[i]['turns'] = 1
mindTracker[i]['secret_code'] = ''
mindTracker[i]['cmd'] = 'new'
else:
# increment turn count and keep playing
turn_count += 1
@@ -266,12 +234,12 @@ def playGameMMind(diff, secret_code, turn_count, nodeID, message):
mindTracker[i]['turns'] = turn_count
elif won == False:
msg += f"🙉Game Over🙈\nThe code was: {getEmojiMMind(secret_code)}"
msg += "\nYou have run out of turns.😿"
msg += "\nWould you like to play again? (N)ormal, (H)ard, or e(X)pert?"
msg += f"\nYou have run out of turns.😿"
msg += f"\nWould you like to play again? (N)ormal, (H)ard, or e(X)pert?"
# reset turn count in tracker
for i in range(len(mindTracker)):
if mindTracker[i]['nodeID'] == nodeID:
mindTracker[i]['turns'] = 1
mindTracker[i]['turns'] = 0
mindTracker[i]['secret_code'] = ''
mindTracker[i]['cmd'] = 'new'
+3 -4
View File
@@ -6,8 +6,7 @@ import pickle
from modules.log import *
vpStartingCash = 20
vpTracker= [{'nodeID': 0, 'cmd': 'new', 'time': time.time(), 'cash': vpStartingCash, 'player': None, 'deck': None, 'highScore': 0, 'drawCount': 0}]
from modules.settings import vpTracker
# Define the Card class
class CardVP:
@@ -304,7 +303,7 @@ def playVideoPoker(nodeID, message):
# create new player if not in tracker
logger.debug(f"System: VideoPoker: New Player {nodeID}")
vpTracker.append({'nodeID': nodeID, 'cmd': 'new', 'time': time.time(), 'cash': vpStartingCash, 'player': None, 'deck': None, 'highScore': 0, 'drawCount': 0})
return f"Welcome to 🎰VideoPoker♥️ you have {vpStartingCash} coins, Whats your bet?"
return f"You have {vpStartingCash} coins, \nWhats your bet?"
# Gather the player's bet
if getLastCmdVp(nodeID) == "new" or getLastCmdVp(nodeID) == "gameOver":
@@ -426,7 +425,7 @@ def playVideoPoker(nodeID, message):
if player.bankroll < 1:
player.bankroll = vpStartingCash
msg += "\nLooks 💸 like you're out of money. 💳 resetting ballance 🏧"
msg += f"\nLooks 💸 like you're out of money. 💳 resetting ballance 🏧"
elif player.bankroll > vpTracker[i]['highScore']:
vpTracker[i]['highScore'] = player.bankroll
msg += " 🎉HighScore!"
+141 -37
View File
@@ -48,7 +48,7 @@ meshBotAI = """
PROMPT
{input}
"""
"""
if llmContext_fromGoogle:
meshBotAI = meshBotAI + """
@@ -76,6 +76,142 @@ if llmEnableHistory:
"""
# Tooling Functions Defined Here
# Example: current_time function
def llmTool_current_time():
"""
Example tool function to get the current time.
:return: Current time string.
"""
return datetime.now().strftime('%Y-%m-%d %H:%M:%S %Z')
def llmTool_math_calculator(expression):
"""
Example tool function to perform basic math calculations.
:param expression: A string containing a math expression (e.g., "2 + 2").
:return: The result of the calculation as a string.
"""
try:
# WARNING: Using eval can be dangerous if not controlled properly.
# This is a simple example; in production, consider using a safe math parser.
result = eval(expression, {"__builtins__": None}, {})
return str(result)
except Exception as e:
return f"Error in calculation: {e}"
def llmTool_get_google(query, num_results=3):
"""
Example tool function to perform a Google search and return results.
:param query: The search query string.
:param num_results: Number of search results to return.
:return: A list of search result titles and descriptions.
"""
results = []
try:
googleSearch = search(query, advanced=True, num_results=num_results)
for result in googleSearch:
results.append(f"{result.title}: {result.description}")
return results
except Exception as e:
return [f"Error in Google search: {e}"]
llmFunctions = [
{
"name": "llmTool_current_time",
"description": "Get the current time.",
"parameters": {
"type": "object",
"properties": {}
}
},
{
"name": "llmTool_math_calculator",
"description": "Perform basic math calculations.",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "A math expression to evaluate, e.g., '2 + 2'."
}
},
"required": ["expression"]
}
},
{
"name": "llmTool_get_google",
"description": "Perform a Google search and return results.",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query string."
},
"num_results": {
"type": "integer",
"description": "Number of search results to return.",
"default": 3
}
},
"required": ["query"]
}
}
]
def get_google_context(input, num_results):
# Get context from Google search results
googleResults = []
try:
googleSearch = search(input, advanced=True, num_results=num_results)
if googleSearch:
for result in googleSearch:
googleResults.append(f"{result.title} {result.description}")
else:
googleResults = ['no other context provided']
except Exception as e:
logger.debug(f"System: LLM Query: context gathering failed, likely due to network issues")
googleResults = ['no other context provided']
return googleResults
def send_ollama_query(llmQuery):
# Send the query to the Ollama API and return the response
result = requests.post(ollamaAPI, data=json.dumps(llmQuery))
if result.status_code == 200:
result_json = result.json()
result = result_json.get("response", "")
# deepseek 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}")
return result
def send_ollama_tooling_query(prompt, functions, model=None, max_tokens=450):
"""
Send a prompt and function/tool definitions to Ollama API for function calling.
:param prompt: The user prompt string.
:param functions: List of function/tool definitions (see Ollama API docs).
:param model: Model name (optional, defaults to llmModel).
:param max_tokens: Max tokens for response.
:return: Ollama API response JSON.
"""
if model is None:
model = llmModel
payload = {
"model": model,
"prompt": prompt,
"functions": functions,
"stream": False,
"max_tokens": max_tokens
}
result = requests.post(ollamaAPI, data=json.dumps(payload))
if result.status_code == 200:
return result.json()
else:
raise Exception(f"HTTP Error: {result.status_code} - {result.text}")
def llm_query(input, nodeID=0, location_name=None):
global antiFloodLLM, llmChat_history
googleResults = []
@@ -109,23 +245,7 @@ def llm_query(input, nodeID=0, location_name=None):
antiFloodLLM.append(nodeID)
if llmContext_fromGoogle and not rawLLMQuery:
# grab some context from the internet using google search hits (if available)
# localization details at https://pypi.org/project/googlesearch-python/
# remove common words from the search query
# commonWordsList = ["is", "for", "the", "of", "and", "in", "on", "at", "to", "with", "by", "from", "as", "a", "an", "that", "this", "these", "those", "there", "here", "where", "when", "why", "how", "what", "which", "who", "whom", "whose", "whom"]
# sanitizedSearch = ' '.join([word for word in input.split() if word.lower() not in commonWordsList])
try:
googleSearch = search(input, advanced=True, num_results=googleSearchResults)
if googleSearch:
for result in googleSearch:
# SearchResult object has url= title= description= just grab title and description
googleResults.append(f"{result.title} {result.description}")
else:
googleResults = ['no other context provided']
except Exception as e:
logger.debug(f"System: LLM Query: context gathering failed, likely due to network issues")
googleResults = ['no other context provided']
googleResults = get_google_context(input, googleSearchResults)
history = llmChat_history.get(nodeID, ["", ""])
@@ -151,20 +271,11 @@ def llm_query(input, nodeID=0, location_name=None):
llmQuery = {"model": llmModel, "prompt": modelPrompt, "stream": False, "max_tokens": tokens}
# Query the model via Ollama web API
result = requests.post(ollamaAPI, data=json.dumps(llmQuery))
# Condense the result to just needed
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}")
result = send_ollama_query(llmQuery)
#logger.debug(f"System: LLM Response: " + result.strip().replace('\n', ' '))
except Exception as e:
antiFloodLLM.remove(nodeID) # Ensure removal on error
logger.warning(f"System: LLM failure: {e}")
return "⛔️I am having trouble processing your request, please try again later."
@@ -175,15 +286,8 @@ def llm_query(input, nodeID=0, location_name=None):
#retryy loop to truncate the response
logger.warning(f"System: LLM Query: Response exceeded {tokens} characters, requesting truncation")
truncateQuery = {"model": llmModel, "prompt": truncatePrompt + response, "stream": False, "max_tokens": tokens}
truncateResult = requests.post(ollamaAPI, data=json.dumps(truncateQuery))
if truncateResult.status_code == 200:
truncate_json = truncateResult.json()
result = truncate_json.get("response", "")
truncateResult = send_ollama_query(truncateQuery)
else:
#use the original result if truncation fails
logger.warning("System: LLM Query: Truncation failed, using original response")
# cleanup for message output
response = result.strip().replace('\n', ' ')
+50 -12
View File
@@ -295,9 +295,36 @@ def get_NOAAweather(lat=0, lon=0, unit=0):
return weather
def abbreviate_noaa(row):
# replace long strings with shorter ones for display
replacements = {
def case_insensitive_replace(text, old, new):
"""Replace all occurrences of old (any case) in text with new."""
idx = 0
old_lower = old.lower()
text_lower = text.lower()
while True:
idx = text_lower.find(old_lower, idx)
if idx == -1:
break
text = text[:idx] + new + text[idx+len(old):]
text_lower = text.lower()
idx += len(new)
return text
def abbreviate_noaa(data=""):
# Long phrases (with spaces)
phrase_replacements = {
"less than a tenth of an inch possible": "< 0.1in",
"between a tenth and quarter of an inch possible": "0.1-0.25in",
"between a quarter and half an inch possible": "0.25-0.5in",
"between a half and three quarters of an inch possible": "0.5-0.75in",
"between one and two inches possible": "1-2in",
"between two and three inches possible": "2-3in",
"between three and four inches possible": "3-4in",
"between four and five inches possible": "4-5in",
"between five and six inches possible": "5-6in",
"between six and eight inches possible": "6-8in",
}
# Single words (no spaces)
word_replacements = {
"monday": "Mon",
"tuesday": "Tue",
"wednesday": "Wed",
@@ -313,6 +340,8 @@ def abbreviate_noaa(row):
"south": "S",
"east": "E",
"west": "W",
"accumulation": "accum",
"visibility": "vis",
"precipitation": "precip",
"showers": "shwrs",
"thunderstorms": "t-storms",
@@ -334,17 +363,26 @@ def abbreviate_noaa(row):
"degrees": "°",
"percent": "%",
"department": "Dept.",
"amounts less than a tenth of an inch possible.": "< 0.1in",
"temperatures": "temps.",
"temperature": "temp.",
"temperatures": "temps:",
"temperature": "temp:",
"amounts": "amts:",
"afternoon": "Aftn",
"evening": "Eve",
}
line = row
for key, value in replacements.items():
for variant in (key, key.capitalize(), key.upper()):
if variant != value:
line = line.replace(variant, value)
return line
text = data
# Replace long phrases (case-insensitive)
for key in sorted(phrase_replacements, key=len, reverse=True):
value = phrase_replacements[key]
text = case_insensitive_replace(text, key, value)
# Replace single words (case-insensitive)
for key in word_replacements:
value = word_replacements[key]
text = case_insensitive_replace(text, key, value)
return text
def getWeatherAlertsNOAA(lat=0, lon=0, useDefaultLatLon=False):
# get weather alerts from NOAA limited to ALERT_COUNT with the total number of alerts found
+68 -32
View File
@@ -17,6 +17,16 @@ if radio_detection_enabled:
import socket
if voxDetectionEnabled:
# methods available for trap word processing, these can be called by VOX detection when trap words are detected
from mesh_bot import tell_joke, handle_wxc, handle_moon, handle_sun, handle_riverFlow, handle_tide, handle_satpass
botMethods = {
"joke": tell_joke,
"weather": handle_wxc,
"moon": handle_moon,
"daylight": handle_sun,
"river": handle_riverFlow,
"tide": handle_tide,
"satellite": handle_satpass}
# module global variables
previousVoxState = False
voxHoldTime = signalHoldTime
@@ -25,7 +35,7 @@ if voxDetectionEnabled:
import sounddevice as sd # pip install sounddevice sudo apt install portaudio19-dev
from vosk import Model, KaldiRecognizer # pip install vosk
import json
q = asyncio.Queue(maxsize=10) # what is a reasonable limit?
q = asyncio.Queue(maxsize=32) # queue for audio data
if useLocalVoxModel:
voxModel = Model(lang=localVoxModelPath) # use built in model for specified language
@@ -116,11 +126,41 @@ def get_sig_strength():
strength = get_hamlib('l STRENGTH')
return strength
def vox_callback(indata, frames, time, status):
if status:
logger.warning(f"RadioMon: VOX input status: {status}")
q.put(bytes(indata))
def checkVoxTrapWords(text):
try:
if not voxOnTrapList:
logger.debug(f"RadioMon: VOX detected: {text}")
return text
if text:
traps = [voxTrapList] if isinstance(voxTrapList, str) else voxTrapList
text_lower = text.lower()
for trap in traps:
trap_clean = trap.strip()
trap_lower = trap_clean.lower()
idx = text_lower.find(trap_lower)
if debugVoxTmsg:
logger.debug(f"RadioMon: VOX checking for trap word '{trap_lower}' in: '{text}' (index: {idx})")
if idx != -1:
new_text = text[idx + len(trap_clean):].strip()
if debugVoxTmsg:
logger.debug(f"RadioMon: VOX detected trap word '{trap_lower}' in: '{text}' (remaining: '{new_text}')")
new_words = new_text.split()
if voxEnableCmd:
for word in new_words:
if word in botMethods:
logger.info(f"RadioMon: VOX action '{word}' with '{new_text}'")
if word == "joke":
return botMethods[word](vox=True)
else:
return botMethods[word](None, None, None, vox=True)
logger.debug(f"RadioMon: VOX returning text after trap word '{trap_lower}': '{new_text}'")
return new_text
if debugVoxTmsg:
logger.debug(f"RadioMon: VOX no trap word found in: '{text}'")
return None
except Exception as e:
logger.debug(f"RadioMon: Error in checkVoxTrapWords: {e}")
return None
async def signalWatcher():
global previousStrength
@@ -146,15 +186,23 @@ async def signalWatcher():
signalCycle = 0
previousStrength = -40
def make_vox_callback(loop, q):
async def make_vox_callback(loop, q):
def vox_callback(indata, frames, time, status):
if status:
logger.warning(f"RadioMon: VOX input status: {status}")
try:
loop.call_soon_threadsafe(q.put_nowait, bytes(indata))
except asyncio.QueueFull:
# Optionally log or just drop the oldest
logger.debug("RadioMon: VOX queue full, dropping audio frame")
# Drop the oldest item and add the new one
try:
q.get_nowait() # Remove oldest
except asyncio.QueueEmpty:
pass
try:
loop.call_soon_threadsafe(q.put_nowait, bytes(indata))
except asyncio.QueueFull:
# If still full, just drop this frame
logger.debug("RadioMon: VOX queue full, dropping audio frame")
except RuntimeError:
# Loop may be closed
pass
@@ -169,11 +217,11 @@ async def voxMonitor():
logger.debug(f"RadioMon: VOX monitor started on device {device_info['name']} with samplerate {samplerate} using trap words: {voxTrapList if voxOnTrapList else 'none'}")
rec = KaldiRecognizer(model, samplerate)
loop = asyncio.get_running_loop()
callback = make_vox_callback(loop, q)
callback = await make_vox_callback(loop, q)
with sd.RawInputStream(
device=voxInputDevice,
samplerate=samplerate,
blocksize=8000,
blocksize=4000,
dtype='int16',
channels=1,
callback=callback
@@ -183,28 +231,16 @@ async def voxMonitor():
if rec.AcceptWaveform(data):
result = rec.Result()
text = json.loads(result).get("text", "")
# check for trap words
# process text
if text and text != 'huh':
if voxOnTrapList:
if isinstance(voxTrapList, str):
traps = [voxTrapList]
else:
traps = voxTrapList
if any(trap.lower() in text.lower() for trap in traps):
#remove the trap words from the text
for trap in traps:
text = text.replace(trap, '')
text = text.strip()
if text:
logger.debug(f"RadioMon: VOX 🎙️Trapped {voxTrapList} in: {text}")
voxMsgQueue.append(f"🎙️Trapped {voxDescription}: {text}")
else:
if debugVoxTmsg:
logger.debug(f"RadioMon: VOX ignored text not on trap list: {text}")
else:
voxMsgQueue.append(f"🎙️Detected {voxDescription}: {text}")
await asyncio.sleep(0.5)
result = checkVoxTrapWords(text)
if result:
# If result is a function return, handle it (send to mesh, log, etc.)
# If it's just text, handle as a normal message
voxMsgQueue.append(result)
await asyncio.sleep(0.1)
except Exception as e:
logger.error(f"RadioMon: Error in VOX monitor: {e}")
# end of file
# end of file
+104
View File
@@ -0,0 +1,104 @@
# modules/scheduler.py 2025 meshing-around
import schedule
from modules.log import logger
from modules.system import send_message, BroadcastScheduler
from modules.system import send_message
# methods available for custom scheduler messages
from mesh_bot import tell_joke, welcome_message, MOTD, handle_wxc, handle_moon, handle_sun, handle_riverFlow, handle_tide, handle_satpass
async def setup_scheduler(
schedulerMotd, MOTD, schedulerMessage, schedulerChannel, schedulerInterface,
schedulerValue, schedulerTime, schedulerInterval, logger, BroadcastScheduler
):
schedulerValue = schedulerValue.lower().strip()
schedulerTime = schedulerTime.strip()
schedulerInterval = schedulerInterval.strip()
schedulerChannel = int(schedulerChannel)
schedulerInterface = int(schedulerInterface)
# Setup the scheduler based on configuration
try:
if schedulerMotd:
scheduler_message = MOTD
else:
scheduler_message = schedulerMessage
# Basic Scheduler Options
if 'custom' not in schedulerValue:
# Basic scheduler job to run the schedule see examples below for custom schedules
if schedulerValue.lower() == 'day':
if schedulerTime != '':
schedule.every().day.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
else:
schedule.every(int(schedulerInterval)).days.do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'mon' in schedulerValue.lower() and schedulerTime != '':
schedule.every().monday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'tue' in schedulerValue.lower() and schedulerTime != '':
schedule.every().tuesday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'wed' in schedulerValue.lower() and schedulerTime != '':
schedule.every().wednesday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'thu' in schedulerValue.lower() and schedulerTime != '':
schedule.every().thursday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'fri' in schedulerValue.lower() and schedulerTime != '':
schedule.every().friday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'sat' in schedulerValue.lower() and schedulerTime != '':
schedule.every().saturday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'sun' in schedulerValue.lower() and schedulerTime != '':
schedule.every().sunday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'hour' in schedulerValue.lower():
schedule.every(int(schedulerInterval)).hours.do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
elif 'min' in schedulerValue.lower():
schedule.every(int(schedulerInterval)).minutes.do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
logger.debug(f"System: Starting the basic scheduler to send '{scheduler_message}' on schedule '{schedulerValue}' every {schedulerInterval} interval at time '{schedulerTime}' on Device:{schedulerInterface} Channel:{schedulerChannel}")
else:
# Default schedule if no valid configuration is provided
# custom scheduler job to run the schedule see examples below
logger.debug(f"System: Starting the scheduler to send reminder every Monday at noon on Device:{schedulerInterface} Channel:{schedulerChannel}")
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Enabled Reminder"))
# send a joke every 15 minutes
#schedule.every(15).minutes.do(lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface))
# Start the Broadcast Scheduler
await BroadcastScheduler()
except Exception as e:
logger.error(f"System: Scheduler Error {e}")
# 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))
# Send a Welcome Notice for group on the 15th and 25th of the month at 12:00 using send_message function to channel 2 on device 1
#schedule.every().day.at("12:00").do(lambda: send_message("Welcome to the group", 2, 0, 1)).day(15, 25)
# Send a Welcome Notice for group on the 15th and 25th of the month at 12:00
#schedule.every().day.at("12:00").do(lambda: send_message("Welcome to the group", schedulerChannel, 0, schedulerInterface)).day(15, 25)
# Send a joke every 6 hours
#schedule.every(6).hours.do(lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface))
# Send a joke every 2 minutes
#schedule.every(2).minutes.do(lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface))
# Send the Welcome Message every other day at 08:00
#schedule.every(2).days.at("08:00").do(lambda: send_message(welcome_message, schedulerChannel, 0, schedulerInterface))
# Send the MOTD every day at 13:00
#schedule.every().day.at("13:00").do(lambda: send_message(MOTD, schedulerChannel, 0, schedulerInterface))
# Send bbslink looking for peers every other day at 10:00
#schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", schedulerChannel, 0, schedulerInterface))
+17 -2
View File
@@ -28,11 +28,23 @@ wiki_return_limit = 3 # limit the number of sentences returned off the first par
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
surveyTracker, tictactoeTracker, hamtestTracker, hangmanTracker, golfTracker, mastermindTracker, vpTracker, blackjackTracker, lemonadeTracker, dwPlayerTracker, jackTracker = [], [], [], [], [], [], [], [], [], [], [] # game trackers
cmdHistory = [] # list to hold the command history for lheard and history commands
msg_history = [] # list to hold the message history for the messages command
max_bytes = 200 # Meshtastic has ~237 byte limit, use conservative 200 bytes for message content
voxMsgQueue = [] # queue for VOX detected messages
# Game trackers
surveyTracker = [] # Survey game tracker
tictactoeTracker = [] # TicTacToe game tracker
hamtestTracker = [] # Ham radio test tracker
hangmanTracker = [] # Hangman game tracker
golfTracker = [] # GolfSim game tracker
mastermindTracker = [] # Mastermind game tracker
vpTracker = [] # Video Poker game tracker
jackTracker = [] # Blackjack game tracker
lemonadeTracker = [] # Lemonade Stand game tracker
dwPlayerTracker = [] # DopeWars player tracker
jackTracker = [] # Jack game tracker
mindTracker = [] # Mastermind (mmind) game tracker
# Read the config file, if it does not exist, create basic config file
config = configparser.ConfigParser()
@@ -268,6 +280,7 @@ try:
highfly_ignoreList = config['sentry'].get('highFlyingIgnoreList', '').split(',') # default empty
highfly_check_openskynetwork = config['sentry'].getboolean('highflyOpenskynetwork', True) # default True check with OpenSkyNetwork if highfly detected
detctionSensorAlert = config['sentry'].getboolean('detectionSensorAlert', False) # default False
reqLocationEnabled = config['sentry'].getboolean('reqLocationEnabled', False) # default False
# location
location_enabled = config['location'].getboolean('enabled', True)
@@ -376,6 +389,7 @@ try:
voxInputDevice = config['radioMon'].get('voxInputDevice', 'default') # default default
voxOnTrapList = config['radioMon'].getboolean('voxOnTrapList', False) # default False
voxTrapList = config['radioMon'].get('voxTrapList', 'chirpy').split(',') # default chirpy
voxEnableCmd = config['radioMon'].getboolean('voxEnableCmd', True) # default True
# file monitor
file_monitor_enabled = config['fileMon'].getboolean('filemon_enabled', False)
@@ -403,6 +417,7 @@ try:
tictactoe_enabled = config['games'].getboolean('tictactoe', True)
quiz_enabled = config['games'].getboolean('quiz', False)
survey_enabled = config['games'].getboolean('survey', False)
default_survey = config['games'].get('defaultSurvey', 'example') # default example
surveyRecordID = config['games'].getboolean('surveyRecordID', True)
surveyRecordLocation = config['games'].getboolean('surveyRecordLocation', True)
@@ -411,7 +426,7 @@ try:
splitDelay = config['messagingSettings'].getfloat('splitDelay', 0) # default 0
MESSAGE_CHUNK_SIZE = config['messagingSettings'].getint('MESSAGE_CHUNK_SIZE', 160) # default 160 chars
wantAck = config['messagingSettings'].getboolean('wantAck', False) # default False
maxBuffer = config['messagingSettings'].getint('maxBuffer', 200) # default 200
maxBuffer = config['messagingSettings'].getint('maxBuffer', 200) # default 200 bytes
enableHopLogs = config['messagingSettings'].getboolean('enableHopLogs', False) # default False
debugMetadata = config['messagingSettings'].getboolean('debugMetadata', False) # default False
metadataFilter = config['messagingSettings'].get('metadataFilter', '').split(',') # default empty
+18 -14
View File
@@ -49,20 +49,24 @@ class SurveyModule:
logger.error(f"Survey: Error loading surveys: {e}")
def start_survey(self, user_id, survey_name='example', location=None):
"""Begin a new survey session for a user."""
if not survey_name:
survey_name = 'example'
if survey_name not in allowedSurveys:
return f"error: survey '{survey_name}' is not allowed."
self.responses[user_id] = {
'survey_name': survey_name,
'current_question': 0,
'answers': [],
'location': location if surveyRecordLocation and location is not None else 'N/A'
}
msg = f"'{survey_name}'📝survey\nSend answer' or 'end'\n"
msg += self.show_question(user_id)
return msg
try:
"""Begin a new survey session for a user."""
if not survey_name:
survey_name = default_survey
if survey_name not in allowedSurveys:
return f"error: survey '{survey_name}' is not allowed."
self.responses[user_id] = {
'survey_name': survey_name,
'current_question': 0,
'answers': [],
'location': location if surveyRecordLocation and location is not None else 'N/A'
}
msg = f"'{survey_name}'📝survey\nSend answer' or 'end'\n"
msg += self.show_question(user_id)
return msg
except Exception as e:
logger.error(f"Error starting survey for user {user_id}: {e}")
return "An error occurred while starting the survey. Please try again later."
def show_question(self, user_id):
"""Show the current question for the user, or end the survey."""
+251 -119
View File
@@ -7,6 +7,7 @@ import meshtastic.ble_interface
import time
import asyncio
import random
import base64
# not ideal but needed?
import contextlib # for suppressing output on watchdog
import io # for suppressing output on watchdog
@@ -315,6 +316,24 @@ if ble_count > 1:
logger.critical(f"System: Multiple BLE interfaces detected. Only one BLE interface is allowed. Exiting")
exit()
def xor_hash(data: bytes) -> int:
"""Compute an XOR hash from bytes."""
result = 0
for char in data:
result ^= char
return result
def generate_hash(name: str, key: str) -> int:
"""generate the channel number by hashing the channel name and psk"""
if key == "AQ==":
key = "1PG7OiApB1nwvP+rz05pAQ=="
replaced_key = key.replace("-", "+").replace("_", "/")
key_bytes = base64.b64decode(replaced_key.encode("utf-8"))
h_name = xor_hash(bytes(name, "utf-8"))
h_key = xor_hash(key_bytes)
result: int = h_name ^ h_key
return result
# Initialize interfaces
logger.debug(f"System: Initializing Interfaces")
interface1 = interface2 = interface3 = interface4 = interface5 = interface6 = interface7 = interface8 = interface9 = None
@@ -331,7 +350,20 @@ for i in range(1, 10):
if interface_type == 'serial':
globals()[f'interface{i}'] = meshtastic.serial_interface.SerialInterface(globals().get(f'port{i}'))
elif interface_type == 'tcp':
globals()[f'interface{i}'] = meshtastic.tcp_interface.TCPInterface(globals().get(f'hostname{i}'))
host = globals().get(f'hostname{i}', '127.0.0.1')
port = 4403
# Allow host:port format
if isinstance(host, str) and ':' in host:
maybe_host, maybe_port = host.rsplit(':', 1)
if maybe_port.isdigit():
host = maybe_host
try:
port = int(maybe_port)
except ValueError:
port = 4403
globals()[f'interface{i}'] = meshtastic.tcp_interface.TCPInterface(hostname=host, portNumber=port)
elif interface_type == 'ble':
globals()[f'interface{i}'] = meshtastic.ble_interface.BLEInterface(globals().get(f'mac{i}'))
else:
@@ -352,6 +384,43 @@ for i in range(1, 10):
else:
globals()[f'myNodeNum{i}'] = 777
# Fetch channel list from each device
channel_list = []
for i in range(1, 10):
if globals().get(f'interface{i}') and globals().get(f'interface{i}_enabled'):
try:
node = globals()[f'interface{i}'].getNode('^local')
channels = node.channels
channel_dict = {}
for channel in channels:
if hasattr(channel, 'role') and channel.role:
channel_name = getattr(channel.settings, 'name', '').strip()
channel_number = getattr(channel, 'index', 0)
# Only add channels with a non-empty name
if channel_name:
channel_dict[channel_name] = channel_number
channel_list.append({
"interface_id": i,
"channels": channel_dict
})
logger.debug(f"System: Fetched Channel List from Device{i}")
except Exception as e:
logger.error(f"System: Error fetching channel list from Device{i}: {e}")
# add channel hash to channel_list
for device in channel_list:
interface_id = device["interface_id"]
for channel_name, channel_number in device["channels"].items():
psk_base64 = base64.b64encode(channel.settings.psk).decode('utf-8')
channel_hash = generate_hash(channel_name, psk_base64)
# add hash to the channel entry in channel_list under key 'hash'
for entry in channel_list:
if entry["interface_id"] == interface_id:
entry["channels"][channel_name] = {
"number": channel_number,
"hash": channel_hash
}
#### FUN-ctions ####
def cleanup_memory():
@@ -386,11 +455,11 @@ def cleanup_memory():
# Clean up stale game tracker entries
cleanup_game_trackers(current_time)
# Clean up multiPingList of completed or stale entries
if 'multiPingList' in globals():
multiPingList[:] = [ping for ping in multiPingList
if ping.get('message_from_id', 0) != 0 and
ping.get('count', 0) > 0]
# # Clean up multiPingList of completed or stale entries
# if 'multiPingList' in globals():
# multiPingList[:] = [ping for ping in multiPingList
# if ping.get('message_from_id', 0) != 0 and
# ping.get('count', 0) > 0]
except Exception as e:
logger.error(f"System: Error during memory cleanup: {e}")
@@ -546,7 +615,8 @@ def get_node_location(nodeID, nodeInt=1, channel=0, round_digits=2):
if fuzzItAll:
latitude = round(latitude, round_digits)
longitude = round(longitude, round_digits)
logger.debug(f"System: Fuzzed location data for {nodeID}")
logger.debug(f"System: Fuzzed location data for {nodeID} is {latitude}, {longitude}")
logger.debug(f"System: Location data for {nodeID} is {latitude}, {longitude}")
return [latitude, longitude]
except Exception as e:
logger.warning(f"System: Error processing position for node {nodeID}: {e}")
@@ -557,57 +627,59 @@ def get_node_location(nodeID, nodeInt=1, channel=0, round_digits=2):
else:
return config_position
def get_closest_nodes(nodeInt=1,returnCount=3, channel=publicChannel):
interface = globals()[f'interface{nodeInt}']
node_list = []
async def get_closest_nodes(nodeInt=1,returnCount=3, channel=publicChannel):
interface = globals()[f'interface{nodeInt}']
node_list = []
if interface.nodes:
for node in interface.nodes.values():
if 'position' in node:
try:
nodeID = node['num']
latitude = node['position']['latitude']
longitude = node['position']['longitude']
#lastheard time in unix time
lastheard = node.get('lastHeard', 0)
#if last heard is over 24 hours ago, ignore the node
if lastheard < (time.time() - 86400):
continue
# Calculate distance to node from config.ini location
distance = round(geopy.distance.geodesic((latitudeValue, longitudeValue), (latitude, longitude)).m, 2)
if (distance < sentry_radius):
if (nodeID not in [globals().get(f'myNodeNum{i}') for i in range(1, 10)]) and str(nodeID) not in sentryIgnoreList:
node_list.append({'id': nodeID, 'latitude': latitude, 'longitude': longitude, 'distance': distance})
except Exception as e:
pass
else:
# request location data
reqLocationEnabled = False
if reqLocationEnabled:
if interface.nodes:
for node in interface.nodes.values():
if 'position' in node:
try:
logger.debug(f"System: Requesting location data for {node['id']}, lastHeard: {node.get('lastHeard', 'N/A')}")
# one idea is to send a ping to the node to request location data for if or when, ask again later
interface.sendPosition(destinationId=node['id'], wantResponse=False, channelIndex=channel)
# wait a bit
time.sleep(3)
# send a traceroute request
interface.sendTraceRoute(destinationId=node['id'], channelIndex=channel, wantResponse=False)
# wait a bit
time.sleep(1)
nodeID = node['num']
latitude = node['position']['latitude']
longitude = node['position']['longitude']
#lastheard time in unix time
lastheard = node.get('lastHeard', 0)
#if last heard is over 24 hours ago, ignore the node
if lastheard < (time.time() - 86400):
continue
# Calculate distance to node from config.ini location
distance = round(geopy.distance.geodesic((latitudeValue, longitudeValue), (latitude, longitude)).m, 2)
if (distance < sentry_radius):
if (nodeID not in [globals().get(f'myNodeNum{i}') for i in range(1, 10)]) and str(nodeID) not in sentryIgnoreList:
node_list.append({'id': nodeID, 'latitude': latitude, 'longitude': longitude, 'distance': distance})
except Exception as e:
logger.error(f"System: Error requesting location data for {node['id']}. Error: {e}")
# sort by distance closest
#node_list.sort(key=lambda x: (x['latitude']-latitudeValue)**2 + (x['longitude']-longitudeValue)**2)
node_list.sort(key=lambda x: x['distance'])
# return the first 3 closest nodes by default
return node_list[:returnCount]
else:
logger.warning(f"System: No nodes found in closest_nodes on interface {nodeInt}")
return ERROR_FETCHING_DATA
pass
else:
# request location data currently blocking needs to be async
reqLocationEnabled = False
if reqLocationEnabled:
try:
logger.debug(f"System: Requesting location data for {node['id']}, lastHeard: {node.get('lastHeard', 'N/A')}")
# if not a interface node
if node['num'] in [globals().get(f'myNodeNum{i}') for i in range(1, 10)]:
ignore = True
else:
# one idea is to send a ping to the node to request location data for if or when, ask again later
interface.sendPosition(destinationId=node['id'], wantResponse=False, channelIndex=channel)
# wayyy too fast async wait
# send a traceroute request
interface.sendTraceRoute(destinationId=node['id'], channelIndex=channel, wantResponse=False)
except Exception as e:
logger.error(f"System: Error requesting location data for {node['id']}. Error: {e}")
# sort by distance closest
#node_list.sort(key=lambda x: (x['latitude']-latitudeValue)**2 + (x['longitude']-longitudeValue)**2)
node_list.sort(key=lambda x: x['distance'])
# return the first 3 closest nodes by default
return node_list[:returnCount]
else:
logger.warning(f"System: No nodes found in closest_nodes on interface {nodeInt}")
return ERROR_FETCHING_DATA
def handleFavoriteNode(nodeInt=1, nodeID=0, aor=False):
# Add or remove a favorite node for the given interface. aor: True to add, False to remove.
@@ -797,11 +869,43 @@ def send_message(message, ch, nodeid=0, nodeInt=1, bypassChuncking=False):
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending DM: " + CustomFormatter.white + message.replace('\n', ' ') + CustomFormatter.purple +\
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
interface.sendText(text=message, channelIndex=ch, destinationId=nodeid)
# Throttle the message sending to prevent spamming the device
time.sleep(responseDelay)
return True
except Exception as e:
logger.error(f"System: Exception during send_message: {e} (message length: {len(message)})")
return False
def send_raw_bytes(nodeid, raw_bytes, nodeInt=1, channel=0, portnum=256, want_ack=True):
# Send raw bytes to a node using the Meshtastic interface.
interface = globals()[f'interface{nodeInt}']
try:
interface.sendData(
raw_bytes,
destinationId=nodeid,
portNum=portnum,
channelIndex=channel,
wantAck=want_ack
)
# Throttle the message sending to prevent spamming the device
logger.debug(f"System: Sent raw bytes to {nodeid} on portnum {portnum} via Device{nodeInt}")
time.sleep(responseDelay)
return True
except Exception as e:
logger.error(f"System: Error sending raw bytes to {nodeid} via Device{nodeInt}: {e} bytes: {raw_bytes}")
return False
def decode_raw_bytes(raw_bytes):
# Decode raw bytes received from a Meshtastic device.
try:
decoded_message = raw_bytes.decode('utf-8', errors='ignore')
# reminder for a synch word check or crc check if needed later
logger.debug(f"Decoded raw bytes: {decoded_message}")
return decoded_message
except Exception as e:
logger.debug(f"System: Error decoding raw bytes: {e} bytes: {raw_bytes}")
return ""
def messageTrap(msg):
# Check if the message contains a trap word, this is the first filter for listning to messages
# after this the message is passed to the command_handler in the bot.py which is switch case filter for applying word to function
@@ -862,17 +966,23 @@ def save_bbsBanList():
def load_bbsBanList():
global bbs_ban_list
# load the bbs_ban_list from file
loaded_list = []
try:
with open('data/bbs_ban_list.txt', 'r') as f:
bbs_ban_list = [line.strip() for line in f.readlines() if line.strip()]
logger.debug("System: BBS ban list loaded")
loaded_list = [line.strip() for line in f if line.strip()]
logger.debug("System: BBS ban list loaded from file")
except FileNotFoundError:
bbs_ban_list = config['bbs'].get('bbs_ban_list', '').split(',')
logger.debug("System: No BBS ban list found, starting with default")
config_val = config['bbs'].get('bbs_ban_list', '')
if config_val:
loaded_list = [x.strip() for x in config_val.split(',') if x.strip()]
logger.debug("System: No BBS ban list file found, loaded from config or started empty")
except Exception as e:
logger.error(f"System: Error loading BBS ban list: {e}")
bbs_ban_list = []
# Merge loaded_list into bbs_ban_list, only adding new entries
for node in loaded_list:
if node not in bbs_ban_list:
bbs_ban_list.append(node)
def isNodeAdmin(nodeID):
# check if the nodeID is in the bbs_admin_list
@@ -907,6 +1017,7 @@ def handle_bbsban(message, message_from_id, isDM):
action = parts[1]
if action == "list":
load_bbsBanList() # Always reload from file for latest list
if bbs_ban_list:
return "BBS Ban List:\n" + "\n".join(bbs_ban_list)
else:
@@ -980,7 +1091,6 @@ def handleMultiPing(nodeID=0, deviceID=1):
# send the DM
send_message(f"🔂{count} {type}", channel_number, message_id_from, deviceID, bypassChuncking=True)
time.sleep(responseDelay + 1)
if count < 2:
# remove the item from the list
for j in range(len(multiPingList)):
@@ -1065,9 +1175,6 @@ def handleAlertBroadcast(deviceID=1):
else:
send_message(deAlert, emergencyAlertBroadcastCh, 0, deviceID)
return True
# pause for traffic
time.sleep(5)
if wxAlertBroadcastEnabled:
if wxAlert:
@@ -1081,9 +1188,6 @@ 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)
@@ -1104,12 +1208,13 @@ def onDisconnect(interface):
interface.close()
# Telemetry Functions
telemetryData = {}
localTelemetryData = {}
def initialize_telemetryData():
telemetryData[0] = {f'interface{i}': 0 for i in range(1, 10)}
telemetryData[0].update({f'lastAlert{i}': '' for i in range(1, 10)})
global localTelemetryData
localTelemetryData[0] = {f'interface{i}': 0 for i in range(1, 10)}
localTelemetryData[0].update({f'lastAlert{i}': '' for i in range(1, 10)})
for i in range(1, 10):
telemetryData[i] = {'numPacketsTx': 0, 'numPacketsRx': 0, 'numOnlineNodes': 0, 'numPacketsTxErr': 0, 'numPacketsRxErr': 0, 'numTotalNodes': 0}
localTelemetryData[i] = {'numPacketsTx': 0, 'numPacketsRx': 0, 'numOnlineNodes': 0, 'numPacketsTxErr': 0, 'numPacketsRxErr': 0, 'numTotalNodes': 0}
# indented to be called from the main loop
initialize_telemetryData()
@@ -1162,23 +1267,26 @@ def compileFavoriteList(getInterfaceIDs=True):
def displayNodeTelemetry(nodeID=0, rxNode=0, userRequested=False):
interface = globals()[f'interface{rxNode}']
myNodeNum = globals().get(f'myNodeNum{rxNode}')
global telemetryData
global localTelemetryData
# throttle the telemetry requests to prevent spamming the device
if 1 <= rxNode <= 9:
if time.time() - telemetryData[0][f'interface{rxNode}'] < 600 and not userRequested:
if time.time() - localTelemetryData[0][f'interface{rxNode}'] < 600 and not userRequested:
return -1
telemetryData[0][f'interface{rxNode}'] = time.time()
localTelemetryData[0][f'interface{rxNode}'] = time.time()
# some telemetry data is not available in python-meshtastic?
# bring in values from the last telemetry dump for the node
numPacketsTx = telemetryData[rxNode]['numPacketsTx']
numPacketsRx = telemetryData[rxNode]['numPacketsRx']
numPacketsTxErr = telemetryData[rxNode]['numPacketsTxErr']
numPacketsRxErr = telemetryData[rxNode]['numPacketsRxErr']
numTotalNodes = telemetryData[rxNode]['numTotalNodes']
totalOnlineNodes = telemetryData[rxNode]['numOnlineNodes']
numPacketsTx = localTelemetryData[rxNode].get('numPacketsTx', 0)
numPacketsRx = localTelemetryData[rxNode].get('numPacketsRx', 0)
numPacketsTxErr = localTelemetryData[rxNode].get('numPacketsTxErr', 0)
numPacketsRxErr = localTelemetryData[rxNode].get('numPacketsRxErr', 0)
numTotalNodes = localTelemetryData[rxNode].get('numTotalNodes', 0)
totalOnlineNodes = localTelemetryData[rxNode].get('numOnlineNodes', 0)
numRXDupes = localTelemetryData[rxNode].get('numRXDupes', 0)
numTxRelays = localTelemetryData[rxNode].get('numTxRelays', 0)
heapFreeBytes = localTelemetryData[rxNode].get('heapFreeBytes', 0)
heapTotalBytes = localTelemetryData[rxNode].get('heapTotalBytes', 0)
# get the telemetry data for a node
chutil = round(interface.nodes.get(decimal_to_hex(myNodeNum), {}).get("deviceMetrics", {}).get("channelUtilization", 0), 1)
airUtilTx = round(interface.nodes.get(decimal_to_hex(myNodeNum), {}).get("deviceMetrics", {}).get("airUtilTx", 0), 1)
@@ -1219,6 +1327,18 @@ def displayNodeTelemetry(nodeID=0, rxNode=0, userRequested=False):
send_message(f"Low Battery Level: {batteryLevel}{emji} on Device: {rxNode}", {secure_channel}, 0, {secure_interface})
elif batteryLevel < 10:
logger.critical(f"System: Critical Battery Level: {batteryLevel}{emji} on Device: {rxNode}")
# if numRXDupes,numTxRelays,heapFreeBytes,heapTotalBytes are available loge them
# if numRXDupes != 0:
# dataResponse += f" RXDupes:{numRXDupes}"
# logger.debug(f"System: Device {rxNode} RX Dupes:{numRXDupes}")
# if numTxRelays != 0:
# dataResponse += f" TxRelays:{numTxRelays}"
# logger.debug(f"System: Device {rxNode} TX Relays:{numTxRelays}")
# if heapFreeBytes != 0 and heapTotalBytes != 0:
# logger.debug(f"System: Device {rxNode} Heap Memory Free:{heapFreeBytes} Total:{heapTotalBytes}")
#dataResponse += f" Heap:{heapFreeBytes}/{heapTotalBytes}"
return dataResponse
positionMetadata = {}
@@ -1248,7 +1368,7 @@ def initializeMeshLeaderboard():
initializeMeshLeaderboard()
def consumeMetadata(packet, rxNode=0, channel=-1):
global positionMetadata, telemetryData, meshLeaderboard
global positionMetadata, localTelemetryData, meshLeaderboard
uptime = battery = temp = iaq = nodeID = 0
deviceMetrics, envMetrics, localStats = {}, {}, {}
@@ -1342,31 +1462,26 @@ def consumeMetadata(packet, rxNode=0, channel=-1):
except Exception as e:
logger.debug(f"System: TELEMETRY_APP iaq error: Device: {rxNode} Channel: {channel} {e} packet {packet}")
# Collect localStats for telemetryData
# Update localStats in telemetryData
if telemetry_packet.get('localStats'):
localStats = telemetry_packet['localStats']
try:
# Check if 'numPacketsTx' and 'numPacketsRx' exist and are not zero
if localStats.get('numPacketsTx') is not None and localStats.get('numPacketsRx') is not None and localStats['numPacketsTx'] != 0:
# Assign the values to the telemetry dictionary
keys = [
'numPacketsTx', 'numPacketsRx', 'numOnlineNodes',
'numOfflineNodes', 'numPacketsTxErr', 'numPacketsRxErr', 'numTotalNodes']
for key in keys:
if localStats.get(key) is not None:
telemetryData[rxNode][key] = localStats.get(key)
# Only store keys where value is not 0
filtered_stats = {k: v for k, v in localStats.items() if v != 0}
localTelemetryData[rxNode].update(filtered_stats)
except Exception as e:
logger.debug(f"System: TELEMETRY_APP localStats error: Device: {rxNode} Channel: {channel} {e} packet {packet}")
#POSITION_APP packets
if packet_type == 'POSITION_APP':
try:
if debugMetadata and 'POSITION_APP' not in metadataFilter:
print(f"DEBUG POSITION_APP: {packet}\n\n")
keys = ['altitude', 'groundSpeed', 'precisionBits']
position_stats_keys = ['altitude', 'groundSpeed', 'precisionBits']
position_data = packet['decoded']['position']
if nodeID not in positionMetadata:
positionMetadata[nodeID] = {}
for key in keys:
for key in position_stats_keys:
positionMetadata[nodeID][key] = position_data.get(key, 0)
# Track fastest speed 🚓
if position_data.get('groundSpeed') is not None:
@@ -1386,7 +1501,7 @@ def consumeMetadata(packet, rxNode=0, channel=-1):
if logMetaStats:
logger.info(f"System: 🚀 New altitude record: {altitude}m from NodeID:{nodeID} ShortName:{get_name_from_number(nodeID, 'short', rxNode)}")
# if altitude is over highfly_altitude 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:
if position_data.get('altitude', 0) > highfly_altitude and highfly_enabled and str(nodeID) not in highfly_ignoreList and not isNodeBanned(nodeID):
logger.info(f"System: High Altitude {position_data['altitude']}m on Device: {rxNode} Channel: {channel} NodeID:{nodeID} Lat:{position_data.get('latitude', 0)} Lon:{position_data.get('longitude', 0)}")
altFeet = round(position_data['altitude'] * 3.28084, 2)
msg = f"🚀 High Altitude Detected! NodeID:{nodeID} Alt:{altFeet:,.0f}ft/{position_data['altitude']:,.0f}m"
@@ -1394,12 +1509,21 @@ def consumeMetadata(packet, rxNode=0, channel=-1):
# check get_openskynetwork to see if the node is an aircraft
if 'latitude' in position_data and 'longitude' in position_data:
flight_info = get_openskynetwork(position_data.get('latitude', 0), position_data.get('longitude', 0))
if flight_info and NO_ALERTS not in flight_info and ERROR_FETCHING_DATA not in flight_info:
msg += f"\n✈️Detected near:\n{flight_info}"
# Only show plane if within altitude
if (
flight_info
and NO_ALERTS not in flight_info
and ERROR_FETCHING_DATA not in flight_info
and isinstance(flight_info, dict)
and 'altitude' in flight_info
):
plane_alt = flight_info['altitude']
node_alt = position_data.get('altitude', 0)
if abs(node_alt - plane_alt) <= 900: # within 900m
msg += f"\n✈️Detected near:\n{flight_info}"
send_message(msg, highfly_channel, 0, highfly_interface)
time.sleep(responseDelay)
# Keep the positionMetadata dictionary at a maximum size of 20
if len(positionMetadata) > 20:
# Keep the positionMetadata dictionary at a maximum size
if len(positionMetadata) > MAX_SEEN_NODES:
# Remove the oldest entry
oldest_nodeID = next(iter(positionMetadata))
del positionMetadata[oldest_nodeID]
@@ -1468,7 +1592,6 @@ def consumeMetadata(packet, rxNode=0, channel=-1):
logger.info(f"System: Detection Sensor Data from Device: {rxNode} Channel: {channel} NodeID:{nodeID} Text:{detction_text}")
if detctionSensorAlert:
send_message(f"🚨Detection Sensor from Device: {rxNode} Channel: {channel} NodeID:{get_name_from_number(nodeID,'long',rxNode)} Alert:{detction_text}", secure_channel, 0, secure_interface)
time.sleep(responseDelay)
except Exception as e:
logger.debug(f"System: DETECTION_SENSOR_APP decode error: Device: {rxNode} Channel: {channel} {e} packet {packet}")
@@ -1545,6 +1668,22 @@ def consumeMetadata(packet, rxNode=0, channel=-1):
# COMPRESSED_TEXT_APP
# ATTAK_APP
# SERIAL_APP
# NODE_DB_APP
# RTTTL_APP
# STORE_AND_FORWARD_APP
# DEBUG_APP
# RANGEREPORT_APP
# CENSUS_APP
# AUDIO_APP - Track audio/voice packets ☎️
if packet_type == 'AUDIO_APP':
try:
@@ -1740,7 +1879,7 @@ def get_sysinfo(nodeID=0, deviceID=1):
# Get the system telemetry data for return on the sysinfo command
sysinfo = ''
stats = str(displayNodeTelemetry(nodeID, deviceID, userRequested=True)) + " 🤖👀" + str(len(seenNodes))
if "numPacketsRx:0" in stats or stats == -1:
if "numPacketsTx:0" in stats or stats == -1:
return "Gathering Telemetry try again later⏳"
# replace Telemetry with Int in string
stats = stats.replace("Telemetry", "Int")
@@ -1769,13 +1908,11 @@ async def handleSignalWatcher():
for ch in sigWatchBroadcastCh:
if antiSpam and ch != publicChannel:
send_message(msg, int(ch), 0, sigWatchBroadcastInterface)
time.sleep(responseDelay)
else:
logger.warning(f"System: antiSpam prevented Alert from Hamlib {msg}")
else:
if antiSpam and sigWatchBroadcastCh != publicChannel:
send_message(msg, int(sigWatchBroadcastCh), 0, sigWatchBroadcastInterface)
time.sleep(responseDelay)
else:
logger.warning(f"System: antiSpam prevented Alert from Hamlib {msg}")
@@ -1798,23 +1935,19 @@ async def handleFileWatcher():
for ch in file_monitor_broadcastCh:
if antiSpam and int(ch) != publicChannel:
send_message(msg, int(ch), 0, 1)
time.sleep(responseDelay)
if multiple_interface:
for i in range(2, 10):
if globals().get(f'interface{i}_enabled'):
send_message(msg, int(ch), 0, i)
time.sleep(responseDelay)
else:
logger.warning(f"System: antiSpam prevented Alert from FileWatcher")
else:
if antiSpam and file_monitor_broadcastCh != publicChannel:
send_message(msg, int(file_monitor_broadcastCh), 0, 1)
time.sleep(responseDelay)
if multiple_interface:
for i in range(2, 10):
if globals().get(f'interface{i}_enabled'):
send_message(msg, int(file_monitor_broadcastCh), 0, i)
time.sleep(responseDelay)
else:
logger.warning(f"System: antiSpam prevented Alert from FileWatcher")
@@ -1874,7 +2007,7 @@ async def handleSentinel(deviceID):
global handleSentinel_spotted, handleSentinel_loop
detectedNearby = ""
resolution = "unknown"
closest_nodes = get_closest_nodes(deviceID)
closest_nodes = await get_closest_nodes(deviceID)
closest_node = closest_nodes[0]['id'] if closest_nodes != ERROR_FETCHING_DATA and closest_nodes else None
closest_distance = closest_nodes[0]['distance'] if closest_nodes != ERROR_FETCHING_DATA and closest_nodes else None
@@ -1904,7 +2037,6 @@ async def handleSentinel(deviceID):
logger.warning(f"System: {detectedNearby} is close to your location on Interface{deviceID} Accuracy is {resolution}bits")
send_message(f"Sentry{deviceID}: {detectedNearby}", secure_channel, 0, secure_interface)
time.sleep(responseDelay + 1)
if enableSMTP and email_sentry_alerts:
for email in sysopEmails:
send_email(email, f"Sentry{deviceID}: {detectedNearby}")
@@ -1925,10 +2057,9 @@ async def process_vox_queue():
for channel in sigWatchBroadcastCh:
if antiSpam and int(channel) != publicChannel:
send_message(message, int(channel), 0, sigWatchBroadcastInterface)
time.sleep(responseDelay)
async def watchdog():
global telemetryData, retry_int1, retry_int2, retry_int3, retry_int4, retry_int5, retry_int6, retry_int7, retry_int8, retry_int9
global localTelemetryData, retry_int1, retry_int2, retry_int3, retry_int4, retry_int5, retry_int6, retry_int7, retry_int8, retry_int9
logger.debug("System: Watchdog started")
while True:
await asyncio.sleep(20)
@@ -1941,14 +2072,15 @@ async def watchdog():
for i in range(1, 10):
interface = globals().get(f'interface{i}')
retry_int = globals().get(f'retry_int{i}')
if interface is not None and not retry_int and globals().get(f'interface{i}_enabled'):
int_enabled = globals().get(f'interface{i}_enabled')
if interface is not None and not retry_int and int_enabled:
try:
firmware = getNodeFirmware(0, i)
except Exception as e:
logger.error(f"System: communicating with interface{i}, trying to reconnect: {e}")
globals()[f'retry_int{i}'] = True
if not globals()[f'retry_int{i}']:
if not retry_int and int_enabled:
if sentry_enabled:
await handleSentinel(i)
@@ -1958,11 +2090,11 @@ async def watchdog():
handleAlertBroadcast(i)
intData = displayNodeTelemetry(0, i)
if intData != -1 and telemetryData[0][f'lastAlert{i}'] != intData:
if intData != -1 and localTelemetryData[0][f'lastAlert{i}'] != intData:
logger.debug(intData + f" Firmware:{firmware}")
telemetryData[0][f'lastAlert{i}'] = intData
localTelemetryData[0][f'lastAlert{i}'] = intData
if globals()[f'retry_int{i}'] and globals()[f'interface{i}_enabled']:
if retry_int and int_enabled:
try:
await retry_interface(i)
except Exception as e:
+126
View File
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# UDP Interface Listener
# credit to pdxlocations for all of this core work https://github.com/pdxlocations/
# depends on: pip install meshtastic protobuf zeroconf pubsub
# 2025 Kelly Keeton K7MHI
from pubsub import pub
from meshtastic.protobuf import mesh_pb2, portnums_pb2
from mudp import UDPPacketStream, node, conn, send_text_message, send_nodeinfo, send_device_telemetry, send_position, send_environment_metrics, send_power_metrics, send_waypoint, send_data
from mudp.encryption import generate_hash
import time
from zeroconf import Zeroconf, ServiceBrowser
import socket
MCAST_GRP, MCAST_PORT, CHANNEL_ID, KEY = "224.0.0.69", 4403, "LongFast", "1PG7OiApB1nwvP+rz05pAQ=="
PUBLIC_CHANNEL_IDS = ["LongFast", "ShortSlow", "MediumFast", "MediumSlow", "ShortFast", "ShortTurbo"]
mudpEnabled, mudpInterface = True, None
messages = []
class ZeroconfListner:
def add_service(self, zeroconf, type, name):
info = zeroconf.get_service_info(type, name)
if info:
txt = info.properties
ip = None
if info.addresses:
ip = socket.inet_ntoa(info.addresses[0])
print(f"Found Meshtastic node: id={txt.get(b'id', b'').decode()} shortname={txt.get(b'shortname', b'').decode()} longname={txt.get(b'longname', b'').decode()} ip={ip}")
def update_service(self, zeroconf, type, name):
# This method is required by zeroconf, but you can leave it empty if you don't need updates.
pass
def initalize_mudp():
global mudpInterface
if mudpEnabled and mudpInterface is None:
mudpInterface = UDPPacketStream(MCAST_GRP, MCAST_PORT, key=KEY)
print(f"MUDP Interface initialized with multicast group", MCAST_GRP, "port", MCAST_PORT)
node.node_id, node.long_name, node.short_name = "!deadbeef", "UDP Test", "UDP"
node.channel, node.key = "LongFast", KEY
conn.setup_multicast(MCAST_GRP, MCAST_PORT)
def on_recieve(packet: mesh_pb2.MeshPacket, addr=None):
print(f"\n[RECV] Packet received from {addr}")
print("from:", getattr(packet, "from", None))
print("to:", packet.to)
# Check against all public channels
matched_channel = None
for channel_name in PUBLIC_CHANNEL_IDS:
channel_hash = generate_hash(channel_name, KEY)
if packet.channel == channel_hash:
matched_channel = channel_name
break
if matched_channel:
channel_status = f"Match ({matched_channel})"
else:
channel_status = f"Hash: {packet.channel}"
print("channel:", channel_status)
if packet.HasField("decoded"):
port_name = portnums_pb2.PortNum.Name(packet.decoded.portnum) if packet.decoded.portnum else "N/A"
try:
payload_decoded = True
packet_payload = packet.decoded.payload.decode("utf-8", "ignore")
except Exception:
print(" payload (raw bytes):", packet.decoded.payload)
else:
print(f"encrypted: { {packet.encrypted} }")
print("id:", packet.id or None)
print("rx_time:", packet.rx_time or None)
print("rx_snr:", packet.rx_snr or None)
print("hop_limit:", packet.hop_limit or None)
priority_name = mesh_pb2.MeshPacket.Priority.Name(packet.priority) if packet.priority else "N/A"
print("priority:", priority_name or None)
print("rx_rssi:", packet.rx_rssi or None)
print("hop_start:", packet.hop_start or None)
print("next_hop:", packet.next_hop or None)
print("relay_node:", packet.relay_node or None)
print(f"decoded {{portnum: {port_name}, payload: {packet_payload if payload_decoded else 'N/A'}, bitfield: {packet.decoded.bitfield or None}}}" if packet.HasField("decoded") else "No decoded field")
pub.subscribe(on_recieve, "mesh.rx.packet")
# pub.subscribe(on_text_message, "mesh.rx.port.1")
# pub.subscribe(on_nodeinfo, "mesh.rx.port.4") # NODEINFO_APP
zeroconf = Zeroconf()
listener = ZeroconfListner()
browser = ServiceBrowser(zeroconf, "_meshtastic._tcp.local.", listener)
def main():
initalize_mudp()
mudpInterface.start()
try:
while True: time.sleep(0.05)
except KeyboardInterrupt: pass
finally: mudpInterface.stop()
if __name__ == "__main__":
main()
# Meshtastic Port Numbers Reference:
# | Port Number | Name | Purpose |
# |-------------|------------------------|--------------------------------|
# | 1 | TEXT_MESSAGE_APP | Text messages |
# | 2 | POSITION_APP | Position updates (GPS) |
# | 3 | ROUTING_APP | Routing info |
# | 4 | NODEINFO_APP | Node info (name, id, etc) |
# | 5 | TELEMETRY_APP | Telemetry (battery, sensors) |
# | 6 | SERIAL_APP | Serial data |
# | 7 | ENVIRONMENTAL_APP | Environmental sensors |
# | 8 | REMOTE_HARDWARE_APP | Remote hardware control |
# | 9 | STORE_FORWARD_APP | Store and forward |
# | 10 | RANGE_TEST_APP | Range test |
# | 11 | ADMIN_APP | Admin/config |
# | 12 | WAYPOINT_APP | Waypoints |
# | 13 | CHANNEL_NODEINFO_APP | Channel node info |
# | 256 | PRIVATE_APP | Private app (custom use) |
# See: https://github.com/meshtastic/protobufs/blob/main/meshtastic/protobuf/portnums.proto
+57 -33
View File
@@ -267,7 +267,23 @@ def onReceive(packet, interface):
# check if the packet has a channel flag use it
if packet.get('channel'):
channel_number = packet.get('channel', 0)
channel_number = packet.get('channel')
channel_name = "unknown"
# get channel hashes for the interface
device = next((d for d in channel_list if d["interface_id"] == rxNode), None)
if device:
# Find the channel name whose hash matches channel_number
for chan_name, info in device['channels'].items():
if info['hash'] == channel_number:
print(f"Matched channel hash {info['hash']} to channel name {chan_name}")
channel_name = chan_name
break
# check if the packet has a simulator flag
simulator_flag = packet.get('decoded', {}).get('simulator', False)
if isinstance(simulator_flag, dict):
# assume Software Simulator
simulator_flag = True
# set the message_from_id
message_from_id = packet['from']
@@ -282,6 +298,7 @@ def onReceive(packet, interface):
message_bytes = packet['decoded']['payload']
message_string = message_bytes.decode('utf-8')
via_mqtt = packet['decoded'].get('viaMqtt', False)
transport_mechanism = packet['decoded'].get('transport_mechanism', 'unknown')
# check if the packet is from us
if message_from_id == myNodeNum1 or message_from_id == myNodeNum2:
@@ -294,46 +311,54 @@ def onReceive(packet, interface):
# check if the packet has a publicKey flag use it
if packet.get('publicKey'):
pkiStatus = (packet.get('pkiEncrypted', False), packet.get('publicKey', 'ABC'))
pkiStatus = packet.get('pkiEncrypted', False), packet.get('publicKey', 'ABC')
# check if the packet has replyId flag // currently unused in the code
if packet.get('replyId'):
replyIDset = packet.get('replyId', False)
# check if the packet has emoji flag set it // currently unused in the code
if packet.get('emoji'):
emojiSeen = packet.get('emoji', False)
# check if the packet has a hop count flag use it
if packet.get('hopsAway'):
hop_away = packet.get('hopsAway', 0)
else:
# if the packet does not have a hop count try other methods
if packet.get('hopLimit'):
hop_limit = packet.get('hopLimit', 0)
else:
hop_limit = 0
if packet.get('hopStart'):
hop_start = packet.get('hopStart', 0)
else:
hop_start = 0
if enableHopLogs:
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start}")
if packet.get('hopStart'):
hop_start = packet.get('hopStart', 0)
if packet.get('hopLimit'):
hop_limit = packet.get('hopLimit', 0)
# calculate hop count
hop = ""
if hop_limit > 0 and hop_start >= hop_limit:
hop_count = hop_away + (hop_start - hop_limit)
elif hop_limit > 0 and hop_start < hop_limit:
hop_count = hop_away + (hop_limit - hop_start)
else:
hop_count = hop_away
if hop_away == 0 and hop_limit == 0 and hop_start == 0:
hop = "Last Hop"
hop_count = 0
if hop_start == hop_limit:
hop = "Direct"
hop_count = 0
elif hop_start == 0 and hop_limit > 0 or via_mqtt:
hop = "MQTT"
hop_count = 0
else:
# set hop to Direct if the message was sent directly otherwise set the hop count
if hop_away > 0:
hop_count = hop_away
else:
hop_count = hop_start - hop_limit
#print (f"calculated hop count: {hop_start} - {hop_limit} = {hop_count}")
hop = f"{hop_count} hops"
if hop_start == hop_limit and "lora" in str(transport_mechanism).lower():
hop = "Direct"
if ((hop_start == 0 and hop_limit >= 0) or via_mqtt or ("mqtt" in str(transport_mechanism).lower())):
hop = "MQTT"
if "unknown" in str(transport_mechanism).lower() and (snr == 0 and rssi == 0):
hop = "IP-Network"
if enableHopLogs:
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start} calculated_hop_count:{hop_count} final_hop_value:{hop} via_mqtt:{via_mqtt} transport_mechanism:{transport_mechanism}")
# check with stringSafeChecker if the message is safe
if stringSafeCheck(message_string) is False:
logger.warning(f"System: Possibly Unsafe Message from {get_name_from_number(message_from_id, 'long', rxNode)}")
if help_message in message_string or welcome_message in message_string or "CMD?:" in message_string:
# ignore help and welcome messages
logger.warning(f"Got Own Welcome/Help header. From: {get_name_from_number(message_from_id, 'long', rxNode)}")
@@ -353,7 +378,6 @@ def onReceive(packet, interface):
else:
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
send_message(welcome_message, channel_number, message_from_id, rxNode)
time.sleep(responseDelay)
# log the message to the message log
if log_messages_to_file:
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Configuration Merge Script
# Merges user configuration with default settings
# 2025 Kelly Keeton K7MHI mesh-around and its meshtastic
import shutil
import configparser
import os
def merge_configs(default_config_path, user_config_path, output_config_path):
# Load default configuration (INI)
default_config = configparser.ConfigParser()
default_config.read(default_config_path)
# Load user configuration (INI)
user_config = configparser.ConfigParser()
user_config.read(user_config_path)
# Merge configurations
for section in user_config.sections():
if not default_config.has_section(section):
default_config.add_section(section)
for key, value in user_config.items(section):
default_config.set(section, key, value)
# Save merged configuration as INI
with open(output_config_path, 'w', encoding='utf-8') as f:
default_config.write(f)
def backup_config(config_path, backup_path):
shutil.copyfile(config_path, backup_path)
def show_config_changes(user_config_path, merged_config_path):
if not os.path.exists(merged_config_path) or os.path.getsize(merged_config_path) == 0:
print(f"Error: {merged_config_path} is empty or missing!")
return
# Load user config (as dict)
user_config = configparser.ConfigParser()
user_config.read(user_config_path)
user_dict = {s: dict(user_config.items(s)) for s in user_config.sections()}
# Load merged config (as dict)
merged_config = configparser.ConfigParser()
merged_config.read(merged_config_path)
merged_dict = {s: dict(merged_config.items(s)) for s in merged_config.sections()}
print("\n--- Changes in merged configuration ---")
for section in merged_dict:
if section not in user_dict:
print(f"[{section}] (new section)")
for k, v in merged_dict[section].items():
print(f" {k} = {v} (added)")
else:
for k, v in merged_dict[section].items():
if k not in user_dict[section]:
print(f"[{section}] {k} = {v} (added)")
elif user_dict[section][k] != v:
print(f"[{section}] {k}: {user_dict[section][k]} -> {v} (changed)")
print("--- End of changes ---\n")
if __name__ == "__main__":
print("MESHING-AROUND: Configuration Merge Script for config.ini checking updates from config.template")
print("---------------------------------------------------------------")
master_config_path = 'config.template'
user_config_path = 'config.ini'
output_config = 'config_new.ini'
backup_config_path = 'config.bak'
# Step 1: Check master config
try:
if not os.path.exists(master_config_path) or os.path.getsize(master_config_path) == 0:
raise FileNotFoundError(f"Master configuration file {master_config_path} is missing or empty.")
except Exception as e:
print(f"Error: {e}")
print("Run the tool from the meshing-around/script/ directory where the config.template is located.")
print(" python3 script/configMerge.py")
exit(1)
# Step 2: Backup user config
try:
backup_config(user_config_path, backup_config_path)
print(f"Backup of user config created at {backup_config_path}")
except Exception as e:
print(f"Error backing up user config: {e}")
exit(1)
# Step 3: Merge configs
try:
merge_configs(master_config_path, user_config_path, output_config)
print(f"Merged configuration saved to {output_config}")
except Exception as e:
print(f"Error merging configuration: {e}")
exit(1)
# Step 4: Show changes
try:
show_config_changes(user_config_path, output_config)
print("Please review the new configuration and replace your existing config.ini if needed.")
print(" cp config_new.ini config.ini")
except Exception as e:
print(f"Error showing configuration changes: {e}")
exit(1)
+29 -13
View File
@@ -24,6 +24,13 @@ if systemctl is-active --quiet mesh_bot_w3.service; then
service_stopped=true
fi
# Fetch latest changes from GitHub
echo "Fetching latest changes from GitHub..."
if ! git fetch origin; then
echo "Error: Failed to fetch from GitHub, check your network connection."
exit 1
fi
# git pull with rebase to avoid unnecessary merge commits
echo "Pulling latest changes from GitHub..."
if ! git pull origin main --rebase; then
@@ -37,22 +44,31 @@ if ! git pull origin main --rebase; then
fi
fi
# Install or update dependencies
echo "Installing or updating dependencies..."
if pip install -r requirements.txt --upgrade 2>&1 | grep -q "externally-managed-environment"; then
# if venv is found ask to run with launch.sh
if [ -d "venv" ]; then
echo "A virtual environment (venv) was found. run from inside venv"
# Backup the data/ directory
echo "Backing up data/ directory..."
#backup_file="backup_$(date +%Y%m%d_%H%M%S).tar.gz"
backup_file="data_backup.tar.gz"
path2backup="data/"
tar -czf "$backup_file" "$path2backup"
if [ $? -ne 0 ]; then
echo "Error: Backup failed."
else
echo "Backup of ${path2backup} completed: ${backup_file}"
fi
# Build a config_new.ini file merging user config with new defaults
echo "Merging configuration files..."
python3 script/configMerge.py > ini_merge_log.txt 2>&1
if [ -f ini_merge_log.txt ]; then
if grep -q "Error during configuration merge" ini_merge_log.txt; then
echo "Configuration merge encountered errors. Please check ini_merge_log.txt for details."
else
read -p "Warning: You are in an externally managed environment. Do you want to continue with --break-system-packages? (y/n): " choice
if [[ "$choice" == "y" || "$choice" == "Y" ]]; then
pip install --break-system-packages -r requirements.txt --upgrade
else
echo "Update aborted due to dependency installation issue."
fi
echo "Configuration merge completed. Please review config_new.ini and ini_merge_log.txt."
fi
else
echo "Dependencies installed or updated."
echo "Configuration merge log (ini_merge_log.txt) not found. check out the script/configMerge.py tool!"
fi
# if service was stopped earlier, restart it