mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-03-28 17:32:36 +01:00
Compare commits
214 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8b2aefa28 | ||
|
|
849565cacb | ||
|
|
fdec3a6754 | ||
|
|
f3a97bc567 | ||
|
|
02625ad0f2 | ||
|
|
b4ba4b0daf | ||
|
|
fd7f8a94f5 | ||
|
|
d252250edd | ||
|
|
d6410e0461 | ||
|
|
050b4ab3ce | ||
|
|
8ac1a1eed7 | ||
|
|
370a417ce6 | ||
|
|
378b05df35 | ||
|
|
d002c5ede8 | ||
|
|
cd03cc56b4 | ||
|
|
d4fd484706 | ||
|
|
82d519279e | ||
|
|
09302e8c91 | ||
|
|
91fc4605ec | ||
|
|
abc6c07ee3 | ||
|
|
bbf8b04bd3 | ||
|
|
5fd293c990 | ||
|
|
a9a65a6c6d | ||
|
|
34e95c86d6 | ||
|
|
80891090c3 | ||
|
|
1b098fbf7b | ||
|
|
165d76cf8d | ||
|
|
045c9d433b | ||
|
|
fbe5e008de | ||
|
|
004adc7d9a | ||
|
|
9a2033452f | ||
|
|
5638204f82 | ||
|
|
e5c3b0cceb | ||
|
|
18ac53b230 | ||
|
|
4aa65dad6a | ||
|
|
f65a7b7934 | ||
|
|
886293087a | ||
|
|
e05e6f3451 | ||
|
|
4b5dd934e9 | ||
|
|
008ddfb5a2 | ||
|
|
e1330b9b9e | ||
|
|
7b43213094 | ||
|
|
b17c2b17ee | ||
|
|
b6505ee577 | ||
|
|
f7379b7ca5 | ||
|
|
ec9a1d88db | ||
|
|
a339570afe | ||
|
|
f5af9f419a | ||
|
|
ad5c1c90da | ||
|
|
eb1e0c82ea | ||
|
|
8d2277bc59 | ||
|
|
dc9908a72c | ||
|
|
21123d2993 | ||
|
|
7dc3134d0b | ||
|
|
b125178492 | ||
|
|
2ad9e84c33 | ||
|
|
63bd288caa | ||
|
|
5c7d199831 | ||
|
|
f56a39eeb6 | ||
|
|
ae5991ee39 | ||
|
|
1324f83f17 | ||
|
|
08ae8c31a0 | ||
|
|
957e803951 | ||
|
|
2de3441d67 | ||
|
|
2b420022f9 | ||
|
|
d01f143adf | ||
|
|
5f5aeeadac | ||
|
|
d57826613c | ||
|
|
af09dc0cf9 | ||
|
|
011bac41f2 | ||
|
|
20467ea886 | ||
|
|
bbfd71f011 | ||
|
|
e1ff87a197 | ||
|
|
a859f830bb | ||
|
|
d99698e7f3 | ||
|
|
5ecc563e96 | ||
|
|
eeeb43cacc | ||
|
|
9fdcea56fc | ||
|
|
24a33fe882 | ||
|
|
5710cebf39 | ||
|
|
b66487863d | ||
|
|
b3c4d208b7 | ||
|
|
f41ff2d5f7 | ||
|
|
48366bc595 | ||
|
|
02dd64382d | ||
|
|
731b48ad65 | ||
|
|
69a7082669 | ||
|
|
fafa7d8a51 | ||
|
|
6e69b5f014 | ||
|
|
03895248cd | ||
|
|
a79de8a325 | ||
|
|
740b53f02f | ||
|
|
76e75551c6 | ||
|
|
51752ae896 | ||
|
|
d81e773c0c | ||
|
|
1f1ed1ca70 | ||
|
|
df9f3806a3 | ||
|
|
081ccd9e2e | ||
|
|
d9a7dafe6e | ||
|
|
921225965b | ||
|
|
3659254785 | ||
|
|
7c502608f6 | ||
|
|
427c25f80b | ||
|
|
c3f15390ea | ||
|
|
e1476a44c6 | ||
|
|
72070fef3e | ||
|
|
b63ea677f6 | ||
|
|
f8389500b8 | ||
|
|
b257625a45 | ||
|
|
a233d8c7b3 | ||
|
|
11c9742ebe | ||
|
|
5af28c3dc2 | ||
|
|
aebb9e3c20 | ||
|
|
d5916f4ccc | ||
|
|
056159a3f3 | ||
|
|
2f6049d94b | ||
|
|
a2d7f664ab | ||
|
|
b26491b646 | ||
|
|
22e97b0eec | ||
|
|
f540866d08 | ||
|
|
c9729c8214 | ||
|
|
49901cbbee | ||
|
|
2aa2b80935 | ||
|
|
95695f4f58 | ||
|
|
b641d2b5e8 | ||
|
|
51d8faab12 | ||
|
|
7a1396b99d | ||
|
|
819bbbcaf4 | ||
|
|
0eeda96670 | ||
|
|
18cca4ffdd | ||
|
|
d169fe2dff | ||
|
|
1c732dfe17 | ||
|
|
bdad3927e5 | ||
|
|
0e0d6416d9 | ||
|
|
0da780371a | ||
|
|
37bf30cbc0 | ||
|
|
817a8601dd | ||
|
|
47cca409be | ||
|
|
e08a82ec39 | ||
|
|
345541dfb5 | ||
|
|
6e89762f1d | ||
|
|
0fb26bc16a | ||
|
|
f1ad5966af | ||
|
|
ac57d4683f | ||
|
|
eab099e5ee | ||
|
|
685bd3491d | ||
|
|
b8d64f3a9e | ||
|
|
852d491030 | ||
|
|
76565c5546 | ||
|
|
af1ec1630e | ||
|
|
0c2b36a206 | ||
|
|
c0934096f0 | ||
|
|
819bfaba90 | ||
|
|
8041a1296b | ||
|
|
10d93b4fd3 | ||
|
|
19dedef1e6 | ||
|
|
d4af0c7e8b | ||
|
|
8730f0fd38 | ||
|
|
9cda8daf65 | ||
|
|
a9223f1613 | ||
|
|
04ca4c99b8 | ||
|
|
3072520e63 | ||
|
|
bd6603766b | ||
|
|
075a23bd2b | ||
|
|
a8e4f653ed | ||
|
|
374a44f4a9 | ||
|
|
3c8d2e646e | ||
|
|
e5df983244 | ||
|
|
fa5f9250c4 | ||
|
|
3f7a831690 | ||
|
|
89aaaddae9 | ||
|
|
e1919616c2 | ||
|
|
8b9e637006 | ||
|
|
0df3e32901 | ||
|
|
1c2fa174ea | ||
|
|
c97aefcef1 | ||
|
|
dfb94c3993 | ||
|
|
7d62f69f12 | ||
|
|
cf896767fb | ||
|
|
1eb4cf71ed | ||
|
|
e959124eac | ||
|
|
d787c72812 | ||
|
|
9f0dd56d43 | ||
|
|
aa71e6045a | ||
|
|
a140ad83cd | ||
|
|
93c2d731e8 | ||
|
|
d8da553af9 | ||
|
|
9d9f070908 | ||
|
|
0f2061af55 | ||
|
|
d8423584d4 | ||
|
|
843320d268 | ||
|
|
216128b15a | ||
|
|
f8bc574753 | ||
|
|
6193c5933f | ||
|
|
b668965bda | ||
|
|
ae039b5baf | ||
|
|
824d43f16e | ||
|
|
2de76e6c5e | ||
|
|
afb02602fd | ||
|
|
99528c2bcf | ||
|
|
b53f5821f3 | ||
|
|
93fc6547b8 | ||
|
|
9a7e321dff | ||
|
|
39257f2d39 | ||
|
|
8c5abecac3 | ||
|
|
16dcc96037 | ||
|
|
b1d32a7745 | ||
|
|
631a2f53ea | ||
|
|
32903c97e3 | ||
|
|
6e61e8122d | ||
|
|
d109803f9d | ||
|
|
09ed4f57cf | ||
|
|
acfb8078a9 | ||
|
|
84f9693833 |
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
18
.github/workflows/greetings.yml
vendored
Normal file
18
.github/workflows/greetings.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: Greetings
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
greeting:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/first-interaction@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue_message: "Dependabot's first issue"
|
||||
36
README.md
36
README.md
@@ -2,6 +2,8 @@
|
||||
|
||||
Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance your [Meshtastic](https://meshtastic.org/docs/introduction/) network experience with a variety of powerful tools and fun features, connectivity and utility through text-based message delivery. Whether you're looking to perform network tests, send messages, or even play games, [mesh_bot.py](mesh_bot.py) has you covered.
|
||||
|
||||
TLDR: [Getting Started](#getting-started)
|
||||
|
||||

|
||||
|
||||
## Key Features
|
||||
@@ -23,10 +25,9 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **Flexible Messaging**: send mail and messages, between networks.
|
||||
|
||||
### Advanced Messaging Capabilities
|
||||
- **Mail Messaging**: Leave messages for other devices, which are sent as DMs when the device is seen.
|
||||
- **Mail Messaging**: Leave messages for other devices, which are sent as DMs when the device is seen. Send mail to nodes using `bbspost @nodeNumber #message` or `bbspost @nodeShortName #message`.
|
||||
- **Scheduler**: Schedule messages like weather updates or reminders for weekly VHF nets.
|
||||
- **Store and Forward**: Replay messages with the `messages` command, and log messages locally to disk.
|
||||
- **Send Mail**: Send mail to nodes using `bbspost @nodeNumber #message` or `bbspost @nodeShortName #message`.
|
||||
- **Store and Forward**: Like voicemail, see messages missed with the `messages` command. Can also log messages locally to disk.
|
||||
- **BBS Linking**: Combine multiple bots to expand BBS reach.
|
||||
- **E-Mail/SMS**: Send mesh-messages to E-Mail or SMS(Email) expanding visibility.
|
||||
- **New Node Hello**: Send a hello to any new node seen in text message.
|
||||
@@ -39,9 +40,17 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **GeoMeasuring**: HowFar from point to point using collected GPS packets on the bot to plot a course or space. Find Center of points for Fox&Hound direction finding.
|
||||
|
||||
### Proximity Alerts
|
||||
- **Location-Based Alerts**: Get notified when members arrive back at a configured lat/long, perfect for remote locations like campsites.
|
||||
- **Location-Based Alerts**: Get notified when members arrive back at a configured lat/long, perfect for remote locations like campsites, or put a geo-fence. You can also run a script or send a email. Another idea is to lower the cycle and use the bot as a 'king of the hill' or 🧭geocache game. You can also run a script to change a node config or turn on the lights🚥, have it drop an alert.txt to send a message like "Hello Start the 📊Survey"
|
||||
- **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.
|
||||
@@ -189,7 +198,7 @@ Players can `q: join` to join the game, `q: leave` to leave the game, `q: score`
|
||||
To Answer a question, just type the answer prefixed with `q: <answer>`
|
||||
|
||||
#### Survey
|
||||
To use the Survey feature edit the json files in data/survey multiple surveys are possible such as `survey snow`
|
||||
To use the Survey feature edit the json files in data/survey multiple surveys are possible such as `survey snow` you can pull data back with `survey report` or `survey report snow`
|
||||
|
||||
## Other Install Options
|
||||
|
||||
@@ -221,7 +230,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]
|
||||
@@ -461,7 +470,7 @@ broadcastCh = 2 # channel to send the message to can be 2,3 multiple channels co
|
||||
enable_read_news = False # news command will return the contents of a text file
|
||||
news_file_path = news.txt
|
||||
news_random_line = False # only return a single random line from the news file
|
||||
enable_runShellCmd = False # enable the use of exernal shell commands, this enables some data in `sysinfo`
|
||||
enable_runShellCmd = False # enable the use of exernal shell commands, this enables more data in `sysinfo` DM
|
||||
# if runShellCmd and you think it is safe to allow the x: command to run
|
||||
# direct shell command handler the x: command in DMs user must be in bbs_admin_list
|
||||
allowXcmd = True
|
||||
@@ -514,11 +523,13 @@ enabled = False # enable or disable the scheduler module
|
||||
interface = 1 # channel to send the message to
|
||||
channel = 2
|
||||
message = "MeshBot says Hello! DM for more info."
|
||||
value = # value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun
|
||||
value = # value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun.
|
||||
# value can also be joke (everyXmin) or weather (hour) for special scheduled messages
|
||||
# custom for module/scheduler.py custom schedule examples
|
||||
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
|
||||
@@ -529,7 +540,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))
|
||||
@@ -587,7 +598,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
|
||||
|
||||
14
SECURITY.md
Normal file
14
SECURITY.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| git pull| :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If its serious, its likley big. otherwise post issues, reachout on discord.
|
||||
@@ -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,19 +127,28 @@ 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
|
||||
# device interface and channel to send the alert message to
|
||||
SentryInterface = 1
|
||||
SentryChannel = 2
|
||||
# holdoff time multiplied by seconds(20) of the watchdog
|
||||
SentryHoldoff = 9
|
||||
emailSentryAlerts = False
|
||||
# Enable detection sensor alert, requires external GPIO sensor connected to node
|
||||
detectionSensorAlert = False
|
||||
|
||||
# list of ignored nodes numbers ex: 2813308004,4258675309
|
||||
sentryIgnoreList =
|
||||
# Enable detection sensor alert, requires external sensor connected to node
|
||||
detectionSensorAlert = False
|
||||
# list of watched nodes numbers ex: 2813308004,4258675309
|
||||
sentryWatchList =
|
||||
|
||||
# radius in meters to detect someone close to the bot
|
||||
SentryRadius = 100
|
||||
# holdoff time multiplied by seconds(20) of the watchdog
|
||||
SentryHoldoff = 9
|
||||
|
||||
# Enable running external shell command when sentry alert is triggered
|
||||
cmdShellSentryAlerts = False
|
||||
# External shell command to run when sentry alert is triggered
|
||||
sentryAlertNear = sentry_alert_near.sh
|
||||
sentryAlertAway = sentry_alert_away.sh
|
||||
|
||||
# HighFlying Node alert
|
||||
highFlyingAlert = True
|
||||
@@ -239,7 +248,7 @@ enableDEalerts = False
|
||||
myRegionalKeysDE = 110000000000,120510000000
|
||||
|
||||
# Satalite Pass Prediction
|
||||
# Register for free API https://www.n2yo.com/login/
|
||||
# Register for free API https://www.n2yo.com/login/ personal data page at bottom 'Are you developer?'
|
||||
n2yoAPIKey =
|
||||
# NORAD list https://www.n2yo.com/satellites/
|
||||
satList = 25544,7530
|
||||
@@ -277,7 +286,9 @@ 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.
|
||||
# value can also be joke (everyXmin) or weather (hour) for special scheduled messages
|
||||
# custom for module/scheduler.py custom schedule examples
|
||||
value =
|
||||
# interval to use when time is not set (e.g. every 2 days)
|
||||
interval =
|
||||
@@ -308,6 +319,7 @@ voxLanguage = en-us
|
||||
voxInputDevice = default
|
||||
voxOnTrapList = True
|
||||
voxTrapList = chirpy
|
||||
voxEnableCmd = True
|
||||
|
||||
|
||||
[fileMon]
|
||||
@@ -377,8 +389,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
|
||||
@@ -393,7 +407,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
|
||||
|
||||
@@ -14,6 +14,8 @@ Group=pi
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=python3 mesh_bot.py
|
||||
ExecStop=pkill -f mesh_bot.py
|
||||
Environment=REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
|
||||
Environment=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
# Disable Python's buffering of STDOUT and STDERR, so that output from the
|
||||
# service shows up immediately in systemd's logs
|
||||
|
||||
@@ -14,6 +14,8 @@ Group=pi
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=python3 modules/web.py
|
||||
ExecStop=pkill -f mesh_bot_w3.py
|
||||
Environment=REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
|
||||
Environment=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
# Disable Python's buffering of STDOUT and STDERR, so that output from the
|
||||
# service shows up immediately in systemd's logs
|
||||
@@ -21,3 +23,6 @@ Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
Restart=on-failure
|
||||
Type=notify #try simple if any problems
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
|
||||
@@ -14,6 +14,8 @@ Group=pi
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=python3 pong_bot.py
|
||||
ExecStop=pkill -f pong_bot.py
|
||||
Environment=REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
|
||||
Environment=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
# Disable Python's buffering of STDOUT and STDERR, so that output from the
|
||||
# service shows up immediately in systemd's logs
|
||||
|
||||
16
install.sh
16
install.sh
@@ -13,6 +13,12 @@ printf "Installer works best in raspian/debian/ubuntu or foxbuntu embedded syste
|
||||
printf "If there is a problem, try running the installer again.\n"
|
||||
printf "\nChecking for dependencies...\n"
|
||||
|
||||
# fuse
|
||||
fi [[ -f config.ini ]]; then
|
||||
printf "\nDetected existing installation, please backup and remove existing installation before proceeding\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# check if we are in /opt/meshing-around
|
||||
if [ $program_path != "/opt/meshing-around" ]; then
|
||||
printf "\nIt is suggested to project path to /opt/meshing-around\n"
|
||||
@@ -207,6 +213,12 @@ sudo chown -R $whoami:$whoami $program_path/logs
|
||||
sudo chown -R $whoami:$whoami $program_path/data
|
||||
echo "Permissions set for meshbot on logs and data directories"
|
||||
|
||||
# check and see if some sort of NTP is running
|
||||
if ! systemctl is-active --quiet ntp.service && \
|
||||
! systemctl is-active --quiet systemd-timesyncd.service && \
|
||||
! systemctl is-active --quiet chronyd.service; then
|
||||
printf "\nNo NTP service detected, it is recommended to have NTP running for proper bot operation.\n"
|
||||
|
||||
# set the correct user in the service file
|
||||
replace="s|User=pi|User=$whoami|g"
|
||||
sed -i $replace etc/pong_bot.service
|
||||
@@ -287,7 +299,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
|
||||
@@ -299,6 +311,7 @@ if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
|
||||
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
|
||||
printf "Reporting chron job added to run report_generator5.py\n" >> install_notes.txt
|
||||
printf "chronjob: %s\n" "$chronjob" >> install_notes.txt
|
||||
printf "*** Stay Up to date using 'bash update.sh' ***\n" >> install_notes.txt
|
||||
|
||||
if [[ $(echo "${venv}" | grep -i "^y") ]]; then
|
||||
printf "\nFor running on venv, virtual launch bot with './launch.sh mesh' in path $program_path\n" >> install_notes.txt
|
||||
@@ -344,6 +357,7 @@ else
|
||||
printf "sudo journalctl -u %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl stop %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
|
||||
printf "*** Stay Up to date using 'bash update.sh' ***\n" >> install_notes.txt
|
||||
fi
|
||||
|
||||
printf "\nInstallation complete!\n"
|
||||
|
||||
688
mesh_bot.py
688
mesh_bot.py
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,13 @@
|
||||
import pickle # pip install pickle
|
||||
from modules.log import *
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
useSynchCompression = False
|
||||
|
||||
if useSynchCompression:
|
||||
import zlib
|
||||
from modules.system import send_raw_bytes
|
||||
|
||||
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp", "bbsinfo", "bbslink", "bbsack")
|
||||
|
||||
@@ -11,6 +18,7 @@ trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp", "bbsin
|
||||
bbs_messages = []
|
||||
bbs_dm = []
|
||||
|
||||
|
||||
def load_bbsdb():
|
||||
global bbs_messages
|
||||
# load the bbs messages from the database file
|
||||
@@ -90,7 +98,7 @@ def bbs_delete_message(messageID = 0, fromNode = 0):
|
||||
|
||||
def bbs_post_message(subject, message, fromNode, threadID=0, replytoID=0):
|
||||
# post a message to the bbsdb
|
||||
now = today.strftime('%Y-%m-%d %H:%M:%S')
|
||||
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
thread = threadID
|
||||
replyto = replytoID
|
||||
# post a message to the bbsdb and assign a messageID
|
||||
@@ -201,6 +209,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 +279,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))
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import asyncio
|
||||
import random
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
trap_list_filemon = ("readnews",)
|
||||
|
||||
@@ -69,28 +70,33 @@ async def watch_file():
|
||||
return content
|
||||
await asyncio.sleep(1) # Check every
|
||||
|
||||
def call_external_script(message, script="script/runShell.sh"):
|
||||
# Call an external script with the message as an argument this is a example only
|
||||
def call_external_script(message, script="runShell.sh"):
|
||||
# If no path is given, assume script/ directory
|
||||
if "/" not in script and "\\" not in script:
|
||||
script = os.path.join("script", script)
|
||||
try:
|
||||
current_working_directory = os.getcwd()
|
||||
script_path = os.path.join(current_working_directory, script)
|
||||
|
||||
if not os.path.exists(script_path):
|
||||
# try the raw script name
|
||||
# Try the raw script name
|
||||
script_path = script
|
||||
if not os.path.exists(script_path):
|
||||
logger.warning(f"FileMon: Script not found: {script_path}")
|
||||
return "sorry I can't do that"
|
||||
|
||||
# Use subprocess.run for better resource management
|
||||
result = subprocess.run(
|
||||
["bash", script_path, message],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.error(f"FileMon: Script error: {result.stderr.strip()}")
|
||||
return None
|
||||
|
||||
output = result.stdout.strip()
|
||||
return output
|
||||
return output if output else None
|
||||
except Exception as e:
|
||||
logger.warning(f"FileMon: Error calling external script: {e}")
|
||||
return None
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -46,15 +46,27 @@ lameJokes = [
|
||||
"Chuck Norris can kill two stones with one bird.",
|
||||
"Chuck Norris can speak braille.",
|
||||
"Chuck Norris can build a snowman out of rain.",
|
||||
"Chuck Norris can hear sign language.",
|
||||
"Death once had a near-Chuck Norris experience.",
|
||||
"Chuck Norris can unscramble an egg.",
|
||||
"Chuck Norris can win a game of Connect Four in only three moves.",
|
||||
"Chuck Norris can make a snowman out of rain.",
|
||||
"Chuck Norris can strangle you with a cordless phone.",
|
||||
"Chuck Norris can do a wheelie on a unicycle.",
|
||||
"Chuck Norris can kill two stones with one bird."]
|
||||
"This is a test. A test of the Joke Brodcast System. If this had been an actual joke, you would have been amused.",
|
||||
"Chuck Norris doesn't join mesh networks. Mesh networks join Chuck's topology.",
|
||||
"Every time Chuck Norris sends a packet, it arrives before he hits 'send'",
|
||||
"Chuck Norris doesn't need LoRa. His roundhouse kick has a 15km range with zero latency.",
|
||||
"When Chuck Norris uses a node, the bandwidth doubles out of fear.",
|
||||
"Chuck Norris once pinged a device. It replied with an apology and a firmware update.",
|
||||
"Chuck Norris doesn't use AES encryption. His packets are so secure, they punch hackers in the bits.",
|
||||
"The Meshtastic protocol has a hidden mode: “Chuck Norris mode.” It only activates when he blinks.",
|
||||
"Chuck Norris doesn't need a GPS fix. Satellites triangulate themselves around him.",
|
||||
"Chuck Norris's mesh node doesn't sleep. It meditates while transmitting at full power.",
|
||||
"Chuck Norris doesn't broadcast. He declares.",
|
||||
"Chuck Norris once bridged two mesh networks using a shoelace.",
|
||||
"Chuck Norris's packets don't hop. They teleport out of respect.",
|
||||
"Chuck Norris doesn't need a repeater. Client_Mute is set to 'Always'.",
|
||||
"Chuck Norris's mesh messages are entangled. When he sends one, it's already received.",
|
||||
"Chuck Norris doesn't mesh with others. Others mesh with Chuck.",
|
||||
"Chuck Norris's node doesn't need a case. The PCB is armored with his beard hair.",
|
||||
"Chuck Norris once typed “Hello World” and the world replied 'Hello Chuck.'",
|
||||
]
|
||||
|
||||
# pylint: disable=C0103, W0612
|
||||
imtellingyourightnowiAmTellingYouRightNowThatMotherfErBackThereIsNotReal = ["🐦", "🦅", "🦆", "🦉", "🦜", "🐤", "🐥", "🐣", "🐔", "🐧", "🦚", "🦢", "🦩", "🦤", "🦃", "🐓"]
|
||||
|
||||
def tableOfContents():
|
||||
@@ -168,14 +180,14 @@ 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
|
||||
return renderedLaugh
|
||||
except Exception as e:
|
||||
return random.choice(lameJokes)
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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!"
|
||||
|
||||
179
modules/llm.py
179
modules/llm.py
@@ -8,6 +8,7 @@ from modules.log import *
|
||||
# https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
if not rawLLMQuery:
|
||||
# this may be removed in the future
|
||||
@@ -48,7 +49,7 @@ meshBotAI = """
|
||||
PROMPT
|
||||
{input}
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
if llmContext_fromGoogle:
|
||||
meshBotAI = meshBotAI + """
|
||||
@@ -76,6 +77,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 +246,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 +272,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 +287,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', ' ')
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import maidenhead as mh # pip install maidenhead
|
||||
import requests # pip install requests
|
||||
import bs4 as bs # pip install beautifulsoup4
|
||||
import xml.dom.minidom
|
||||
from datetime import datetime
|
||||
from modules.log import *
|
||||
import math
|
||||
|
||||
@@ -174,8 +175,8 @@ def get_NOAAtide(lat=0, lon=0):
|
||||
station_id = ""
|
||||
location = lat,lon
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
logger.error("Location:No GPS data, try sending location for tide")
|
||||
return NO_DATA_NOGPS
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
station_lookup_url = "https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/tidepredstations.json?lat=" + str(lat) + "&lon=" + str(lon) + "&radius=50"
|
||||
try:
|
||||
station_data = requests.get(station_lookup_url, timeout=urlTimeoutSeconds)
|
||||
@@ -239,7 +240,8 @@ def get_NOAAweather(lat=0, lon=0, unit=0):
|
||||
weather = ""
|
||||
location = lat,lon
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
return NO_DATA_NOGPS
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
|
||||
# get weather data from NOAA units for metric unit = 1 is metric
|
||||
if use_metric:
|
||||
@@ -295,9 +297,37 @@ 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",
|
||||
"gusts as high as": "gusts to",
|
||||
}
|
||||
# Single words (no spaces)
|
||||
word_replacements = {
|
||||
"monday": "Mon",
|
||||
"tuesday": "Tue",
|
||||
"wednesday": "Wed",
|
||||
@@ -313,6 +343,8 @@ def abbreviate_noaa(row):
|
||||
"south": "S",
|
||||
"east": "E",
|
||||
"west": "W",
|
||||
"accumulation": "accum",
|
||||
"visibility": "vis",
|
||||
"precipitation": "precip",
|
||||
"showers": "shwrs",
|
||||
"thunderstorms": "t-storms",
|
||||
@@ -334,28 +366,37 @@ 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",
|
||||
"around": "~",
|
||||
"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
|
||||
alerts = ""
|
||||
location = lat,lon
|
||||
if useDefaultLatLon:
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
if float(lat) == 0 and float(lon) == 0 and not useDefaultLatLon:
|
||||
return NO_DATA_NOGPS
|
||||
else:
|
||||
if useDefaultLatLon:
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
|
||||
alert_url = "https://api.weather.gov/alerts/active.atom?point=" + str(lat) + "," + str(lon)
|
||||
#alert_url = "https://api.weather.gov/alerts/active.atom?area=WA"
|
||||
@@ -428,8 +469,8 @@ def getActiveWeatherAlertsDetailNOAA(lat=0, lon=0):
|
||||
alerts = ""
|
||||
location = lat,lon
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
logger.warning("Location:No GPS data, try sending location for weather alerts")
|
||||
return NO_DATA_NOGPS
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
|
||||
alert_url = "https://api.weather.gov/alerts/active.atom?point=" + str(lat) + "," + str(lon)
|
||||
#alert_url = "https://api.weather.gov/alerts/active.atom?area=WA"
|
||||
@@ -489,10 +530,10 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
try:
|
||||
alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds)
|
||||
if not alert_data.ok:
|
||||
logger.warning("System: iPAWS fetching IPAWS alerts from FEMA")
|
||||
logger.warning(f"System: iPAWS fetching IPAWS alerts from FEMA (HTTP {alert_data.status_code})")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("System: iPAWS fetching IPAWS alerts from FEMA")
|
||||
except Exception as e:
|
||||
logger.warning(f"System: iPAWS fetching IPAWS alerts from FEMA failed: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# main feed bulletins
|
||||
@@ -574,14 +615,13 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
|
||||
# check if the alert is for the SAME location, if wanted keep alert
|
||||
if (sameVal in mySAMEList) or (geocode_value in mySAMEList) or mySAMEList == ['']:
|
||||
# ignore the FEMA test alerts
|
||||
ignore_alert = False
|
||||
if ignoreFEMAenable:
|
||||
ignore_alert = False
|
||||
for word in ignoreFEMAwords:
|
||||
if word.lower() in headline.lower():
|
||||
logger.debug(f"System: Filtering FEMA Alert by WORD: {headline} containing {word} at {areaDesc}")
|
||||
ignore_alert = True
|
||||
break
|
||||
ignore_alert = any(
|
||||
word.lower() in headline.lower()
|
||||
for word in ignoreFEMAwords)
|
||||
if ignore_alert:
|
||||
logger.debug(f"System: Filtering FEMA Alert by WORD: {headline} containing one of {ignoreFEMAwords} at {areaDesc}")
|
||||
if ignore_alert:
|
||||
continue
|
||||
|
||||
@@ -706,7 +746,7 @@ def get_volcano_usgs(lat=0, lon=0):
|
||||
return alerts
|
||||
|
||||
def get_nws_marine(zone, days=3):
|
||||
# forcast from NWS coastal products
|
||||
# forecast from NWS coastal products
|
||||
try:
|
||||
marine_pz_data = requests.get(zone, timeout=urlTimeoutSeconds)
|
||||
if not marine_pz_data.ok:
|
||||
@@ -715,18 +755,21 @@ def get_nws_marine(zone, days=3):
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("Location:Error fetching NWS Marine PZ data")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
|
||||
marine_pz_data = marine_pz_data.text
|
||||
#validate data
|
||||
todayDate = today.strftime("%Y%m%d")
|
||||
if marine_pz_data.startswith("Expires:"):
|
||||
expires = marine_pz_data.split(";;")[0].split(":")[1]
|
||||
expires_date = expires[:8]
|
||||
if expires_date < todayDate:
|
||||
logger.debug("Location: NWS Marine PZ data expired")
|
||||
todayDate = datetime.now().strftime("%Y%m%d")
|
||||
if marine_pz_data and marine_pz_data.startswith("Expires:"):
|
||||
try:
|
||||
expires = marine_pz_data.split(";;")[0].split(":")[1]
|
||||
expires_date = expires[:8]
|
||||
if expires_date < todayDate:
|
||||
logger.debug("Location: NWS Marine PZ data expired")
|
||||
return ERROR_FETCHING_DATA
|
||||
except Exception as e:
|
||||
logger.debug(f"Location: NWS Marine PZ data parse error: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
else:
|
||||
logger.debug("Location: NWS Marine PZ data not valid")
|
||||
logger.debug("Location: NWS Marine PZ data not valid or empty")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# process the marine forecast data
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import logging
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from modules.settings import *
|
||||
# if LOGGING_LEVEL is not set in settings.py, default to DEBUG
|
||||
if not LOGGING_LEVEL:
|
||||
@@ -38,11 +36,17 @@ class CustomFormatter(logging.Formatter):
|
||||
return formatter.format(record)
|
||||
|
||||
class plainFormatter(logging.Formatter):
|
||||
ansi_escape = re.compile(r'\x1b\[([0-9]+)(;[0-9]+)*m')
|
||||
ansi_codes = [
|
||||
'\x1b[38;21m', '\x1b[38;5;231m', '\x1b[38;5;39m', '\x1b[38;5;226m',
|
||||
'\x1b[38;5;196m', '\x1b[38;5;46m', '\x1b[38;5;129m', '\x1b[31;1m',
|
||||
'\x1b[37;1m', '\x1b[0m'
|
||||
]
|
||||
|
||||
def format(self, record):
|
||||
message = super().format(record)
|
||||
return self.ansi_escape.sub('', message)
|
||||
for code in self.ansi_codes:
|
||||
message = message.replace(code, '')
|
||||
return message
|
||||
|
||||
# Create logger
|
||||
logger = logging.getLogger("MeshBot System Logger")
|
||||
@@ -56,7 +60,6 @@ msgLogger.propagate = False
|
||||
# Define format for logs
|
||||
logFormat = '%(asctime)s | %(levelname)8s | %(message)s'
|
||||
msgLogFormat = '%(asctime)s | %(message)s'
|
||||
today = datetime.now()
|
||||
|
||||
# Create stdout handler for logging to the console
|
||||
stdout_handler = logging.StreamHandler()
|
||||
|
||||
100
modules/radio.py
100
modules/radio.py
@@ -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
|
||||
|
||||
118
modules/scheduler.py
Normal file
118
modules/scheduler.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# modules/scheduler.py 2025 meshing-around
|
||||
# Scheduler setup for Mesh Bot
|
||||
import asyncio
|
||||
import schedule
|
||||
from modules.log import logger
|
||||
from modules.system import send_message
|
||||
|
||||
async def setup_scheduler(
|
||||
schedulerMotd, MOTD, schedulerMessage, schedulerChannel, schedulerInterface,
|
||||
schedulerValue, schedulerTime, schedulerInterval, logger, BroadcastScheduler):
|
||||
|
||||
# methods available for custom scheduler messages
|
||||
from mesh_bot import tell_joke, welcome_message, handle_wxc, handle_moon, handle_sun, handle_riverFlow, handle_tide, handle_satpass
|
||||
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
|
||||
basicOptions = ['day', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun', 'hour', 'min']
|
||||
if any(option.lower() in schedulerValue.lower() for option in basicOptions):
|
||||
# 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}")
|
||||
elif 'joke' in schedulerValue.lower():
|
||||
# Schedule to send a joke every specified interval
|
||||
schedule.every(int(schedulerInterval)).minutes.do(lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface))
|
||||
logger.debug(f"System: Starting the joke scheduler to send a joke every {schedulerInterval} minutes on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
elif 'weather' in schedulerValue.lower():
|
||||
# Schedule to send weather updates every specified interval
|
||||
schedule.every(int(schedulerInterval)).hours.do(lambda: send_message(handle_wxc(0, schedulerInterface, 'wx'), schedulerChannel, 0, schedulerInterface))
|
||||
logger.debug(f"System: Starting the weather scheduler to send weather updates every {schedulerInterval} hours on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
elif 'custom' in schedulerValue.lower():
|
||||
# Custom scheduler job to run the schedule see examples below
|
||||
|
||||
# custom scheduler job to run the schedule see examples below
|
||||
logger.debug(f"System: Starting the custom scheduler default 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))
|
||||
|
||||
# Place your custom schedule code below this line, helps with merges
|
||||
|
||||
# Place your custom schedule code above this line
|
||||
|
||||
# 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))
|
||||
@@ -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()
|
||||
@@ -259,6 +271,7 @@ try:
|
||||
secure_interface = config['sentry'].getint('SentryInterface', 1) # default 1
|
||||
sentry_holdoff = config['sentry'].getint('SentryHoldoff', 9) # default 9
|
||||
sentryIgnoreList = config['sentry'].get('sentryIgnoreList', '').split(',')
|
||||
sentryWatchList = config['sentry'].get('sentryWatchList', '').split(',')
|
||||
sentry_radius = config['sentry'].getint('SentryRadius', 100) # default 100 meters
|
||||
email_sentry_alerts = config['sentry'].getboolean('emailSentryAlerts', False) # default False
|
||||
highfly_enabled = config['sentry'].getboolean('highFlyingAlert', True) # default True
|
||||
@@ -269,6 +282,9 @@ try:
|
||||
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
|
||||
cmdShellSentryAlerts = config['sentry'].getboolean('cmdShellSentryAlerts', False) # default False
|
||||
sentryAlertNear = config['sentry'].get('sentryAlertNear', 'sentry_alert_near.sh') # default sentry_alert_near.sh
|
||||
sentryAlertFar = config['sentry'].get('sentryAlertFar', 'sentry_alert_far.sh') # default sentry_alert_far.sh
|
||||
|
||||
# location
|
||||
location_enabled = config['location'].getboolean('enabled', True)
|
||||
@@ -377,6 +393,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)
|
||||
@@ -404,6 +421,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)
|
||||
|
||||
@@ -412,7 +430,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
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
import json
|
||||
import os # For file operations
|
||||
import csv
|
||||
from datetime import datetime
|
||||
from collections import Counter
|
||||
from modules.log import *
|
||||
|
||||
@@ -49,20 +51,25 @@ 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\n"
|
||||
msg += self.show_question(user_id)
|
||||
msg += f"\nSend answer' or 'end'"
|
||||
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."""
|
||||
@@ -92,17 +99,92 @@ class SurveyModule:
|
||||
filename = os.path.join(self.response_dir, f'{survey_name}_responses.csv')
|
||||
try:
|
||||
with open(filename, 'a', encoding='utf-8') as f:
|
||||
row = list(map(str, self.responses[user_id]['answers']))
|
||||
if surveyRecordID:
|
||||
row.insert(0, str(user_id))
|
||||
if surveyRecordLocation:
|
||||
location = self.responses[user_id].get('location')
|
||||
row.insert(1 if surveyRecordID else 0, str(location) if location is not None else "N/A")
|
||||
# Always write: timestamp, userID, position, answers...
|
||||
timestamp = datetime.now().strftime('%d%m%Y%H%M%S')
|
||||
user_id_str = str(user_id)
|
||||
location = self.responses[user_id].get('location', "N/A")
|
||||
answers = list(map(str, self.responses[user_id]['answers']))
|
||||
row = [timestamp, user_id_str, str(location)] + answers
|
||||
f.write(','.join(row) + '\n')
|
||||
logger.info(f"Survey: Responses for user {user_id} saved for survey '{survey_name}' to {filename}.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving responses to {filename}: {e}")
|
||||
|
||||
def format_survey_results(self, results):
|
||||
if isinstance(results, dict) and "error" in results:
|
||||
return results["error"]
|
||||
if not results:
|
||||
return "No results found."
|
||||
msg = "📊Survey Results:\n"
|
||||
for idx, q in enumerate(results):
|
||||
msg += f"\nQ{idx+1}: {q['question']}\n"
|
||||
if q['type'] == 'multiple_choice':
|
||||
for opt, count in q['summary'].items():
|
||||
msg += f" {opt}: {count}\n"
|
||||
elif q['type'] == 'integer':
|
||||
s = q['summary']
|
||||
msg += f" Count: {s['count']}, Avg: {s['average']:.2f}, Min: {s['min']}, Max: {s['max']}\n"
|
||||
elif q['type'] == 'text':
|
||||
msg += f" Responses: {q['summary']['responses_count']}\n"
|
||||
return msg
|
||||
|
||||
def get_survey_results(self, survey_name='example'):
|
||||
if survey_name not in self.surveys:
|
||||
return {"error": f"Survey '{survey_name}' not found."}
|
||||
filename = os.path.join(self.response_dir, f'{survey_name}_responses.csv')
|
||||
questions = self.surveys[survey_name]
|
||||
results = []
|
||||
try:
|
||||
with open(filename, encoding='utf-8') as f:
|
||||
reader = csv.reader(f)
|
||||
lines = []
|
||||
for row in reader:
|
||||
if not row or len(row) < 4:
|
||||
continue
|
||||
# If location field is split due to comma, join columns 2 and 3
|
||||
if row[2].startswith('[') and not row[2].endswith(']') and len(row) > 4:
|
||||
location = row[2] + ',' + row[3]
|
||||
answers = row[4:]
|
||||
else:
|
||||
location = row[2]
|
||||
answers = row[3:]
|
||||
lines.append(answers)
|
||||
|
||||
for q_idx, question in enumerate(questions):
|
||||
qtype = question.get('type', 'multiple_choice')
|
||||
answers = [row[q_idx] for row in lines if len(row) > q_idx]
|
||||
|
||||
summary = {}
|
||||
if qtype == 'multiple_choice':
|
||||
counts = Counter(answers)
|
||||
summary = {chr(65+i): counts.get(chr(65+i), 0) for i in range(len(question.get('options', [])))}
|
||||
|
||||
elif qtype == 'integer':
|
||||
ints = [int(a) for a in answers if a.isdigit()]
|
||||
summary = {
|
||||
"count": len(ints),
|
||||
"average": sum(ints)/len(ints) if ints else 0,
|
||||
"min": min(ints) if ints else None,
|
||||
"max": max(ints) if ints else None
|
||||
}
|
||||
|
||||
elif qtype == 'text':
|
||||
summary = {"responses_count": len([a for a in answers if a.strip()])}
|
||||
|
||||
|
||||
results.append({
|
||||
"question": question['question'],
|
||||
"type": qtype,
|
||||
"summary": summary
|
||||
})
|
||||
|
||||
return results
|
||||
except FileNotFoundError:
|
||||
return {"error": f"No responses recorded yet for '{survey_name}'."}
|
||||
except Exception as e:
|
||||
logger.error(f"Error summarizing survey results: {e}")
|
||||
return NO_ALERTS
|
||||
|
||||
def answer(self, user_id, answer, location=None):
|
||||
try:
|
||||
"""Record an answer and return the next question or end message."""
|
||||
@@ -121,7 +203,8 @@ class SurveyModule:
|
||||
return "Please answer with a letter (A, B, C, ...)."
|
||||
option_index = ord(answer_char) - 65
|
||||
if 0 <= option_index < len(question['options']):
|
||||
self.responses[user_id]['answers'].append(str(option_index))
|
||||
# Valid answer record letter, not index
|
||||
self.responses[user_id]['answers'].append(answer_char)
|
||||
self.responses[user_id]['current_question'] += 1
|
||||
return f"Recorded..\n" + self.show_question(user_id)
|
||||
else:
|
||||
|
||||
@@ -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
|
||||
@@ -289,7 +290,7 @@ if voxDetectionEnabled:
|
||||
from modules.radio import * # from the spudgunman/meshing-around repo
|
||||
|
||||
# File Monitor Configuration
|
||||
if file_monitor_enabled or read_news_enabled or bee_enabled:
|
||||
if file_monitor_enabled or read_news_enabled or bee_enabled or enable_runShellCmd or cmdShellSentryAlerts:
|
||||
from modules.filemon import * # from the spudgunman/meshing-around repo
|
||||
if read_news_enabled:
|
||||
trap_list = trap_list + trap_list_filemon # items readnews
|
||||
@@ -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,44 @@ 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"]
|
||||
interface = globals().get(f'interface{interface_id}')
|
||||
for channel_name, channel_number in device["channels"].items():
|
||||
psk_base64 = "AQ==" # default PSK
|
||||
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 +456,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}")
|
||||
@@ -441,7 +511,6 @@ def get_name_from_number(number, type='long', nodeInt=1):
|
||||
name = str(decimal_to_hex(number)) # If name not found, use the ID as string
|
||||
return name
|
||||
|
||||
|
||||
def get_num_from_short_name(short_name, nodeInt=1):
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
# Get the node number from the short name, converting all to lowercase for comparison (good practice?)
|
||||
@@ -587,6 +656,7 @@ async def get_closest_nodes(nodeInt=1,returnCount=3, channel=publicChannel):
|
||||
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')}")
|
||||
@@ -596,12 +666,10 @@ async def get_closest_nodes(nodeInt=1,returnCount=3, channel=publicChannel):
|
||||
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)
|
||||
# wait a bit
|
||||
time.sleep(3)
|
||||
# wayyy too fast async wait
|
||||
|
||||
# send a traceroute request
|
||||
interface.sendTraceRoute(destinationId=node['id'], channelIndex=channel, wantResponse=False)
|
||||
# wait a bit
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error requesting location data for {node['id']}. Error: {e}")
|
||||
# sort by distance closest
|
||||
@@ -801,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
|
||||
@@ -991,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)):
|
||||
@@ -1076,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:
|
||||
@@ -1092,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)
|
||||
@@ -1236,12 +1329,14 @@ def displayNodeTelemetry(nodeID=0, rxNode=0, userRequested=False):
|
||||
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}"
|
||||
if numTxRelays != 0:
|
||||
dataResponse += f" TxRelays:{numTxRelays}"
|
||||
if heapFreeBytes != 0 and heapTotalBytes != 0:
|
||||
logger.debug(f"System: Device {rxNode} Heap Memory Free:{heapFreeBytes} Total:{heapTotalBytes}")
|
||||
# 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
|
||||
@@ -1406,20 +1501,38 @@ 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"
|
||||
|
||||
# throttle sending alerts for the same node more than once every 30 minutes
|
||||
last_alert_time = positionMetadata[nodeID].get('lastHighFlyAlert', 0)
|
||||
current_time = time.time()
|
||||
if current_time - last_alert_time < 1800:
|
||||
return False # less than 30 minutes since last alert
|
||||
positionMetadata[nodeID]['lastHighFlyAlert'] = current_time
|
||||
|
||||
if highfly_check_openskynetwork:
|
||||
# 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]
|
||||
@@ -1488,7 +1601,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}")
|
||||
|
||||
@@ -1565,6 +1677,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:
|
||||
@@ -1789,13 +1917,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}")
|
||||
|
||||
@@ -1818,23 +1944,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")
|
||||
|
||||
@@ -1892,46 +2014,62 @@ handleSentinel_spotted = []
|
||||
handleSentinel_loop = 0
|
||||
async def handleSentinel(deviceID):
|
||||
global handleSentinel_spotted, handleSentinel_loop
|
||||
detectedNearby = ""
|
||||
detectedNearby = None
|
||||
resolution = "unknown"
|
||||
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
|
||||
|
||||
# check if the handleSentinel_spotted list contains the closest node already
|
||||
if closest_node in [i['id'] for i in handleSentinel_spotted]:
|
||||
# check if the distance is closer than the last time, if not just return
|
||||
for i in range(len(handleSentinel_spotted)):
|
||||
if handleSentinel_spotted[i]['id'] == closest_node and closest_distance is not None and closest_distance < handleSentinel_spotted[i]['distance']:
|
||||
handleSentinel_spotted[i]['distance'] = closest_distance
|
||||
break
|
||||
else:
|
||||
return
|
||||
|
||||
if closest_nodes != ERROR_FETCHING_DATA and closest_nodes:
|
||||
if closest_nodes[0]['id'] is not None:
|
||||
detectedNearby = get_name_from_number(closest_node, 'long', deviceID)
|
||||
detectedNearby += ", " + get_name_from_number(closest_nodes[0]['id'], 'short', deviceID)
|
||||
detectedNearby += ", " + str(closest_nodes[0]['id'])
|
||||
detectedNearby += ", " + decimal_to_hex(closest_nodes[0]['id'])
|
||||
detectedNearby += f" at {closest_distance}m"
|
||||
closest_nodes = await get_closest_nodes(deviceID, returnCount=10)
|
||||
#logger.debug(f"handleSentinel: closest_nodes={closest_nodes}")
|
||||
|
||||
if handleSentinel_loop >= sentry_holdoff and detectedNearby not in ["", None]:
|
||||
if closest_nodes and positionMetadata and closest_nodes[0]['id'] in positionMetadata:
|
||||
metadata = positionMetadata[closest_nodes[0]['id']]
|
||||
if metadata.get('precisionBits') is not None:
|
||||
resolution = metadata.get('precisionBits')
|
||||
if not closest_nodes or closest_nodes == ERROR_FETCHING_DATA:
|
||||
return
|
||||
|
||||
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}")
|
||||
handleSentinel_loop = 0
|
||||
handleSentinel_spotted.append({'id': closest_node, 'distance': closest_distance})
|
||||
else:
|
||||
# Find any watched node inside or outside the zone
|
||||
for node in closest_nodes:
|
||||
node_id = node['id']
|
||||
distance = node['distance']
|
||||
|
||||
if str(node_id) in sentryIgnoreList:
|
||||
return
|
||||
# Message conditions
|
||||
if distance >= sentry_radius and str(node_id) and str(node_id) in sentryWatchList:
|
||||
# Outside zone
|
||||
detectedNearby = f"{get_name_from_number(node_id, 'long', deviceID)}, {get_name_from_number(node_id, 'short', deviceID)}, {node_id}, {decimal_to_hex(node_id)} at {distance}m (OUTSIDE ZONE)"
|
||||
elif distance <= sentry_radius and str(node_id) not in sentryWatchList:
|
||||
# Inside the zone
|
||||
detectedNearby = f"{get_name_from_number(node_id, 'long', deviceID)}, {get_name_from_number(node_id, 'short', deviceID)}, {node_id}, {decimal_to_hex(node_id)} at {distance}m (INSIDE ZONE)"
|
||||
|
||||
#logger.debug(f"handleSentinel: loop={handleSentinel_loop}/{sentry_holdoff}, detectedNearby={detectedNearby} closest_nodes={closest_nodes}")
|
||||
if detectedNearby:
|
||||
handleSentinel_loop += 1
|
||||
#logger.debug(f"handleSentinel: detectedNearby={detectedNearby}, loop={handleSentinel_loop}/{sentry_holdoff}")
|
||||
if handleSentinel_loop >= sentry_holdoff:
|
||||
# Get resolution if available
|
||||
if positionMetadata and node_id in positionMetadata:
|
||||
metadata = positionMetadata[node_id]
|
||||
if metadata.get('precisionBits') is not None:
|
||||
resolution = metadata.get('precisionBits')
|
||||
# Send message alert
|
||||
logger.warning(f"System: {detectedNearby} on Interface{deviceID} Accuracy is {resolution}bits")
|
||||
send_message(f"Sentry{deviceID}: {detectedNearby}", secure_channel, 0, secure_interface)
|
||||
|
||||
# Send email alerts
|
||||
if enableSMTP and email_sentry_alerts:
|
||||
for email in sysopEmails:
|
||||
send_email(email, f"Sentry{deviceID}: {detectedNearby}")
|
||||
|
||||
# Execute external script alerts
|
||||
if cmdShellSentryAlerts and distance <= sentry_radius:
|
||||
# inside zone
|
||||
call_external_script('', script=sentryAlertNear)
|
||||
logger.info(f"System: Sentry Script Alert {sentryAlertNear} for NodeID:{node_id} on Interface{deviceID}")
|
||||
elif cmdShellSentryAlerts and distance >= sentry_radius:
|
||||
# outside zone
|
||||
call_external_script('', script=sentryAlertFar)
|
||||
logger.info(f"System: Sentry Script Alert {sentryAlertFar} for NodeID:{node_id} on Interface{deviceID}")
|
||||
|
||||
handleSentinel_loop = 0 # Loop reset
|
||||
else:
|
||||
handleSentinel_loop = 0 # Reset if nothing detected
|
||||
|
||||
async def process_vox_queue():
|
||||
# process the voxMsgQueue
|
||||
@@ -1945,7 +2083,6 @@ 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 localTelemetryData, retry_int1, retry_int2, retry_int3, retry_int4, retry_int5, retry_int6, retry_int7, retry_int8, retry_int9
|
||||
|
||||
126
modules/udp.py
Normal file
126
modules/udp.py
Normal 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
|
||||
|
||||
|
||||
182
pong_bot.py
182
pong_bot.py
@@ -10,6 +10,7 @@ except ImportError:
|
||||
|
||||
import asyncio
|
||||
import time # for sleep, get some when you can :)
|
||||
from datetime import datetime
|
||||
import random
|
||||
from modules.log import *
|
||||
from modules.system import *
|
||||
@@ -92,10 +93,13 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
else:
|
||||
msg = "🔊 Can you hear me now?"
|
||||
|
||||
if hop == "Direct":
|
||||
msg = msg + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
msg = msg + hop
|
||||
# append SNR/RSSI or hop info
|
||||
if hop.startswith("Direct?") and (snr != 0 or rssi != 0):
|
||||
msg += f"? SNR:{snr} RSSI:{rssi}"
|
||||
elif hop.startswith("Direct"):
|
||||
msg += f"SNR:{snr} RSSI:{rssi}"
|
||||
elif hop:
|
||||
msg += f"{hop}"
|
||||
|
||||
if "@" in message:
|
||||
msg = msg + " @" + message.split("@")[1]
|
||||
@@ -150,7 +154,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
def handle_motd(message, message_from_id, isDM):
|
||||
global MOTD
|
||||
isAdmin = False
|
||||
msg = ""
|
||||
msg = MOTD
|
||||
# check if the message_from_id is in the bbs_admin_list
|
||||
if bbs_admin_list != ['']:
|
||||
for admin in bbs_admin_list:
|
||||
@@ -216,11 +220,16 @@ def onReceive(packet, interface):
|
||||
rxType = type(interface).__name__
|
||||
|
||||
# Valies assinged to the packet
|
||||
rxNode, message_from_id, snr, rssi, hop, hop_away, channel_number = 0, 0, 0, 0, 0, 0, 0
|
||||
rxNode = message_from_id = snr = rssi = hop = hop_away = channel_number = hop_start = hop_count = hop_limit = 0
|
||||
pkiStatus = (False, 'ABC')
|
||||
replyIDset = False
|
||||
rxNodeHostName = None
|
||||
emojiSeen = False
|
||||
simulator_flag = False
|
||||
isDM = False
|
||||
channel_name = "unknown"
|
||||
session_passkey = None
|
||||
playingGame = False
|
||||
|
||||
if DEBUGpacket:
|
||||
# Debug print the interface object
|
||||
@@ -229,45 +238,60 @@ def onReceive(packet, interface):
|
||||
# Debug print the packet for debugging
|
||||
logger.debug(f"Packet Received\n {packet} \n END of packet \n")
|
||||
|
||||
# set the value for the incomming interface
|
||||
if rxType == 'SerialInterface':
|
||||
rxInterface = interface.__dict__.get('devPath', 'unknown')
|
||||
if port1 in rxInterface: rxNode = 1
|
||||
elif multiple_interface and port2 in rxInterface: rxNode = 2
|
||||
elif multiple_interface and port3 in rxInterface: rxNode = 3
|
||||
elif multiple_interface and port4 in rxInterface: rxNode = 4
|
||||
elif multiple_interface and port5 in rxInterface: rxNode = 5
|
||||
elif multiple_interface and port6 in rxInterface: rxNode = 6
|
||||
elif multiple_interface and port7 in rxInterface: rxNode = 7
|
||||
elif multiple_interface and port8 in rxInterface: rxNode = 8
|
||||
elif multiple_interface and port9 in rxInterface: rxNode = 9
|
||||
|
||||
# determine the rxNode based on the interface type
|
||||
if rxType == 'TCPInterface':
|
||||
rxHost = interface.__dict__.get('hostname', 'unknown')
|
||||
if rxHost and hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
|
||||
elif multiple_interface and rxHost and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
|
||||
elif multiple_interface and rxHost and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
|
||||
elif multiple_interface and rxHost and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
|
||||
elif multiple_interface and rxHost and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
|
||||
elif multiple_interface and rxHost and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
|
||||
elif multiple_interface and rxHost and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
|
||||
elif multiple_interface and rxHost and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
|
||||
elif multiple_interface and rxHost and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
|
||||
rxNodeHostName = interface.__dict__.get('ip', None)
|
||||
rxNode = next(
|
||||
(i for i in range(1, 10)
|
||||
if multiple_interface and rxHost and
|
||||
globals().get(f'hostname{i}', '').split(':', 1)[0] in rxHost and
|
||||
globals().get(f'interface{i}_type', '') == 'tcp'),None)
|
||||
|
||||
if rxType == 'BLEInterface':
|
||||
if interface1_type == 'ble': rxNode = 1
|
||||
elif multiple_interface and interface2_type == 'ble': rxNode = 2
|
||||
elif multiple_interface and interface3_type == 'ble': rxNode = 3
|
||||
elif multiple_interface and interface4_type == 'ble': rxNode = 4
|
||||
elif multiple_interface and interface5_type == 'ble': rxNode = 5
|
||||
elif multiple_interface and interface6_type == 'ble': rxNode = 6
|
||||
elif multiple_interface and interface7_type == 'ble': rxNode = 7
|
||||
elif multiple_interface and interface8_type == 'ble': rxNode = 8
|
||||
elif multiple_interface and interface9_type == 'ble': rxNode = 9
|
||||
if rxType == 'SerialInterface':
|
||||
rxInterface = interface.__dict__.get('devPath', 'unknown')
|
||||
rxNode = next(
|
||||
(i for i in range(1, 10)
|
||||
if globals().get(f'port{i}', '') in rxInterface),None)
|
||||
|
||||
# check if the packet has a channel flag use it
|
||||
if rxType == 'BLEInterface':
|
||||
rxNode = next(
|
||||
(i for i in range(1, 10)
|
||||
if globals().get(f'interface{i}_type', '') == 'ble'),0)
|
||||
|
||||
if rxNode is None:
|
||||
# default to interface 1 ## FIXME needs better like a default interface setting or hash lookup
|
||||
if 'decoded' in packet and packet['decoded']['portnum'] in ['ADMIN_APP', 'SIMULATOR_APP']:
|
||||
session_passkey = packet.get('decoded', {}).get('admin', {}).get('sessionPasskey', None)
|
||||
rxNode = 1
|
||||
|
||||
# check if the packet has a channel flag use it ## FIXME needs to be channel hash lookup
|
||||
if packet.get('channel'):
|
||||
channel_number = packet.get('channel', 0)
|
||||
channel_number = packet.get('channel')
|
||||
# get channel name from channel number from connected devices
|
||||
for device in channel_list:
|
||||
if device["interface_id"] == rxNode:
|
||||
device_channels = device['channels']
|
||||
for chan_name, info in device_channels.items():
|
||||
if info['number'] == channel_number:
|
||||
channel_name = chan_name
|
||||
break
|
||||
|
||||
# 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 +306,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 +319,58 @@ 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)
|
||||
|
||||
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
|
||||
hop_count = hop_away
|
||||
|
||||
if hop == "" and hop_count > 0:
|
||||
hop = f"{hop_count} Hop" if hop_count == 1 else 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"
|
||||
|
||||
## FIXME should this be here?
|
||||
if hop == "" and hop_count ==0 and (snr != 0 or rssi != 0):
|
||||
hop = "Direct?"
|
||||
|
||||
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}")
|
||||
|
||||
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}")
|
||||
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} Hostname:{rxNodeHostName}")
|
||||
|
||||
# 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)}")
|
||||
|
||||
hop = f"{hop_count} hops"
|
||||
|
||||
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 +390,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
script/configMerge.py
Normal file
104
script/configMerge.py
Normal 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)
|
||||
@@ -42,3 +42,15 @@ then
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Get public and local IP addresses
|
||||
public_ip=$(curl -s https://ifconfig.me 2>/dev/null)
|
||||
public_ip=${public_ip:-""}
|
||||
local_ip=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
local_ip=${local_ip:-""}
|
||||
if [ -n "$public_ip" ]; then
|
||||
echo "Public IP: $public_ip"
|
||||
fi
|
||||
if [ -n "$local_ip" ]; then
|
||||
echo "Local IP: $local_ip"
|
||||
fi
|
||||
42
update.sh
42
update.sh
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user