Compare commits

..

46 Commits

Author SHA1 Message Date
SpudGunMan
54837884a7 Update locationdata.py 2025-10-23 15:48:08 -07:00
Kelly
91501d42db Merge pull request #158 from pcs3rd/create-docker-image
Create docker image and push to packages on new release
2025-10-23 15:44:57 -07:00
SpudGunMan
e0bcc31204 Update README.md 2025-10-23 15:37:43 -07:00
SpudGunMan
1cb56aa1b7 set bbslink via config.ini 2025-10-23 15:32:10 -07:00
SpudGunMan
8cf2db3b49 Update custom_scheduler.py 2025-10-23 15:22:05 -07:00
SpudGunMan
755fc4fac3 Update joke.py 2025-10-23 14:51:14 -07:00
SpudGunMan
7c341ed0e7 cleanup 2025-10-23 14:50:18 -07:00
SpudGunMan
c87db75f2f cleanupPartOne
I hope I didnt break a lot of things!
2025-10-23 13:46:36 -07:00
SpudGunMan
13875b7cf8 Update pong_bot.py 2025-10-23 13:33:17 -07:00
SpudGunMan
fd4925ee92 Update mesh_bot.py 2025-10-23 13:18:31 -07:00
SpudGunMan
eccc48ff3f whats all this 2025-10-23 13:15:51 -07:00
SpudGunMan
3a6d464398 Update bbstools.md 2025-10-23 12:52:54 -07:00
SpudGunMan
6c7e8558b0 Update bbstools.md 2025-10-23 12:51:01 -07:00
SpudGunMan
74a744c77e Update wiki.py 2025-10-23 12:45:12 -07:00
SpudGunMan
5225998c92 Update rss.py 2025-10-23 12:43:33 -07:00
SpudGunMan
97a0ff3112 enhance 2025-10-23 12:41:37 -07:00
SpudGunMan
1250479219 Update README.md 2025-10-23 12:33:17 -07:00
SpudGunMan
f8bcc4f495 Update README.md 2025-10-23 12:26:34 -07:00
SpudGunMan
5d1608f366 Update README.md 2025-10-23 12:25:16 -07:00
SpudGunMan
a19dc93350 Update locationdata.py 2025-10-23 12:13:25 -07:00
SpudGunMan
30d4e487c9 Update locationdata.py 2025-10-23 12:12:54 -07:00
SpudGunMan
5bf1ade2b0 enhance 2025-10-23 12:10:45 -07:00
SpudGunMan
13cefc2002 Update locationdata.py 2025-10-23 12:08:00 -07:00
SpudGunMan
640bead32c Update locationdata.py 2025-10-23 12:06:45 -07:00
SpudGunMan
49d9b58627 Update mesh_bot.py 2025-10-23 12:02:55 -07:00
SpudGunMan
ad1a8aa1ce Update locationdata.py 2025-10-23 12:01:56 -07:00
SpudGunMan
55567815ef map
to csv
2025-10-23 11:57:55 -07:00
SpudGunMan
346fb38bbd heasder csv 2025-10-23 11:40:24 -07:00
SpudGunMan
104e70c01c Update README.md 2025-10-23 11:33:55 -07:00
SpudGunMan
2111bb46ae bbs doc 2025-10-23 10:27:28 -07:00
Kelly
459dad4c32 Merge branch 'main' into create-docker-image 2025-10-23 09:43:52 -07:00
SpudGunMan
d9febeef0f cleanupX2 2025-10-23 09:39:21 -07:00
SpudGunMan
f8a94fca71 cleanup
yikes this was messy
2025-10-23 09:21:48 -07:00
SpudGunMan
a30b3dc2d2 Update dependabot.yml
https://github.com/SpudGunMan/meshing-around/pull/228
2025-10-23 08:38:09 -07:00
Kelly
b45254795d Merge pull request #228 from SpudGunMan/dependabot/docker/python-3.14-slim
Bump python from 3.13-slim to 3.14-slim
2025-10-23 08:37:41 -07:00
SpudGunMan
4a209e0c17 Update greetings.yml 2025-10-23 08:30:14 -07:00
dependabot[bot]
1aecb42186 Bump python from 3.13-slim to 3.14-slim
Bumps python from 3.13-slim to 3.14-slim.

---
updated-dependencies:
- dependency-name: python
  dependency-version: 3.14-slim
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-23 15:29:14 +00:00
SpudGunMan
6513e9f177 Update dependabot.yml 2025-10-23 08:28:22 -07:00
SpudGunMan
dd14034f3c Update update.sh 2025-10-23 00:20:45 -07:00
Raymond Dean
57093d09ef Merge branch 'SpudGunMan:main' into create-docker-image 2025-10-22 09:43:27 -04:00
Raymond Dean
29a26d5d14 Merge branch 'SpudGunMan:main' into create-docker-image 2025-08-08 19:29:53 -04:00
Raymond Dean
43e6349351 Update compose example. 2025-07-15 17:42:17 -04:00
Raymond Dean
628f66e4b7 Update compose.yaml to expose port 8420 2025-07-12 14:08:29 -04:00
Raymond Dean
29f97c62d0 add compose example 2025-07-12 11:50:21 -04:00
Raymond Dean
b805e6d428 Update docker-image.yml 2025-07-12 11:19:35 -04:00
Raymond Dean
6d5ded7df6 Create and publish a Docker image on new release 2025-07-12 11:18:05 -04:00
25 changed files with 720 additions and 158 deletions

View File

@@ -5,7 +5,24 @@ updates:
directory: "/"
schedule:
interval: "weekly"
assignees:
- "SpudGunMan"
labels:
- "dependencies"
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
interval: "weekly"
assignees:
- "SpudGunMan"
labels:
- "dependencies"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
assignees:
- "SpudGunMan"
labels:
- "dependencies"

61
.github/workflows/docker-image.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
#
name: Create and publish a Docker image on new release
# Configures this workflow to run every time a change is pushed to the branch called `release`.
on:
release:
types: [released]
workflow_dispatch:
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
jobs:
build-and-push-image:
runs-on: ubuntu-latest
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
permissions:
contents: read
packages: write
attestations: write
id-token: write
#
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see [Usage](https://github.com/docker/build-push-action#usage) in the README of the `docker/build-push-action` repository.
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
- name: Build and push Docker image
id: push
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see [Using artifact attestations to establish provenance for builds](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds).
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v2
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true

View File

@@ -2,17 +2,21 @@ name: Greetings
on:
issues:
types:
- opened
types: [opened]
pull_request:
types: [opened]
permissions:
issues: write
pull-requests: write
jobs:
greeting:
name: Greet first-time contributors
runs-on: ubuntu-latest
steps:
- uses: actions/first-interaction@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue_message: "Dependabot's first issue"
issue_message: "Dependabot's first issue"
pr_message: "Thank you for your pull request!"

View File

@@ -1,21 +1,32 @@
FROM python:3.13-slim
FROM python:3.14-slim
ENV PYTHONUNBUFFERED=1
RUN apt-get update && apt-get install -y gettext tzdata locales nano && rm -rf /var/lib/apt/lists/*
ENV PYTHONUNBUFFERED=1 \
LANG=en_US.UTF-8 \
LANGUAGE=en_US:en \
LC_ALL=en_US.UTF-8 \
TZ=America/Los_Angeles
# Set the locale default to en_US.UTF-8
RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \
dpkg-reconfigure --frontend=noninteractive locales && \
update-locale LANG=en_US.UTF-8
ENV LANG="en_US.UTF-8"
ENV TZ="America/Los_Angeles"
RUN apt-get update && \
apt-get install -y gettext tzdata locales nano && \
sed -i 's/^# *\(en_US.UTF-8 UTF-8\)/\1/' /etc/locale.gen && \
locale-gen en_US.UTF-8 && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install dependencies first for better caching
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the application
COPY . /app
COPY config.template /app/config.ini
RUN pip install -r requirements.txt
RUN chmod +x /app/script/docker/entrypoint.sh
# Add a non-root user and switch to it
RUN useradd -m appuser
USER appuser
ENTRYPOINT ["/bin/bash", "/app/script/docker/entrypoint.sh"]

View File

@@ -3,13 +3,13 @@
Mesh Bot is a feature-rich Python bot designed to enhance your [Meshtastic](https://meshtastic.org/docs/introduction/) network experience. It provides powerful tools for network testing, messaging, games, and more—all via text-based message delivery. Whether you want to test your mesh, send messages, or play games, [mesh_bot.py](mesh_bot.py) has you covered.
* [Getting Started](#getting-started)
* [Configuration](#configuration-guide)
![Example Use](etc/pong-bot.jpg "Example Use")
#### TLDR
* [install.sh](INSTALL.md)
* [modules/README.md](modules/README.md)
* [modules/games/README.md](modules/games/README.md)
* [Configuration Guide](modules/README.md)
* [Games Help](modules/games/README.md)
## Key Features
![CodeQlBadge](https://github.com/SpudGunMan/meshing-around/actions/workflows/dynamic/github-code-scanning/codeql/badge.svg)
@@ -25,6 +25,8 @@ Mesh Bot is a feature-rich Python bot designed to enhance your [Meshtastic](http
- **Hardware Testing**: The `test` command sends incrementally sized data to test radio buffer limits.
- **Network Monitoring**: Alerts for noisy nodes, tracks node locations, and suggests optimal relay placement.
- **Site Survey & Location Logging**: Use the `map` command to log your current GPS location with a custom description—ideal for site surveys, asset tracking, or mapping nodes locations. Entries are saved to a CSV file for later analysis or visualization.
### Multi-Radio/Node Support
- **Simultaneous Monitoring**: Observe up to nine networks at once.
- **Flexible Messaging**: Send mail and messages between networks.

43
compose.yaml Normal file
View File

@@ -0,0 +1,43 @@
# Docker Compose configuration for Meshing Around.
# This setup includes the main Meshing Around service, with optional Ollama and Prometheus Node Exporter services.
# Adjust device mappings, ports, and configurations as needed for your environment.
services:
meshing-around:
stdin_open: true
tty: true
ports:
- 8420:8420
devices: # Optional if using meshtasticd. Pass through radio device.
- /dev/ttyUSB0 # Replace this with your actual device!
#- /dev/ttyAMA0 # Example
volumes:
- /data/meshing-around/config.ini:/app/config.ini:rw
image: ghcr.io/SpudGunMan/meshing-around:test-all-changes
container_name: meshing-around
restart: unless-stopped
environment:
- OLLAMA_API_URL=http://ollama:11434
extra_hosts:
#- "host.docker.internal:host-gateway" # Enables access to host services from within the container.
user: "1000:1000" # run as non-root user for better security
meshtasticd: # Runs a virtual node. Optional, but can be used to link meshing-around directly to mqtt.
ports:
- 4403:4403
restart: unless-stopped
container_name: meshtasticd
image: meshtastic/meshtasticd:beta
ollama: # Used for enabling LLM interactions.
ports:
- 11434:11434 # Ollama API port
volumes:
- /data/ollama:/root/.ollama
container_name: ollama
image: ollama/ollama:latest
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"]
interval: 30s
timeout: 10s
retries: 5

View File

@@ -287,7 +287,7 @@ 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 also be joke (everyXmin) or weather (hour) for special scheduled messages
# value can also be joke (everyXmin) or weather (hour) or link (hour) for special auto messages
# custom for module/scheduler.py custom schedule examples
value =
# interval to use when time is not set (e.g. every 2 days)

View File

@@ -4,26 +4,28 @@ from modules.system import send_message
def setup_custom_schedules(send_message, tell_joke, welcome_message, handle_wxc, MOTD, schedulerChannel, schedulerInterface):
# 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}")
logger.debug(f"System: Starting the custom_scheduler.py 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"))
# Enhanced Examples of using the scheduler, Times here are in 24hr format
# https://schedule.readthedocs.io/en/stable/
# Send a joke every 2 minutes
#logger.debug(f"System: Custom Scheduler: Send a joke every 2 minutes on Device:{schedulerInterface} Channel:{schedulerChannel}")
#schedule.every(2).minutes.do(lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface))
# 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
#logger.debug("System: Custom Scheduler: Send WX every Morning at 08:00")
#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
#logger.debug("System: Custom Scheduler: Config URL for Medium Fast Network Use every other day at 10:00")
#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
@@ -33,6 +35,7 @@ def setup_custom_schedules(send_message, tell_joke, welcome_message, handle_wxc,
#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
#logger.debug(f"System: Custom Scheduler: Welcome Notice for group on the 15th and 25th of the month at 12:00 on Device:{schedulerInterface} Channel:{schedulerChannel}")
#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
@@ -45,4 +48,5 @@ def setup_custom_schedules(send_message, tell_joke, welcome_message, handle_wxc,
#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
#logger.debug("System: Custom Scheduler: bbslink MeshBot 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))

View File

@@ -78,9 +78,9 @@ fi
# add user to groups for serial access
printf "\nAdding user to dialout, bluetooth, and tty groups for serial access\n"
sudo usermod -a -G dialout $USER
sudo usermod -a -G tty $USER
sudo usermod -a -G bluetooth $USER
sudo usermod -a -G dialout "$USER"
sudo usermod -a -G tty "$USER"
sudo usermod -a -G bluetooth "$USER"
# copy service files
cp etc/pong_bot.tmp etc/pong_bot.service
@@ -186,10 +186,10 @@ fi
# set the correct path in the service file
replace="s|/dir/|$program_path/|g"
sed -i $replace etc/pong_bot.service
sed -i $replace etc/mesh_bot.service
sed -i $replace etc/mesh_bot_reporting.service
sed -i $replace etc/mesh_bot_w3.service
sed -i "$replace" etc/pong_bot.service
sed -i "$replace" etc/mesh_bot.service
sed -i "$replace" etc/mesh_bot_reporting.service
sed -i "$replace" etc/mesh_bot_w3.service
# set the correct user in the service file?
#ask if we should add a user for the bot
@@ -209,9 +209,9 @@ else
whoami=$(whoami)
fi
# set basic permissions for the bot user
sudo usermod -a -G dialout $whoami
sudo usermod -a -G tty $whoami
sudo usermod -a -G bluetooth $whoami
sudo usermod -a -G dialout "$whoami"
sudo usermod -a -G tty "$whoami"
sudo usermod -a -G bluetooth "$whoami"
echo "Added user $whoami to dialout, tty, and bluetooth groups"
sudo chown -R "$whoami:$whoami" "$program_path/logs"
@@ -227,15 +227,15 @@ fi
# set the correct user in the service file
replace="s|User=pi|User=$whoami|g"
sed -i $replace etc/pong_bot.service
sed -i $replace etc/mesh_bot.service
sed -i $replace etc/mesh_bot_reporting.service
sed -i $replace etc/mesh_bot_w3.service
sed -i "$replace" etc/pong_bot.service
sed -i "$replace" etc/mesh_bot.service
sed -i "$replace" etc/mesh_bot_reporting.service
sed -i "$replace" etc/mesh_bot_w3.service
replace="s|Group=pi|Group=$whoami|g"
sed -i $replace etc/pong_bot.service
sed -i $replace etc/mesh_bot.service
sed -i $replace etc/mesh_bot_reporting.service
sed -i $replace etc/mesh_bot_w3.service
sed -i "$replace" etc/pong_bot.service
sed -i "$replace" etc/mesh_bot.service
sed -i "$replace" etc/mesh_bot_reporting.service
sed -i "$replace" etc/mesh_bot_w3.service
printf "\n service files updated\n"
if [[ $(echo "${bot}" | grep -i "^p") ]]; then
@@ -355,7 +355,7 @@ else
else
printf "\nCron job already exists, skipping\n"
fi
printf "Reference following commands:\n\n" "$service" > install_notes.txt
printf "Reference following commands:\n\n" > install_notes.txt
printf "sudo systemctl status %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl start %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl restart %s.service\n\n" "$service" >> install_notes.txt

View File

@@ -5,12 +5,12 @@
cd "$(dirname "$0")"
if [ ! -f "config.ini" ]; then
if [[ ! -f "config.ini" ]]; then
cp config.template config.ini
fi
# activate the virtual environment if it exists
if [ -d "venv" ]; then
if [[ -d "venv" ]]; then
source venv/bin/activate
else
echo "Virtual environment not found, this tool just launches the .py in venv"
@@ -22,9 +22,9 @@ if [[ "$1" == pong* ]]; then
python3 pong_bot.py
elif [[ "$1" == mesh* ]]; then
python3 mesh_bot.py
elif [ "$1" == "html" ]; then
elif [[ "$1" == "html" ]]; then
python3 etc/report_generator.py
elif [ "$1" == "html5" ]; then
elif [[ "$1" == "html5" ]]; then
python3 etc/report_generator5.py
elif [[ "$1" == add* ]]; then
python3 script/addFav.py

View File

@@ -68,6 +68,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
"leaderboard": lambda: get_mesh_leaderboard(message, message_from_id, deviceID),
"lemonstand": lambda: handleLemonade(message, message_from_id, deviceID),
"lheard": lambda: handle_lheard(message, message_from_id, deviceID, isDM),
"map": lambda: mapHandler(message_from_id, deviceID, channel_number, message, snr, rssi, hop),
"mastermind": lambda: handleMmind(message, message_from_id, deviceID),
"messages": lambda: handle_messages(message, deviceID, channel_number, msg_history, publicChannel, isDM),
"moon": lambda: handle_moon(message_from_id, deviceID, channel_number),
@@ -253,12 +254,12 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
# append SNR/RSSI or hop info
if hop.startswith("Gateway") or hop.startswith("MQTT"):
msg += f" [GW]"
msg += " [GW]"
elif hop.startswith("Direct"):
msg += f" [RF]"
msg += " [RF]"
else:
#flood
msg += f" [F]"
msg += " [F]"
if (float(snr) != 0 or float(rssi) != 0) and "Hops" not in hop:
msg += f"\nSNR:{snr} RSSI:{rssi}"
@@ -527,7 +528,8 @@ def handle_satpass(message_from_id, deviceID, message='', vox=False):
userList = message.split("satpass ")[1].split(" ")[0]
#split userList and make into satList overrided the config.ini satList
satList = userList.split(",")
except:
except Exception as e:
logger.error(f"Exception occurred: {e}")
return "example use:🛰satpass 25544,33591"
# Detailed satellite pass
@@ -904,8 +906,8 @@ def handleGolf(message, nodeID, deviceID):
if last_cmd == "new" and nodeID != 0:
# create new player
msg = f"Welcome to 🏌GolfSim⛳\n"
msg += f"Clubs: (D)river, (L)ow Iron, (M)id Iron, (H)igh Iron, (G)ap Wedge, Lob (W)edge (C)addie\n"
msg = "Welcome to 🏌GolfSim⛳\n"
msg += "Clubs: (D)river, (L)ow Iron, (M)id Iron, (H)igh Iron, (G)ap Wedge, Lob (W)edge (C)addie\n"
msg += playGolf(nodeID=nodeID, message=message, last_cmd=last_cmd)
return msg
@@ -1316,7 +1318,7 @@ def handle_lheard(message, nodeid, deviceID, isDM):
else:
# trim the last \n
bot_response = bot_response[:-1]
# get count of nodes heard
bot_response += f"\n👀In Mesh: {len(seenNodes)}"
@@ -1327,7 +1329,7 @@ def handle_history(message, nodeid, deviceID, isDM, lheard=False):
global cmdHistory, lheardCmdIgnoreNode, bbs_admin_list
msg = ""
buffer = []
if "?" in message and isDM:
return message.split("?")[0].title() + " command returns a list of commands received."
@@ -1367,7 +1369,7 @@ def handle_history(message, nodeid, deviceID, isDM, lheard=False):
for j in range(len(buffer)):
if buffer[j][0] == nodeName:
buffer[j] = (nodeName, prettyTime)
# create the message from the buffer list
buffer.reverse() # reverse the list to show the latest first
for i in range(0, len(buffer)):
@@ -1411,11 +1413,11 @@ def handle_whoami(message_from_id, deviceID, hop, snr, rssi, pkiStatus):
msg += f"I see the signal strength is {rssi} and the SNR is {snr} with hop count of {hop}"
if pkiStatus[1] != 'ABC':
msg += f"\nYour PKI bit is {pkiStatus[0]} pubKey: {pkiStatus[1]}"
loc = get_node_location(message_from_id, deviceID)
if loc != [latitudeValue, longitudeValue]:
msg += f"\nYou are at: lat:{loc[0]} lon:{loc[1]}"
# check the positionMetadata for nodeID and get metadata
if positionMetadata and message_from_id in positionMetadata:
metadata = positionMetadata[message_from_id]
@@ -1498,20 +1500,20 @@ def onReceive(packet, interface):
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 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 == '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)
if globals().get(f'port{i}', '') in rxInterface),None)
if rxType == 'BLEInterface':
rxNode = next(
(i for i in range(1, 10)
if globals().get(f'interface{i}_type', '') == 'ble'),0)
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
@@ -1556,7 +1558,6 @@ def onReceive(packet, interface):
# BBS DM MAIL CHECKER
if bbs_enabled and 'decoded' in packet:
msg = bbs_check_dm(message_from_id)
if msg:
logger.info(f"System: BBS DM Delivery: {msg[1]} For: {get_name_from_number(message_from_id, 'long', rxNode)}")
@@ -1699,7 +1700,7 @@ def onReceive(packet, interface):
else:
# respond with help message on DM
send_message(help_message, channel_number, message_from_id, rxNode)
# log the message to the message log
if log_messages_to_file:
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | DM | " + message_string.replace('\n', '-nl-'))
@@ -1756,7 +1757,7 @@ def onReceive(packet, interface):
if log_messages_to_file:
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
# repeat the message on the other device
# repeat the message on the other device
if repeater_enabled and multiple_interface:
# wait a responseDelay to avoid message collision from lora-ack.
time.sleep(responseDelay)

View File

@@ -19,7 +19,7 @@ Updated Oct-2025 "ver 1.9.8.4"
- [Voice Commands (VOX)](#voice-commands-vox)
- [Ollama LLM/AI](#ollama-llmai)
- [Wikipedia Search](#wikipedia-search)
- [Scheduler](#scheduler)
- [Scheduler](#-mesh-bot-scheduler-user-guide)
- [Other Utilities](#other-utilities)
- [Configuration](#configuration)
- [Troubleshooting](#troubleshooting)
@@ -37,6 +37,37 @@ See [modules/adding_more.md](adding_more.md) for developer notes.
## Networking
### ping / pinging / test / testing / ack
- **Usage:** `ping`, `pinging`, `test`, `testing`, `ack`, `ping @user`, `ping #tag`
- **Description:** Sends a ping to the bot. The bot responds with signal information such as SNR (Signal-to-Noise Ratio), RSSI (Received Signal Strength Indicator), and hop count. Used for making field report etc.
- **Targeted Ping:**
You can direct a ping to a specific user or group by mentioning their short name or tag:
- `ping @NODE` — Pings a Joke to specific node by its short name.
- **Example:**
```
ping
```
Response:
```
SNR: 12.5, RSSI: -80, Hops: 2
```
```
ping @Top of the hill
```
Response:
```
PING @Top of the hill SNR: 10.2, RSSI: -85, Hops: 1
```
- **Help:**
Send `ping?` in a Direct Message (DM) for usage instructions.
---
### Notes
- You can mention users or tags in your ping/test messages (e.g., `ping @user` or `ping #group`) to target specific nodes or groups.
- Some commands may only be available in Direct Messages, depending on configuration.
| Command | Description | ✅ Works Off-Grid |
|--------------|-------------|------------------|
| `ping`, `ack` | Return data for signal. Example: `ping 15 #DrivingI5` (activates auto-ping every 20 seconds for count 15 via DM only) you can also ping @NODE short name and if BBS DM enabled it will send them a joke | ✅ |
@@ -92,6 +123,8 @@ Enable/disable games in `[games]` section of `config.ini`.
Enable in `[bbs]` section of `config.ini`.
more at [meshBBS: How-To & API Documentation](bbstools.md)
---
## Checklist
@@ -127,6 +160,40 @@ Enable in `[checklist]` section of `config.ini`.
Configure in `[location]` section of `config.ini`.
Certainly! Heres a README help section for your `mapHandler` command, suitable for users of your meshbot:
---
## 📍 Map Command
The `map` command allows you to log your current GPS location with a custom description. This is useful for mapping mesh nodes, events, or points of interest.
### Usage
- **Show Help**
```
map help
```
Displays usage instructions for the map command.
- **Log a Location**
```
map <description>
```
Example:
```
map Found a new mesh node near the park
```
This will log your current location with the description "Found a new mesh node near the park".
### How It Works
- The bot records your user ID, latitude, longitude, and your description in a CSV file (`data/map_data.csv`).
- If your location data is missing or invalid, youll receive an error message.
- You can view or process the CSV file later for mapping or analysis.
**Tip:** Use `map help` at any time to see these instructions in the bot.
---
## EAS & Emergency Alerts
@@ -201,7 +268,7 @@ Configure in `[wikipedia]` section of `config.ini`.
---
## Scheduler
## 📅 Mesh Bot Scheduler User Guide
Automate messages and tasks using the scheduler module.
@@ -239,6 +306,76 @@ To send a daily message at 09:00:
- All scheduled jobs run asynchronously as long as the bot is running.
- For troubleshooting, check the logs for scheduler activity and errors.
### Basic Scheduler Options
You can schedule messages or actions using the following options in your configuration:
- **day**: Run every day at a specific time or every N days.
- **mon, tue, wed, thu, fri, sat, sun**: Run on a specific day of the week at a specific time.
- **hour**: Run every N hours.
- **min**: Run every N minutes.
#### **Examples:**
| Option | Time/Interval | What it does |
|-------------|--------------|---------------------------------------------------|
| `day` | `08:00` | Runs every day at 8:00 AM |
| `day` | `2` | Runs every 2 days |
| `mon` | `09:30` | Runs every Monday at 9:30 AM |
| `hour` | `1` | Runs every hour |
| `min` | `30` | Runs every 30 minutes |
- If you specify a day (e.g., `mon`) and a time (e.g., `09:30`), the message will be sent at that time on that day.
- If you specify `hour` or `min`, set the interval (e.g., every 2 hours or every 15 minutes).
---
### Special Scheduler Options
#### **joke**
- Schedules the bot to send a random joke at the specified interval.
- **Example:**
- Option: `joke`
- Interval: `60`
- → Sends a joke every 60 minutes.
#### **link**
- Schedules the bot to send a satellite link message at the specified interval (in hours).
- **Example:**
- Option: `link`
- Interval: `2`
- → Sends a bbslink message every 2 hours.
#### **weather**
- Schedules the bot to send a weather update at the specified interval (in hours).
- **Example:**
- Option: `weather`
- Interval: `3`
- → Sends a weather update every 3 hours.
---
### Days of the Week
You can use any of these options to schedule messages on specific days:
- `mon`, `tue`, `wed`, `thu`, `fri`, `sat`, `sun`
**Example:**
- Option: `fri`
- Time: `17:00`
- → Sends the message every Friday at 5:00 PM.
---
### Configuration Fields
- **schedulerValue**: The schedule type (e.g., `day`, `joke`, `weather`, `mon`, etc.)
- **schedulerTime**: The time to run (e.g., `08:00`). Leave blank for interval-based schedules.
- **schedulerInterval**: The interval (e.g., `2` for every 2 hours/days/minutes).
- **schedulerChannel**: The channel number to send to.
- **schedulerInterface**: The device/interface number.
---
## Other Utilities

189
modules/bbstools.md Normal file
View File

@@ -0,0 +1,189 @@
---
# 📡 meshBBS: How-To & API Documentation
This document covers the Bulliten Board System or BBS componment of the meshing-around project.
## Table of Contents
1. [BBS Core Functions](#1-bbs-core-functions)
- [Direct Messages (DMs)](#11-direct-messages-dms)
2. [BBS Database Sync: File-Based (Out-of-Band)](#1-bbs-database-sync-file-based-out-of-band)
3. [BBS Over-the-Air (OTA) Sync: Linking](#2-bbs-over-the-air-ota-sync-linking)
4. [Scheduling BBS Sync](#3-scheduling-bbs-sync)
5. [Best Practices](#4-best-practices)
6. [Example: Full Sync Workflow](#5-example-full-sync-workflow)
7. [Troubleshooting](#6-troubleshooting)
8. [API Reference: BBS Sync](#7-api-reference-bbs-sync)
## 1. **BBS Core Functions**
## 1.1 **Direct Messages (DMs)**
### **How DMs Work**
- Direct Messages (DMs) are private messages sent from one node to another.
- DMs are stored separately from public posts in `data/bbsdm.pkl`.
- Each DM entry in the pickle, typically includes: `[id, toNode, message, fromNode, timestamp, threadID, replytoID]`.
### **DM Delivery**
- When a DM is posted using `bbs_post_dm(toNode, message, fromNode)`, it is added to the recipient's DM database.
- DMs can be delivered in two ways:
1. **File-Based Sync:**
- The `bbsdm.pkl` file is copied between nodes using SCP, rsync, or other file transfer methods.
- After syncing, the recipient node can check for new DMs using `bbs_check_dm(toNode)`.
2. **Over-the-Air (OTA) Sync:**
- DMs can be exchanged between nodes using the same OTA sync mechanism as other posts.
- The bot will receive (onRX) or detect any packet and deliver the DM/mail to the recipient.
- DMs are only visible to the intended recipient node and are not listed in the public message list.
### **DM Commands**
| Command | Description |
|-----------------|---------------------------------------------|
| `bbs_post_dm` | Send a direct message to another node |
| `bbs_check_dm` | Check for new DMs for your node |
| `bbs_delete_dm` | Delete a DM after reading |
---
### **Message Storage**
The .. database is
- Messages are stored in `data/bbsdb.pkl` (public posts) and `data/bbsdm.pkl` (direct messages).
- Format: Each message is a list, e.g. `[id, subject, body, fromNode, timestamp, threadID, replytoID]`.
| Command | Description |
|--------------|-----------------------------------------------|
| `bbshelp` | Show BBS help |
| `bbslist` | List messages |
| `bbsread` | Read a message by ID |
| `bbspost` | Post a message or DM |
| `bbsdelete` | Delete a message |
| `bbsinfo` | BBS stats (sysop) |
| `bbslink` | Link messages between BBS systems |
---
Enable in `[bbs]` section of `config.ini`.
## 1. **BBS Database Sync: File-Based (Out-of-Band)**
### **Manual/Automated File Sync (e.g., SSH/SCP)**
- **Purpose:** Sync BBS data between nodes by copying `bbsdb.pkl` and `bbsdm.pkl` files.
- **How-To:**
1. **Locate Files:**
- `data/bbsdb.pkl` (public posts)
- `data/bbsdm.pkl` (direct messages)
2. **Copy Files:**
Use `scp` or `rsync` to copy files between nodes:
```sh
scp user@remote:/path/to/meshing-around/data/bbsdb.pkl ./data/bbsdb.pkl
scp user@remote:/path/to/meshing-around/data/bbsdm.pkl ./data/bbsdm.pkl
```
3. **Reload Database:**
After copying, when the "API" is enabled the watchdog will look for changes and injest.
- **Automating with Cron/Scheduler:**
- Set up a cron job or use the bots scheduler to periodically pull/push files.
---
## 2. **BBS Over-the-Air (OTA) Sync: Linking**
### **How OTA Sync Works**
- Nodes can exchange BBS messages using special commands over the mesh network.
- Uses `bbslink` and `bbsack` commands for message exchange.
- Future supports compression for bandwidth efficiency.
### **Enabling BBS Linking**
- Set `bbs_link_enabled = True` in your config.
- Optionally, set `bbs_link_whitelist` to restrict which nodes can sync.
### **Manual Sync Command**
- To troubleshoot request sync from another node, send:
```
bbslink <messageID> $<subject> #<body>
```
- The receiving node will respond with `bbsack <messageID>`.
### **Out-of-Band Channel**
- For high-reliability sync, configure a dedicated channel (not used for chat).
---
## 3. **Scheduling BBS Sync**
### **Using the Bots Scheduler**
- You can schedule periodic sync requests to a peer node.
- Example: Every hour, send a `bbslink` request to a peer.
see more at [Module Readme](README.md#scheduler)
---
## 4. **Best Practices**
- **Backup:** Regularly back up `bbsdb.pkl` and `bbsdm.pkl`.
- **Security:** Use SSH keys for file transfer; restrict OTA sync to trusted nodes.
- **Reliability:** Use a dedicated channel for BBS sync to avoid chat congestion.
- **Automation:** Use the scheduler for regular syncs, both file-based and OTA.
---
## 5. **Example: Full Sync Workflow**
1. **Set up a dedicated sync channel** (e.g., channel bot-admin).
2. **Configure both nodes** with `bbs_link_enabled = True` and add each other to `bbs_link_whitelist`.
3. **Schedule sync** every hour:
- Node A sends `bbslink 0` to Node B on channel 99.
- Node B responds with messages and `bbsack`.
4. **Optionally, use SSH/scp** to copy `bbsdb.pkl` for full out-of-band backup.
---
## 6. **Troubleshooting**
- **Messages not syncing?**
- Check `bbs_link_enabled` and whitelist settings.
- Ensure both nodes are on the same sync channel.
- Check logs for errors.
- **File sync issues?**
- Verify file permissions and paths.
- Ensure the bot reloads the database after file copy.
## 7. **API Reference: BBS Sync**
### **Key Functions in Python**
| Function | Purpose | Usage Example |
|-------------------------|-------------------------------------------|----------------------------------------------------|
| `bbs_post_message()` | Post a new public message | `bbs_post_message(subject, body, fromNode)` |
| `bbs_read_message()` | Read a message by ID | `bbs_read_message(messageID)` |
| `bbs_delete_message()` | Delete a message (admin/owner only) | `bbs_delete_message(messageID, fromNode)` |
| `bbs_list_messages()` | List all message subjects | `bbs_list_messages()` |
| `bbs_post_dm()` | Post a direct message | `bbs_post_dm(toNode, message, fromNode)` |
| `bbs_check_dm()` | Check for DMs for a node | `bbs_check_dm(toNode)` |
| `bbs_delete_dm()` | Delete a DM after reading | `bbs_delete_dm(toNode, message)` |
| `get_bbs_stats()` | Get stats on BBS and DMs | `get_bbs_stats()` |
| Function | Purpose |
|---------------------------|-------------------------------------------|
| `bbs_sync_posts()` | Handles incoming/outgoing sync requests |
| `bbs_receive_compressed()`| Handles compressed sync data |
| `compress_data()` | Compresses data for OTA transfer |
| `decompress_data()` | Decompresses received data |
### **Handle Incoming Sync**
- The bot automatically processes incoming `bbslink` and `bbsack` commands via `bbs_sync_posts()`.
### **Compressed Sync**
Future Use
- If `useSynchCompression` is enabled, use:
```python
compressed = compress_data(msg)
send_raw_bytes(peerNode, compressed)
```
- Receiving node uses `bbs_receive_compressed()`.
---
---

View File

@@ -100,8 +100,8 @@ def tableOfContents():
'plant': '🌱', 'tree': '🌳', 'flower': '🌸', 'leaf': '🍃', 'cactus': '🌵', 'mushroom': '🍄', 'herb': '🌿', 'bamboo': '🎍', 'rose': '🌹', 'tulip': '🌷', 'sunflower': '🌻',
'hibiscus': '🌺', 'cherry blossom': '🌸', 'bouquet': '💐', 'seedling': '🌱', 'palm tree': '🌴', 'evergreen tree': '🌲', 'deciduous tree': '🌳', 'fallen leaf': '🍂', 'maple leaf': '🍁',
'ear of rice': '🌾', 'shamrock': '☘️', 'four leaf clover': '🍀', 'grapes': '🍇', 'melon': '🍈', 'watermelon': '🍉', 'tangerine': '🍊', 'lemon': '🍋', 'banana': '🍌', 'pineapple': '🍍',
'mango': '🥭', 'apple': '🍎', 'green apple': '🍏', 'pear': '🍐', 'peach': '🍑', 'cherries': '🍒', 'strawberry': '🍓', 'kiwi': '🥝', 'tomato': '🍅', 'coconut': '🥥', 'avocado': '🥑',
'eggplant': '🍆', 'potato': '🥔', 'carrot': '🥕', 'corn': '🌽', 'hot pepper': '🌶️', 'cucumber': '🥒', 'leafy green': '🥬', 'broccoli': '🥦', 'garlic': '🧄', 'onion': '🧅',
'green apple': '🍏', 'pear': '🍐', 'peach': '🍑', 'cherries': '🍒', 'strawberry': '🍓', 'kiwi': '🥝', 'tomato': '🍅', 'coconut': '🥥', 'avocado': '🥑',
'hot pepper': '🌶️', 'cucumber': '🥒', 'leafy green': '🥬', 'broccoli': '🥦', 'garlic': '🧄', 'onion': '🧅',
'peanuts': '🥜', 'chestnut': '🌰', 'bread': '🍞', 'croissant': '🥐', 'baguette': '🥖', 'flatbread': '🥙', 'pretzel': '🥨', 'bagel': '🥯', 'pancakes': '🥞', 'waffle': '🧇', 'cheese': '🧀',
'meat': '🍖', 'poultry': '🍗', 'bacon': '🥓', 'hamburger': '🍔', 'fries': '🍟', 'pizza': '🍕', 'hot dog': '🌭', 'sandwich': '🥪', 'taco': '🌮', 'burrito': '🌯', 'tamale': '🫔',
'stuffed flatbread': '🥙', 'falafel': '🧆', 'egg': '🥚', 'fried egg': '🍳', 'shallow pan of food': '🥘', 'pot of food': '🍲', 'fondue': '🫕', 'bowl with spoon': '🥣', 'green salad': '🥗',
@@ -115,19 +115,19 @@ def tableOfContents():
'globe with meridians': '🌐', 'world map': '🗺️', 'mountain': '⛰️', 'volcano': '🌋', 'mount fuji': '🗻', 'camping': '🏕️', 'beach with umbrella': '🏖️', 'desert': '🏜️', 'desert island': '🏝️',
'national park': '🏞️', 'stadium': '🏟️', 'classical building': '🏛️', 'building construction': '🏗️', 'brick': '🧱', 'rock': '🪨', 'wood': '🪵', 'hut': '🛖', 'houses': '🏘️', 'derelict house': '🏚️',
'house with garden': '🏡', 'office building': '🏢', 'japanese post office': '🏣', 'post office': '🏤', 'hospital': '🏥', 'bank': '🏦', 'hotel': '🏨', 'love hotel': '🏩', 'convenience store': '🏪',
'school': '🏫', 'department store': '🏬', 'factory': '🏭', 'japanese castle': '🏯', 'castle': '🏰', 'wedding': '💒', 'tokyo tower': '🗼', 'statue of liberty': '🗽', 'church': '', 'mosque': '🕌',
'hindu temple': '🛕', 'synagogue': '🕍', 'shinto shrine': '⛩️', 'kaaba': '🕋', 'fountain': '', 'tent': '', 'foggy': '🌁', 'night with stars': '🌃', 'sunrise over mountains': '🌄', 'sunrise': '🌅',
'cityscape at dusk': '🌆', 'sunset': '🌇', 'cityscape': '🏙️', 'bridge at night': '🌉', 'hot springs': '♨️', 'carousel horse': '🎠', 'ferris wheel': '🎡', 'roller coaster': '🎢', 'barber pole': '💈',
'castle': '🏰', 'wedding': '💒', 'tokyo tower': '🗼', 'statue of liberty': '🗽', 'church': '', 'mosque': '🕌',
'fountain': '', 'tent': '', 'foggy': '🌁', 'night with stars': '🌃', 'sunrise over mountains': '🌄', 'sunrise': '🌅',
'cityscape at dusk': '🌆', 'sunset': '🌇', 'cityscape': '🏙️', 'bridge at night': '🌉', 'hot springs': '♨️', 'carousel horse': '🎠', 'barber pole': '💈',
'robot': '🤖', 'alien': '👽', 'ghost': '👻', 'skull': '💀', 'pumpkin': '🎃', 'clown': '🤡', 'wizard': '🧙', 'elf': '🧝', 'fairy': '🧚', 'mermaid': '🧜', 'vampire': '🧛',
'zombie': '🧟', 'genie': '🧞', 'superhero': '🦸', 'supervillain': '🦹', 'mage': '🧙', 'knight': '🛡️', 'ninja': '🥷', 'pirate': '🏴‍☠️', 'angel': '👼', 'devil': '😈', 'dragon': '🐉',
'unicorn': '🦄', 'phoenix': '🦅', 'griffin': '🦅', 'centaur': '🐎', 'minotaur': '🐂', 'cyclops': '👁️', 'medusa': '🐍', 'sphinx': '🦁', 'kraken': '🦑', 'yeti': '❄️', 'sasquatch': '🦧',
'loch ness monster': '🦕', 'chupacabra': '🐐', 'banshee': '👻', 'golem': '🗿', 'djinn': '🧞', 'basilisk': '🐍', 'hydra': '🐉', 'cerberus': '🐶', 'chimera': '🐐', 'manticore': '🦁', 'wyvern': '🐉',
'pegasus': '🦄', 'hippogriff': '🦅', 'kelpie': '🐎', 'selkie': '🦭', 'kitsune': '🦊', 'tanuki': '🦝', 'tengu': '🦅', 'oni': '👹', 'yokai': '👻', 'kappa': '🐢', 'yurei': '👻',
'kami': '👼', 'shinigami': '💀', 'bakemono': '👹', 'tsukumogami': '🧸', 'noppera-bo': '👤', 'rokurokubi': '🧛', 'yuki-onna': '❄️', 'jorogumo': '🕷️', 'nue': '🐍', 'ubume': '👼',
'atom': '⚛️', 'dna': '🧬', 'microscope': '🔬', 'telescope': '🔭', 'rocket': '🚀', 'satellite': '🛰️', 'spaceship': '🛸', 'planet': '🪐', 'black hole': '🕳️', 'galaxy': '🌌',
'comet': '☄️', 'constellation': '🌠', 'lightning': '', 'magnet': '🧲', 'battery': '🔋', 'computer': '💻', 'keyboard': '⌨️', 'mouse': '🖱️', 'printer': '🖨️', 'floppy disk': '💾',
'atom': '⚛️', 'dna': '🧬', 'microscope': '🔬', 'telescope': '🔭', 'satellite': '🛰️', 'spaceship': '🛸', 'planet': '🪐', 'black hole': '🕳️', 'galaxy': '🌌',
'constellation': '🌠', 'lightning': '', 'magnet': '🧲', 'computer': '💻', 'keyboard': '⌨️', 'mouse': '🖱️', 'printer': '🖨️', 'floppy disk': '💾',
'cd': '💿', 'dvd': '📀', 'smartphone': '📱', 'tablet': '📲', 'watch': '', 'camera': '📷', 'video camera': '📹', 'projector': '📽️', 'radio': '📻', 'television': '📺',
'satellite dish': '📡', 'game controller': '🎮', 'joystick': '🕹️', 'vr headset': '🕶️', 'headphones': '🎧', 'speaker': '🔊', 'flashlight': '🔦', 'circuit': '🔌', 'chip': '💻',
'satellite dish': '📡', 'game controller': '🎮', 'joystick': '🕹️', 'vr headset': '🕶️', 'headphones': '🎧', 'speaker': '🔊', 'circuit': '🔌', 'chip': '💻',
'server': '🖥️', 'database': '💾', 'cloud': '☁️', 'network': '🌐', 'code': '💻', 'bug': '🐛', 'virus': '🦠', 'bacteria': '🦠', 'lab coat': '🥼', 'safety goggles': '🥽',
'test tube': '🧪', 'petri dish': '🧫', 'beaker': '🧪', 'bunsen burner': '🔥', 'graduated cylinder': '🧪', 'pipette': '🧪', 'scalpel': '🔪', 'syringe': '💉', 'pill': '💊',
'stethoscope': '🩺', 'thermometer': '🌡️', 'x-ray': '🩻', 'brain': '🧠', 'heart': '❤️', 'lung': '🫁', 'bone': '🦴', 'muscle': '💪', 'robot arm': '🦾', 'robot leg': '🦿',
@@ -136,21 +136,18 @@ def tableOfContents():
'spider': '🕷️', 'scorpion': '🦂', 'turkey': '🦃', 'peacock': '🦚', 'parrot': '🦜', 'swan': '🦢', 'flamingo': '🦩', 'dodo': '🦤', 'sloth': '🦥', 'otter': '🦦',
'skunk': '🦨', 'kangaroo': '🦘', 'badger': '🦡', 'beaver': '🦫', 'bison': '🦬', 'mammoth': '🦣', 'raccoon': '🦝', 'hedgehog': '🦔', 'squirrel': '🐿️', 'chipmunk': '🐿️',
'porcupine': '🦔', 'llama': '🦙', 'giraffe': '🦒', 'zebra': '🦓', 'hippopotamus': '🦛', 'rhinoceros': '🦏', 'gorilla': '🦍', 'orangutan': '🦧', 'elephant': '🐘', 'camel': '🐫',
'llama': '🦙', 'alpaca': '🦙', 'buffalo': '🐃', 'ox': '🐂', 'deer': '🦌', 'moose': '🦌', 'reindeer': '🦌', 'goat': '🐐', 'sheep': '🐑', 'ram': '🐏', 'lamb': '🐑', 'horse': '🐴',
'unicorn': '🦄', 'zebra': '🦓', 'cow': '🐄', 'pig': '🐖', 'boar': '🐗', 'mouse': '🐁', 'rat': '🐀', 'hamster': '🐹', 'rabbit': '🐇', 'chipmunk': '🐿️', 'beaver': '🦫', 'hedgehog': '🦔',
'bat': '🦇', 'bear': '🐻', 'koala': '🐨', 'panda': '🐼', 'sloth': '🦥', 'otter': '🦦', 'skunk': '🦨', 'kangaroo': '🦘', 'badger': '🦡', 'turkey': '🦃', 'chicken': '🐔', 'rooster': '🐓',
'peacock': '🦚', 'parrot': '🦜', 'swan': '🦢', 'flamingo': '🦩', 'dodo': '🦤', 'crocodile': '🐊', 'turtle': '🐢', 'lizard': '🦎', 'snake': '🐍', 'dragon': '🐉', 'sauropod': '🦕', 't-rex': '🦖',
'whale': '🐋', 'dolphin': '🐬', 'fish': '🐟', 'blowfish': '🐡', 'shark': '🦈', 'octopus': '🐙', 'shell': '🐚', 'crab': '🦀', 'lobster': '🦞', 'shrimp': '🦐', 'squid': '🦑', 'snail': '🐌', 'butterfly': '🦋',
'bee': '🐝', 'beetle': '🐞', 'ant': '🐜', 'cricket': '🦗', 'spider': '🕷️', 'scorpion': '🦂', 'mosquito': '🦟', 'microbe': '🦠', 'locomotive': '🚂', 'arm': '💪', 'leg': '🦵', 'sponge': '🧽',
'toothbrush': '🪥', 'broom': '🧹', 'basket': '🧺', 'roll of paper': '🧻', 'bucket': '🪣', 'soap': '🧼', 'toilet paper': '🧻', 'shower': '🚿', 'bathtub': '🛁', 'razor': '🪒', 'lotion': '🧴',
'letter': '✉️', 'envelope': '✉️', 'mail': '📬', 'post': '📮', 'golf': '⛳️', 'golfing': '⛳️', 'office': '🏢', 'meeting': '📅', 'presentation': '📊', 'report': '📄', 'document': '📄',
'alpaca': '🦙', 'buffalo': '🐃', 'ox': '🐂', 'deer': '🦌', 'moose': '🦌', 'reindeer': '🦌', 'goat': '🐐', 'sheep': '🐑', 'ram': '🐏', 'lamb': '🐑', 'horse': '🐴',
'rat': '🐀', 'hedgehog': '🦔', 'chicken': '🐔', 'rooster': '🐓', 'crocodile': '🐊', 'turtle': '🐢', 'lizard': '🦎', 'dragon': '🐉', 'sauropod': '🦕', 't-rex': '🦖', 'butterfly': '🦋',
'mosquito': '🦟', 'microbe': '🦠', 'locomotive': '🚂', 'arm': '💪', 'leg': '🦵', 'sponge': '🧽',
'toothbrush': '🪥', 'roll of paper': '🧻', 'soap': '🧼', 'toilet paper': '🧻', 'shower': '🚿', 'bathtub': '🛁', 'razor': '🪒', 'lotion': '🧴',
'letter': '✉️', 'envelope': '✉️', 'mail': '📬', 'post': '📮', 'golf': '⛳️', 'golfing': '⛳️', 'meeting': '📅', 'presentation': '📊', 'report': '📄', 'document': '📄',
'file': '📁', 'folder': '📂', 'sports': '🏅', 'athlete': '🏃', 'competition': '🏆', 'race': '🏁', 'tournament': '🏆', 'champion': '🏆', 'medal': '🏅', 'victory': '🏆', 'win': '🏆', 'lose': '😞',
'draw': '🤝', 'team': '👥', 'player': '👤', 'coach': '👨‍🏫', 'referee': '🧑‍⚖️', 'stadium': '🏟️', 'arena': '🏟️', 'field': '🏟️', 'court': '🏟️', 'track': '🏟️', 'gym': '🏋️', 'fitness': '🏋️', 'exercise': '🏋️',
'workout': '🏋️', 'training': '🏋️', 'practice': '🏋️', 'game': '🎮', 'match': '🎮', 'score': '🏅', 'goal': '🥅', 'point': '🏅', 'basket': '🏀', 'home run': '⚾️', 'strike': '🎳', 'spare': '🎳', 'frame': '🎳',
'inning': '⚾️', 'quarter': '🏈', 'half': '🏈', 'overtime': '🏈', 'penalty': '⚽️', 'foul': '⚽️', 'timeout': '⏱️', 'substitute': '🔄', 'bench': '🪑', 'sideline': '🏟️', 'dugout': '⚾️', 'locker room': '🚪', 'shower': '🚿',
'uniform': '👕', 'jersey': '👕', 'cleats': '👟', 'helmet': '⛑️', 'pads': '🛡️', 'gloves': '🧤', 'bat': '⚾️', 'ball': '⚽️', 'puck': '🏒', 'stick': '🏒', 'net': '🥅', 'hoop': '🏀', 'goalpost': '🥅', 'whistle': '🔔',
'scoreboard': '📊', 'fans': '👥', 'crowd': '👥', 'cheer': '📣', 'boo': '😠', 'applause': '👏', 'celebration': '🎉', 'parade': '🎉', 'trophy': '🏆', 'medal': '🏅', 'ribbon': '🎀', 'cup': '🏆', 'championship': '🏆',
'league': '🏆', 'season': '🏆', 'playoffs': '🏆', 'finals': '🏆', 'champion': '🏆', 'runner-up': '🥈', 'third place': '🥉', 'snowman': '☃️', 'snowmen': '⛄️'
'league': '🏆', 'season': '🏆', 'playoffs': '🏆', 'finals': '🏆', 'runner-up': '🥈', 'third place': '🥉', 'snowman': '☃️', 'snowmen': '⛄️'
}
return wordToEmojiMap

View File

@@ -16,7 +16,7 @@ def get_govUK_alerts(lat, lon):
try:
# get UK.gov alerts
url = 'https://www.gov.uk/alerts'
response = requests.get(url)
response = requests.get(url, timeout=urlTimeoutSeconds)
soup = bs.BeautifulSoup(response.text, 'html.parser')
# the alerts are in <h2 class="govuk-heading-m" id="alert-status">
alert = soup.find('h2', class_='govuk-heading-m', id='alert-status')
@@ -35,7 +35,7 @@ def get_nina_alerts():
alerts = []
for regionalKey in myRegionalKeysDE:
url = ("https://nina.api.proxy.bund.dev/api31/dashboard/" + regionalKey + ".json")
response = requests.get(url)
response = requests.get(url, timeout=urlTimeoutSeconds)
data = response.json()
for item in data:
@@ -53,7 +53,7 @@ def get_wxUKgov():
try:
# get UK weather warnings
url = 'https://www.metoffice.gov.uk/weather/guides/rss'
response = requests.get(url)
response = requests.get(url, timeout=urlTimeoutSeconds)
soup = bs.BeautifulSoup(response.content, 'xml')
items = soup.find_all('item')

View File

@@ -10,9 +10,11 @@ import xml.dom.minidom
from datetime import datetime
from modules.log import *
import math
import csv
import os
trap_list_location = ("whereami", "wx", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow", "valert", "earthquake", "howfar")
trap_list_location = ("whereami", "wx", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow", "valert", "earthquake", "howfar", "map",)
def where_am_i(lat=0, lon=0, short=False, zip=False):
whereIam = ""
@@ -133,7 +135,7 @@ def getArtSciRepeaters(lat=0, lon=0):
if zipCode.isnumeric():
try:
artsci_url = f"http://www.artscipub.com/mobile/showstate.asp?zip={zipCode}"
response = requests.get(artsci_url)
response = requests.get(artsci_url, timeout=urlTimeoutSeconds)
if response.status_code!=200:
logger.error(f"Location:Error fetching data from {artsci_url} with status code {response.status_code}")
soup = bs.BeautifulSoup(response.text, 'html.parser')
@@ -1054,3 +1056,89 @@ def get_openskynetwork(lat=0, lon=0):
aircraft_report = aircraft_report[:-1]
aircraft_report = abbreviate_noaa(aircraft_report)
return aircraft_report if aircraft_report else NO_ALERTS
def log_locationData_toMap(userID, location, message):
"""
Logs location data to a CSV file for meshing purposes.
Returns True if successful, False otherwise.
"""
lat, lon = location
if lat is None or lon is None or lat == 0.0 or lon == 0.0:
return False
# Set default directory to ../data/
default_dir = os.path.join(os.path.dirname(__file__), "..", "data")
os.makedirs(default_dir, exist_ok=True)
map_filepath = os.path.join(default_dir, "map_data.csv")
# Check if the file exists to determine if we need to write headers
write_header = not os.path.isfile(map_filepath) or os.path.getsize(map_filepath) == 0
try:
with open(map_filepath, mode='a', newline='') as csvfile:
fieldnames = ['userID', 'Latitude', 'Longitude', 'Description']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
# Write headers only if the file did not exist before or is empty
if write_header:
writer.writeheader()
writer.writerow({
'userID': userID,
'Latitude': lat,
'Longitude': lon,
'Description': message if message else ""
})
logger.debug(f"Logged location for {userID} to {map_filepath}")
return True
except Exception as e:
logger.error(f"Failed to log location for {userID}: {e}")
return False
def mapHandler(userID, deviceID, channel_number, message, snr, rssi, hop):
from modules.system import get_node_location
command = message[len("map"):].strip()
location = get_node_location(userID, deviceID)
lat = location[0]
lon = location[1]
"""
Handles 'map' commands from meshbot.
Usage:
map <description text> - Log current location with description
"""
command = str(command) # Ensure command is always a string
if command.strip().lower() == "?":
return (
"Usage:\n"
" 🗺map <description text> - Log your current location with a description\n"
"Example:\n"
" 🗺map Found a new mesh node near the park"
)
description = command.strip()
# if no description provided, set to default
if not description:
description = "Logged:"
# Sanitize description for CSV injection
if description and description[0] in ('=', '+', '-', '@'):
description = "'" + description
# if there is SNR and RSSI info, append to description
if snr is not None and rssi is not None:
description += f" SNR:{snr}dB RSSI:{rssi}dBm"
# if there is hop info, append to description
if hop is not None:
description += f" Meta:{hop}"
# location should be a tuple: (lat, lon)
if not location or len(location) != 2:
return "🚫Location data is missing or invalid."
success = log_locationData_toMap(userID, location, description)
if success:
return f"📍Location logged "
else:
return "🚫Failed to log location. Please try again."

View File

@@ -4,6 +4,7 @@ import urllib.request
import xml.etree.ElementTree as ET
import html
from html.parser import HTMLParser
import bs4 as bs
class MLStripper(HTMLParser):
def __init__(self):
@@ -16,9 +17,12 @@ class MLStripper(HTMLParser):
return ''.join(self.fed)
def strip_tags(html_text):
s = MLStripper()
s.feed(html_text)
return s.get_data()
# use BeautifulSoup to strip HTML tags
if not html_text:
return ""
soup = bs.BeautifulSoup(html_text, "html.parser")
text = soup.get_text(separator=" ", strip=True)
return ' '.join(text.split())
RSS_FEED_URLS = rssFeedURL
RSS_FEED_NAMES = rssFeedNames

View File

@@ -55,6 +55,10 @@ async def setup_scheduler(
# 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 'link' in schedulerValue.lower():
# Schedule to send a link message every specified interval
schedule.every(int(schedulerInterval)).hours.do(lambda: send_message(handle_satpass(schedulerInterface, 'link'), schedulerChannel, 0, schedulerInterface))
logger.debug(f"System: Starting the link scheduler to send link messages every {schedulerInterval} hours 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))

View File

@@ -441,6 +441,6 @@ try:
noisyTelemetryLimit = config['messagingSettings'].getint('noisyTelemetryLimit', 5) # default 5 packets
except Exception as e:
print(f"System: Error reading config file: {e}")
print(f"System: Check the config.ini against config.template file for missing sections or values.")
print(f"System: Exiting...")
print("System: Check the config.ini against config.template file for missing sections or values.")
print("System: Exiting...")
exit(1)

View File

@@ -98,7 +98,13 @@ class SurveyModule:
return
filename = os.path.join(self.response_dir, f'{survey_name}_responses.csv')
try:
# Check if file exists and if it has a header
write_header = not os.path.isfile(filename) or os.path.getsize(filename) == 0
with open(filename, 'a', encoding='utf-8') as f:
# Write header if needed
if write_header:
header = ['timestamp', 'user_id', 'location'] + [f'Q{i+1}' for i in range(len(self.responses[user_id]['answers']))]
f.write(','.join(header) + '\n')
# Always write: timestamp, userID, position, answers...
timestamp = datetime.now().strftime('%d%m%Y%H%M%S')
user_id_str = str(user_id)

View File

@@ -6,9 +6,8 @@ import wikipedia # pip install wikipedia
# Kiwix support for local wiki
if use_kiwix_server:
import requests
from bs4 import BeautifulSoup
import bs4 as bs
from urllib.parse import quote
from bs4.element import Comment
# Kiwix helper functions (only loaded if use_kiwix_server is True)
if wikipedia_enabled and use_kiwix_server:
@@ -16,13 +15,13 @@ if wikipedia_enabled and use_kiwix_server:
"""Filter visible text from HTML elements for Kiwix"""
if element.parent.name in ['style', 'script', 'head', 'title', 'meta', '[document]']:
return False
if isinstance(element, Comment):
if isinstance(element, bs.element.Comment):
return False
return True
def text_from_html(body):
"""Extract visible text from HTML content"""
soup = BeautifulSoup(body, 'html.parser')
soup = bs.BeautifulSoup(body, 'html.parser')
texts = soup.find_all(string=True)
visible_texts = filter(tag_visible, texts)
return " ".join(t.strip() for t in visible_texts if t.strip())

View File

@@ -51,9 +51,6 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
logger.debug(f"System: Bot detected Commands:{cmds}")
# run the first command after sorting
bot_response = command_handler[cmds[0]['cmd']]()
# wait a responseDelay to avoid message collision from lora-ack
time.sleep(responseDelay)
return bot_response
@@ -86,21 +83,21 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
type = "✋ACK"
elif "cqcq" in message.lower() or "cq" in message.lower() or "cqcqcq" in message.lower():
if deviceID == 1:
myname = get_name_from_number(myNodeNum1, 'short', 1)
myname = get_name_from_number(deviceID, 'short', 1)
elif deviceID == 2:
myname = get_name_from_number(myNodeNum2, 'short', 2)
myname = get_name_from_number(deviceID, 'short', 2)
msg = f"QSP QSL OM DE {myname} K\n"
else:
msg = "🔊 Can you hear me now?"
# append SNR/RSSI or hop info
if hop.startswith("Gateway") or hop.startswith("MQTT"):
msg += f" [GW]"
msg += " [GW]"
elif hop.startswith("Direct"):
msg += f" [RF]"
msg += " [RF]"
else:
#flood
msg += f" [F]"
msg += " [F]"
if (float(snr) != 0 or float(rssi) != 0) and "Hops" not in hop:
msg += f"\nSNR:{snr} RSSI:{rssi}"
@@ -317,8 +314,8 @@ def onReceive(packet, interface):
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:
logger.warning(f"System: Packet from self {message_from_id} loop or traffic replay deteted")
if message_from_id in [myNodeNum1, myNodeNum2, myNodeNum3, myNodeNum4, myNodeNum5, myNodeNum6, myNodeNum7, myNodeNum8, myNodeNum9]:
logger.warning(f"System: Packet from self {message_from_id} loop or traffic replay detected")
# get the signal strength and snr if available
if packet.get('rxSnr') or packet.get('rxRssi'):
@@ -389,7 +386,7 @@ def onReceive(packet, interface):
return
# If the packet is a DM (Direct Message) respond to it, otherwise validate its a message for us on the channel
if packet['to'] == myNodeNum1 or packet['to'] == myNodeNum2:
if packet['to'] in [myNodeNum1, myNodeNum2, myNodeNum3, myNodeNum4, myNodeNum5, myNodeNum6, myNodeNum7, myNodeNum8, myNodeNum9]:
# message is DM to us
isDM = True
# check if the message contains a trap word, DMs are always responded to
@@ -470,7 +467,7 @@ def onReceive(packet, interface):
logger.debug(f"System: Error Packet = {packet}")
async def start_rx():
print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
print (CustomFormatter.bold_white + "\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
# Start the receive subscriber using pubsub via meshtastic library
pub.subscribe(onReceive, 'meshtastic.receive')
pub.subscribe(onDisconnect, 'meshtastic.connection.lost')

View File

@@ -1,52 +1,51 @@
# OLD Docker Compose configuration for Meshing Around application with optional services.
# This setup includes the main Meshing Around service, with optional Ollama and Prometheus Node Exporter services.
# Adjust device mappings, ports, and configurations as needed for your environment.
configs:
me_config:
file: ./config.ini # Path to the configuration file for Meshing Around.
# Windows users may need to adjust the path format, e.g., C:/path/to/config.ini
services:
meshing-around:
build:
context: ../..
depends_on:
ollama:
condition: service_healthy
devices:
- /dev/ttyAMA10 # Replace this with your actual device!
configs:
- source: me_config
target: /app/config.ini
devices: # Optional if using meshtasticd. Pass through radio device.
- /dev/ttyAMA10 # Replace this with your actual device!
#- /dev/ttyUSB0 # Example for USB device
user: 1000:1000 # run as non-root user for better security.
extra_hosts:
- "host.docker.internal:host-gateway" # Used to access a local linux meshtasticd device via tcp
ollama:
image: ollama/ollama:0.5.1
volumes:
- ./ollama:/root/.ollama
- ./ollama-entrypoint.sh:./entrypoint.sh
container_name: ollama
pull_policy: always
tty: true
restart: always
entrypoint:
- /usr/bin/bash
- /script/docker/entrypoint.sh
expose:
- 11434
healthcheck:
test: "apt update && apt install curl -y && curl -f http://localhost:11434/api/tags | grep -q llama3.2:3b"
interval: 30s
timeout: 10s
retries: 20
node-exporter:
image: quay.io/prometheus/node-exporter:latest
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- --path.procfs=/host/proc
- --path.rootfs=/rootfs
- --path.sysfs=/host/sys
- --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)
# - "host.docker.internal:host-gateway" # Enables access to host services from within the container.
container_name: meshing-around
restart: unless-stopped
expose:
- 9100
network_mode: host
pid: host
configs:
me_config:
file: ./config.ini
#tty: true # Enable only if interactive terminal is needed.
ports:
#- "8420:8420" # web report interface
#- "443:443" # HTTPS interface meshtasticD
environment:
- OLLAMA_API_URL=http://ollama:11434
# Uncomment the following service if you want to enable Ollama for local LLM API access.
# ollama:
# image: ollama/ollama:0.5.1
# volumes:
# - ./ollama:/root/.ollama
# - ./ollama-entrypoint.sh:./entrypoint.sh
# container_name: ollama
# pull_policy: always
# tty: true
# restart: always
# entrypoint:
# - /usr/bin/bash
# - /script/docker/entrypoint.sh
# ports:
# - "11434:11434"
# healthcheck:
# test: "curl -f http://localhost:11434/api/tags | grep -q llama3.2:3b"
# interval: 30s
# timeout: 10s
# retries: 20

View File

@@ -8,8 +8,8 @@ pid=$!
# Pause for Ollama to start.
sleep 5
echo "🔴 Retrieve llama3.2:3b model..."
ollama pull llama3.2:3b
echo "🔴 Retrieve gemma3:270m model..."
ollama pull gemma3:270m
echo "🟢 Done!"
# Wait for Ollama process to finish.

View File

@@ -46,7 +46,7 @@ fi
# copy modules/custom_scheduler.py template if it does not exist
if [[ ! -f modules/custom_scheduler.py ]]; then
cp etc/custom_scheduler.py modules/custom_scheduler.py
cp -n etc/custom_scheduler.py modules/
printf "\nCustom scheduler template copied to modules/custom_scheduler.py\n"
fi
@@ -56,7 +56,7 @@ echo "Backing up data/ directory..."
backup_file="data_backup.tar.gz"
path2backup="data/"
#copy custom_scheduler.py if it exists
if [ -f "modules/custom_scheduler.py" ]; then
if [[ -f "modules/custom_scheduler.py" ]]; then
echo "Including custom_scheduler.py in backup..."
cp modules/custom_scheduler.py data/
fi
@@ -71,8 +71,7 @@ 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 [[ -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
@@ -83,7 +82,7 @@ else
fi
# if service was stopped earlier, restart it
if [ "$service_stopped" = true ]; then
if [[ "$service_stopped" = true ]]; then
echo "Restarting services..."
systemctl start mesh_bot.service
systemctl start pong_bot.service