Commits vergleichen

...
39 Commits
Autor SHA1 Nachricht Datum
tom 1b6bb7ef32 .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Successful in 6s
Build of FoxMesh / build-and-test (push) Successful in 2m28s
2026-10-22 23:53:34 +02:00
tom ea9639bca0 .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Canceled after 4s
Build of FoxMesh / build-and-test (push) Canceled after 11s
2026-10-22 23:42:39 +02:00
tom 807621b92c .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Failing after 2s
Build of FoxMesh / build-and-test (push) Canceled after 2m14s
2026-10-22 23:40:56 +02:00
tom 7c19f0663b .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Failing after 3s
Build of FoxMesh / build-and-test (push) Canceled after 11s
2026-10-22 23:38:30 +02:00
tom 5f035381f1 .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Failing after 3s
Build of FoxMesh / build-and-test (push) Successful in 2m25s
2026-10-22 23:21:08 +02:00
tom ca09806376 .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Failing after 3s
Build of FoxMesh / build-and-test (push) Canceled after 11s
2026-10-22 23:07:58 +02:00
tom 326b602a99 .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Failing after 3s
Build of FoxMesh / build-and-test (push) Successful in 2m25s
2026-10-22 23:01:42 +02:00
tom a7e356ce04 .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Failing after 3s
Build of FoxMesh / build-and-test (push) Successful in 2m26s
2026-10-22 22:46:01 +02:00
tom 2f67c1df97 typo: foxmesh-protocol
Build and Release Typst PDF / build-pdf (push) Failing after 3s
Build of FoxMesh / build-and-test (push) Successful in 2m25s
2026-10-22 22:35:58 +02:00
tom 6b4f711c40 .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Failing after 3s
Build of FoxMesh / build-and-test (push) Successful in 2m25s
2026-10-22 22:35:02 +02:00
tom 3943d3de14 fix: retension-days 10 -- for artifacts in foxmesh-ci.yml
Build and Release Typst PDF / build-pdf (push) Failing after 4s
Build of FoxMesh / build-and-test (push) Successful in 2m27s
2026-10-22 21:53:19 +02:00
tom bdcb07c374 .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Failing after 2s
Build of FoxMesh / build-and-test (push) Successful in 2m31s
2026-10-22 02:35:10 +02:00
tom b9001572cd .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Failing after 2s
Build of FoxMesh / build-and-test (push) Failing after 11s
2026-10-22 02:28:08 +02:00
tom 21c3dc8771 remove generated pdf files
Build and Release Typst PDF / build-pdf (push) Failing after 1s
Build of FoxMesh / build-and-test (push) Failing after 2m29s
2026-10-22 02:13:06 +02:00
tom 652b794aae Merge pull request 'doc: foxmash protocol in typst, display chars' (#6) from docs/lora-protocol into main
Build and Release Typst PDF / build-pdf (push) Failing after 1s
Build of FoxMesh / build-and-test (push) Successful in 2m35s
Reviewed-on: #6
2026-10-22 01:51:11 +02:00
tom acd7d316bc doc: foxmash protocol in typst, display chars
Build of FoxMesh / build-and-test (pull_request) Successful in 2m41s
2026-10-22 00:27:17 +02:00
tom 0a1c781f0d foxmesh-ci.yml actions/checkout with all tags download
Build of FoxMesh / build-and-test (push) Successful in 2m55s
Only with these https://gitea.com/actions/checkout usage, we can checkout all tags, for versioning.
2026-10-21 01:23:22 +02:00
tom b54a9fc714 Merge pull request 'Refactor/lora cli' (#5) from refactor/lora-cli into main
Build of FoxMesh / build-and-test (push) Successful in 2m45s
Reviewed-on: #5
2026-10-21 00:29:14 +02:00
tom 1a56c456f6 version 0.1
Build of FoxMesh / build-and-test (pull_request) Successful in 2m38s
2026-10-21 00:23:54 +02:00
tom fb0e096e4e refactor Build Date, GIT_TAG, and GIT_HASH support
Build of FoxMesh / build-and-test (pull_request) Successful in 2m44s
2026-10-21 00:13:59 +02:00
tom 618ff87d5c lora: race condition removed, preparation for rx, getSnapshot, beautify led and i2c files 2026-10-20 22:13:38 +02:00
tom 256fb9cdac display: utf8 table for 0xc3 and 0xc2, umlauts, etc. 2026-10-20 22:11:59 +02:00
tom 4823f519b2 cli: add LoRa snapshot status, test-display command 2026-10-20 22:10:29 +02:00
tom 1d22bcb4ce Merge pull request 'display: looking ahead function for UTF-8 with efficiency case' (#4) from refactor/display-utf8-decoder into main
Build of FoxMesh / build-and-test (push) Successful in 2m40s
Reviewed-on: #4
2026-10-20 12:56:17 +02:00
tom 1616bb25f0 display: looking ahead function for UTF-8 with efficiency case
Build of FoxMesh / build-and-test (pull_request) Successful in 2m43s
2026-10-20 12:54:18 +02:00
tom aab6f8de43 sdkconfig.ttgo-lora32-v21 gelöscht
Build of FoxMesh / build-and-test (push) Successful in 2m41s
2026-10-20 12:37:58 +02:00
tom f2e4df9cc5 Merge branch 'feature/display-lines' into main
Build of FoxMesh / build-and-test (push) Successful in 2m47s
2026-10-20 12:29:40 +02:00
tom 6b43bd8dbb Merge pull request 'Feature/add sx1276 support' (#2) from feature/add-sx1276-support into main
Build of FoxMesh / build-and-test (push) Successful in 2m41s
Reviewed-on: #2
2026-10-20 04:34:43 +02:00
tom f8088d3793 Merge branch 'main' into feature/add-sx1276-support
Build of FoxMesh / build-and-test (pull_request) Successful in 2m51s
2026-10-20 04:34:17 +02:00
tom 5c90b419bf Add Bob WB4APR's SSID document
foxmesh-ci.yml (push) Invalid workflow file
2026-10-20 04:23:07 +02:00
tom 13b9df523d display: move FoxMesh and version to line # 0 2026-10-20 04:18:02 +02:00
tom 398cf135a4 docs: update foxmash protocol 2026-10-20 04:04:16 +02:00
tom ae747dc0cc upd documentation foxmash-protocol and APRS ref. 2026-10-20 02:16:38 +02:00
tom e175568531 cli: replace fraction of frequency to just frequency 2026-10-19 20:29:54 +02:00
tom c0a94ce627 configuration: add lora specific default values 2026-10-19 17:25:48 +02:00
tom 9124f55b32 Merge pull request 'Feature/gitea support' (#1) from feature/gitea-support into main
Build of FoxMesh / build-and-test (push) Successful in 2m51s
Reviewed-on: #1
2026-10-19 15:21:09 +02:00
tom 95e74a960a yaml: trigger the workflow on pull-request events targeting the main branch
Build of FoxMesh / build-and-test (push) Skipped
Build of FoxMesh / build-and-test (pull_request) Successful in 2m54s
2026-10-19 15:16:39 +02:00
tom 8b5145bc30 platform: add no warnings for #include-next
Build of FoxMesh / build-and-test (push) Skipped
2026-10-19 15:03:09 +02:00
tom f16a243e3f yaml: fix the indentification, adapt to host only 2026-10-19 15:02:16 +02:00
24 geänderte Dateien mit 2128 neuen und 2345 gelöschten Zeilen

Keine Dateien entsprechen Ihrer Suche

+62
Datei anzeigen
@@ -0,0 +1,62 @@
name: Build and Release Typst PDF
on:
push:
branches:
- main
tags:
- 'v*' # Triggers when a release tag like v1.0.0 is pushed
workflow_dispatch: # Enables manual start in the Web interface
jobs:
build-pdf:
runs-on: host
steps:
# 1. Checkout the source code from Git
- name: Checkout Repository
uses: actions/checkout@v4
# 2. Download and extract the latest Typst Binary (No installation required)
- name: Download Latest Typst
run: |
cd docs/lora/
XZ_FILE_URL=$(curl -s https://api.github.com/repos/typst/typst/releases/latest | jq -r '.assets[] | select(.name | endswith("x86_64-unknown-linux-musl.tar.xz")) | .browser_download_url')
echo "$XZ_FILE_URL"
curl -L "$XZ_FILE_URL" | tar -xJ --strip-components=1
chmod +x ./typst
# 3. Compile the PDF using the local binary
- name: Compile Typst Documents
run: |
cd docs/lora/
ls -la
./typst compile foxmesh-protocol.typ FoxMesh_Protocol.pdf
./typst compile foxmesh-protocol_de.typ FoxMesh_Protocol_de.pdf
# 3. Upload PDF as a Gitea artifact (runs on every push to main)
- name: Upload PDF Artifact
uses: actions/upload-artifact@v7
with:
name: foxmesh-protocol-pdf
path: docs/lora/FoxMesh_Protocol.pdf
retention-days: 14
archive: false
- name: Upload PDF Artifact de
uses: actions/upload-artifact@v7
with:
name: foxmesh-protocol-de-pdf
path: docs/lora/FoxMesh_Protocol_de.pdf
retention-days: 14
archive: false
# 4. Optional: PDF direkt an ein Gitea-Release anhängen (wenn ein Tag gepusht wird)
# - name: Upload to Gitea Release
# if: startsWith(github.ref, 'refs/tags/')
# uses: svenstaro/upload-release-action@v2
# with:
# repo_token: ${{ secrets.GITHUB_TOKEN }}
# file: FoxMesh_Engineering_Manual.pdf
# asset_name: FoxMesh_Engineering_Manual.pdf
# tag: ${{ github.ref }}
# overwrite: true
+197 -184
Datei anzeigen
@@ -1,9 +1,12 @@
name: Automated CI Build of FoxMesh
name: Build of FoxMesh
on:
push:
branches:
- main
branches: [ main ]
pull_request:
branches: [ main ] # Trigger the workflow on pull request events targeting the main branch
tags:
- 'v*' # Triggers when a release tag like v1.0.0 is pushed
workflow_dispatch:
inputs:
target_branch:
@@ -19,113 +22,123 @@ jobs:
contents: write # We want also create official Releases
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
# 1. Checkout the target branch
- name: Checkout Target branch
uses: actions/checkout@v7
with:
ref: ${{ github.event.inputs.target_branch || github.ref }}
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
# 2. Extract Git and Date Metadata for the dynamic artifact filename
- name: Generate Dynamic Artifact Metadata
id: meta
run: |
# Get current date in YYYYMMDD format
echo "CURRENT_DATE=$(date +'%Y%m%d')" >> $GITHUB_ENV
# 1. Checkout the target branch, with fetch-depth: 0 and fetch-tags: true -> loading all tags
- name: Checkout Target branch
uses: actions/checkout@v7
with:
ref: ${{ github.event.inputs.target_branch || github.ref }}
fetch-depth: 0
fetch-tags: true
# Get short 7-character git commit hash
echo "GIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
# 2. Extract Git and Date Metadata for the dynamic artifact filename
- name: Generate Dynamic Artifact Metadata
id: meta
run: |
# Get current date in YYYYMMDD format
echo "CURRENT_DATE=$(date +'%Y%m%d')" >> $GITHUB_ENV
# Extract and sanitize branch name (replace slashes and special characters with dashes)
RAW_BRANCH=${{ github.event.inputs.target_branch || github.ref_name }}
CLEAN_BRANCH=$(echo "$RAW_BRANCH" | sed 's/[^a-zA-Z0-9.\-]/_§/g' | sed 's|§|-|g' | sed 's|_||g')
echo "GIT_BRANCH=$CLEAN_BRANCH" >> $GITHUB_ENV
# Get short 7-character git commit hash
echo "GIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
# 3. Set up Python environment
- name: Set up Python 3.x
uses: actions/setup-python@v7
with:
python-version: '3.x'
# Extract and sanitize branch name (replace slashes and special characters with dashes)
RAW_BRANCH=${{ github.event.inputs.target_branch || github.ref_name }}
CLEAN_BRANCH=$(echo "$RAW_BRANCH" | sed 's/[^a-zA-Z0-9.\-]/_§/g' | sed 's|§|-|g' | sed 's|_||g')
echo "GIT_BRANCH=$CLEAN_BRANCH" >> $GITHUB_ENV
# 4. Cache global PlatformIO directories, uses a dynamic key based on platformio.ini
- name: Cache PlatformIO
uses: actions/cache@v6
with:
path: |
~/.cache/pio
~/.platformio
.pio
key: ${{ runner.os }}-pio-${{ hashFiles('**/platformio.ini') }}
restore-keys: |
${{ runner.os }}-pio-
#3 Python alread installed
- name: Show Python Version
run: python --version
# 5. Install PlatformIO Core and the required intelhex extension in a single, quiet command
- name: Install PlatformIO Core
run: python -m pip install --quiet --upgrade pip platformio intelhex
# 4. Cache global PlatformIO directories, uses a dynamic key based on platformio.ini
- name: Cache PlatformIO
uses: actions/cache@v6
with:
path: |
.platformio
.pio
key: ${{ runner.os }}-pio-${{ hashFiles('**/platformio.ini') }}
restore-keys: |
${{ runner.os }}-pio-
# 6. Install ESP32 Platform SDK
- name: Install ESP32 platform
run: pio pkg install --silent --global --platform espressif32
- name: PlatformIO in venv installieren
run: |
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install --quiet --upgrade pip platformio intelhex
# Ab hier steht platformio in diesem Schritt zur Verfügung
pio --version
# Fügt den Pfad absolut dem GitHub/Gitea-Pfad hinzu:
echo "$PWD/.venv/bin" >> $GITHUB_PATH
# 7. Compile the Project
- name: Compile the Project
run: pio run
# 5. Install PlatformIO Core and the required intelhex extension in a single, quiet command
# - name: Install PlatformIO Core
# run: python -m pip install --quiet --upgrade pip platformio intelhex
# 8. Dynamically create the smart flashing scripts next to the T-Beam binary
# with optional bootloader detection and group check
# the table of flash addresses usually found at /partitions-4MB-safeboot.csv
- name: Create Desktop Flashing Scripts
run: |
# ----------------------------------------------------------------------
# 0. Flash Address Configuration ← edit here to change the layout
# ----------------------------------------------------------------------
FLASH_BOOTLOADER_ADDR="0x1000"
FLASH_PARTITIONS_ADDR="0x8000"
FLASH_OTADATA_ADDR="0xE000"
FLASH_SAFEBOOT_ADDR="0x10000"
FLASH_FIRMWARE_ADDR="0x10000"
# 6. Install ESP32 Platform SDK
# - name: Install ESP32 platform
# run: pio pkg install --silent --global --platform espressif32
# ----------------------------------------------------------------------
# 1. Universal Linux Shell Script
# ----------------------------------------------------------------------
cat << 'EOF' > flash_esp32_board.sh
#!/usr/bin/env bash
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
# 7. Compile the Project
- name: Compile the Project
run: pio run
echo -e "${YELLOW}=== FoxMesh Universal ESP32 Flashing Script ===${NC}"
# 8. Dynamically create the smart flashing scripts next to the T-Beam binary
# with optional bootloader detection and group check
# the table of flash addresses usually found at /partitions-4MB-safeboot.csv
- name: Create Desktop Flashing Scripts
run: |
# ----------------------------------------------------------------------
# 0. Flash Address Configuration ← edit here to change the layout
# ----------------------------------------------------------------------
FLASH_BOOTLOADER_ADDR="0x1000"
FLASH_PARTITIONS_ADDR="0x8000"
FLASH_OTADATA_ADDR="0xE000"
FLASH_SAFEBOOT_ADDR="0x10000"
FLASH_FIRMWARE_ADDR="0x10000"
if ! command -v esptool &> /dev/null; then
# ----------------------------------------------------------------------
# 1. Universal Linux Shell Script
# ----------------------------------------------------------------------
cat << 'EOF' > flash_esp32_board.sh
#!/usr/bin/env bash
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
echo -e "${YELLOW}=== FoxMesh Universal ESP32 Flashing Script ===${NC}"
if ! command -v esptool &> /dev/null; then
echo -e "${RED}Error: esptool is not installed.${NC}"
echo "Please install it via your package manager or pip: 'pip install esptool'"
echo "Option Linux Arch: 'sudo pacman -Sy && sudo pacman -S extra-testing/esptool'"
exit 1
fi
fi
FIRMWARE="firmware.bin"
FIRMWARE="firmware.bin"
# Firmware File
if [ ! -f "$FIRMWARE" ]; then
# Firmware File
if [ ! -f "$FIRMWARE" ]; then
echo -e "${RED}Error: No matching firmware binary found in this folder!${NC}"
exit 1
fi
fi
echo -e "${GREEN}Selected Firmware:${NC} $FIRMWARE ($BOARD_NAME)"
PORT=$(ls /dev/ttyUSB* /dev/ttyACM* 2>/dev/null | head -n 1)
if [ -z "$PORT" ]; then
echo -e "${GREEN}Selected Firmware:${NC} $FIRMWARE ($BOARD_NAME)"
PORT=$(ls /dev/ttyUSB* /dev/ttyACM* 2>/dev/null | head -n 1)
if [ -z "$PORT" ]; then
echo -e "${RED}Error: No connected ESP32 Board (ttyUSB/ttyACM) found!${NC}"
exit 1
fi
echo -e "${GREEN}Detected Board on port:${NC} $PORT"
fi
echo -e "${GREEN}Detected Board on port:${NC} $PORT"
# Dynamic Permission Check: Determine required group for the specific TTY device
REQUIRED_GROUP=$(stat -c '%G' "$PORT")
if [ "$REQUIRED_GROUP" != "root" ]; then
# Dynamic Permission Check: Determine required group for the specific TTY device
REQUIRED_GROUP=$(stat -c '%G' "$PORT")
if [ "$REQUIRED_GROUP" != "root" ]; then
if ! groups "$USER" | grep -q "\b${REQUIRED_GROUP}\b"; then
echo -e "${RED}Error: You do not have permissions to access $PORT.${NC}"
echo -e "The device belongs to the group: ${YELLOW}$REQUIRED_GROUP${NC}"
@@ -134,107 +147,107 @@ jobs:
echo -e "After running the command, please log out and log back in for the changes to take effect."
exit 1
fi
fi
fi
# Build flash arguments from the address table (only include files that exist)
FLASH_ARGS=""
if [ -f "bootloader.bin" ]; then
FLASH_ARGS="$FLASH_ARGS __BOOTLOADER_ADDR__ bootloader.bin"
fi
if [ -f "partitions.bin" ]; then
FLASH_ARGS="$FLASH_ARGS __PARTITIONS_ADDR__ partitions.bin"
fi
if [ -f "otadata.bin" ]; then
FLASH_ARGS="$FLASH_ARGS __OTADATA_ADDR__ otadata.bin"
fi
if [ -f "safeboot.bin" ]; then
FLASH_ARGS="$FLASH_ARGS __SAFEBOOT_ADDR__ safeboot.bin"
fi
FLASH_ARGS="$FLASH_ARGS __FIRMWARE_ADDR__ $FIRMWARE"
# Build flash arguments from the address table (only include files that exist)
FLASH_ARGS=""
if [ -f "bootloader.bin" ]; then
FLASH_ARGS="$FLASH_ARGS __BOOTLOADER_ADDR__ bootloader.bin"
fi
if [ -f "partitions.bin" ]; then
FLASH_ARGS="$FLASH_ARGS __PARTITIONS_ADDR__ partitions.bin"
fi
if [ -f "otadata.bin" ]; then
FLASH_ARGS="$FLASH_ARGS __OTADATA_ADDR__ otadata.bin"
fi
if [ -f "safeboot.bin" ]; then
FLASH_ARGS="$FLASH_ARGS __SAFEBOOT_ADDR__ safeboot.bin"
fi
FLASH_ARGS="$FLASH_ARGS __FIRMWARE_ADDR__ $FIRMWARE"
echo -e "${GREEN}Flashing:${NC}$FLASH_ARGS"
esptool --chip esp32 --port "$PORT" --baud 921600 write-flash -z $FLASH_ARGS
echo -e "${GREEN}Flashing:${NC}$FLASH_ARGS"
esptool --chip esp32 --port "$PORT" --baud 921600 write-flash -z $FLASH_ARGS
if [ $? -eq 0 ]; then
if [ $? -eq 0 ]; then
echo -e "${GREEN}=== Success! $BOARD_NAME successfully updated ===${NC}"
else
else
echo -e "${RED}=== Flash failed! ===${NC}"
exit 1
fi
EOF
fi
EOF
# Substitute address placeholders with configured hex values
sed -i \
# Substitute address placeholders with configured hex values
sed -i \
-e "s|__BOOTLOADER_ADDR__|${FLASH_BOOTLOADER_ADDR}|g" \
-e "s|__PARTITIONS_ADDR__|${FLASH_PARTITIONS_ADDR}|g" \
-e "s|__OTADATA_ADDR__|${FLASH_OTADATA_ADDR}|g" \
-e "s|__SAFEBOOT_ADDR__|${FLASH_SAFEBOOT_ADDR}|g" \
-e "s|__FIRMWARE_ADDR__|${FLASH_FIRMWARE_ADDR}|g" \
flash_esp32_board.sh
chmod +x flash_esp32_board.sh
chmod +x flash_esp32_board.sh
# ----------------------------------------------------
# 2. Universal Windows Batch Script
# ----------------------------------------------------
cat << 'EOF' > flash_esp32_board.bat
@echo off
setlocal enabledelayedexpansion
echo ===================================================
echo === FoxMesh Universal ESP32 Windows Flashing ======
echo ===================================================
# ----------------------------------------------------
# 2. Universal Windows Batch Script
# ----------------------------------------------------
cat << 'EOF' > flash_esp32_board.bat
@echo off
setlocal enabledelayedexpansion
echo ===================================================
echo === FoxMesh Universal ESP32 Windows Flashing ======
echo ===================================================
set "FIRMWARE=firmware.bin"
set "FIRMWARE=firmware.bin"
if not exist "!FIRMWARE!" (
if not exist "!FIRMWARE!" (
echo [ERROR] No matching firmware binary found in this folder!
pause
exit /b 1
)
)
echo [INFO] Selected Firmware: %FIRMWARE% (%BOARD_NAME%)
echo [INFO] Selected Firmware: %FIRMWARE% (%BOARD_NAME%)
set "COMPORT="
for /f "tokens=2 delims=()" %%A in ('wmic path Win32_PnPEntity get Name ^| findstr /i "COM" ^| findstr /i "Silicon CP210 CH340 CH9102 USB-to-UART"') do set "COMPORT=%%A"
if "%COMPORT%"=="" (
for /f "tokens=2 delims=()" %%A in ('wmic path Win32_PnPEntity get Name ^| findstr /i "(COM"') do set "COMPORT=%%A"
)
if "%COMPORT%"=="" (
echo [ERROR] No ESP32 Board or USB-UART bridge detected!
set "COMPORT="
for /f "tokens=2 delims=()" %%A in ('wmic path Win32_PnPEntity get Name ^| findstr /i "COM" ^| findstr /i "Silicon CP210 CH340 CH9102 USB-to-UART"') do set "COMPORT=%%A"
if "%COMPORT%"=="" (
for /f "tokens=2 delims=()" %%A in ('wmic path Win32_PnPEntity get Name ^| findstr /i "(COM"') do set "COMPORT=%%A"
)
if "%COMPORT%"=="" (
echo [ERROR] No ESP32 Board or USB-UART bridge detected!
pause
exit /b 1
)
echo [INFO] Detected Board on port: %COMPORT%
set "FLASH_ARGS="
if exist "bootloader.bin" (
set "FLASH_ARGS=!FLASH_ARGS! __BOOTLOADER_ADDR__ bootloader.bin"
)
if exist "partitions.bin" (
set "FLASH_ARGS=!FLASH_ARGS! __PARTITIONS_ADDR__ partitions.bin"
)
if exist "otadata.bin" (
set "FLASH_ARGS=!FLASH_ARGS! __OTADATA_ADDR__ otadata.bin"
)
if exist "safeboot.bin" (
set "FLASH_ARGS=!FLASH_ARGS! __SAFEBOOT_ADDR__ safeboot.bin"
)
set "FLASH_ARGS=!FLASH_ARGS! __FIRMWARE_ADDR__ %FIRMWARE%"
echo [INFO] Flashing: !FLASH_ARGS!
esptool --chip esp32 --port %COMPORT% --baud 921600 write-flash -z !FLASH_ARGS!
if %ERRORLEVEL% EQU 0 (
echo === SUCCESS! Flash operation successfully completed ===
) else (
echo [ERROR] Flash failed!
pause
exit /b 1
)
pause
exit /b 1
)
echo [INFO] Detected Board on port: %COMPORT%
EOF
set "FLASH_ARGS="
if exist "bootloader.bin" (
set "FLASH_ARGS=!FLASH_ARGS! __BOOTLOADER_ADDR__ bootloader.bin"
)
if exist "partitions.bin" (
set "FLASH_ARGS=!FLASH_ARGS! __PARTITIONS_ADDR__ partitions.bin"
)
if exist "otadata.bin" (
set "FLASH_ARGS=!FLASH_ARGS! __OTADATA_ADDR__ otadata.bin"
)
if exist "safeboot.bin" (
set "FLASH_ARGS=!FLASH_ARGS! __SAFEBOOT_ADDR__ safeboot.bin"
)
set "FLASH_ARGS=!FLASH_ARGS! __FIRMWARE_ADDR__ %FIRMWARE%"
echo [INFO] Flashing: !FLASH_ARGS!
esptool --chip esp32 --port %COMPORT% --baud 921600 write-flash -z !FLASH_ARGS!
if %ERRORLEVEL% EQU 0 (
echo === SUCCESS! Flash operation successfully completed ===
) else (
echo [ERROR] Flash failed!
pause
exit /b 1
)
pause
EOF
# Substitute address placeholders with configured hex values
sed -i \
# Substitute address placeholders with configured hex values
sed -i \
-e "s|__BOOTLOADER_ADDR__|${FLASH_BOOTLOADER_ADDR}|g" \
-e "s|__PARTITIONS_ADDR__|${FLASH_PARTITIONS_ADDR}|g" \
-e "s|__OTADATA_ADDR__|${FLASH_OTADATA_ADDR}|g" \
@@ -242,27 +255,27 @@ jobs:
-e "s|__FIRMWARE_ADDR__|${FLASH_FIRMWARE_ADDR}|g" \
flash_esp32_board.bat
# -----------------------------------------------------------------------------
# 3. Gather binaries and scripts into target directory for structural packaging
# -----------------------------------------------------------------------------
mkdir -p deploy_package
BUILD_OUTPUT=".pio/build/ttgo-lora32-v21"
# -----------------------------------------------------------------------------
# 3. Gather binaries and scripts into target directory for structural packaging
# -----------------------------------------------------------------------------
mkdir -p deploy_package
BUILD_OUTPUT=".pio/build/ttgo-lora32-v21"
# Copy binaries only if they exist
[ -f "$BUILD_OUTPUT/firmware.bin" ] && cp "$BUILD_OUTPUT/firmware.bin" ./deploy_package/
[ -f "$BUILD_OUTPUT/bootloader.bin" ] && cp "$BUILD_OUTPUT/bootloader.bin" ./deploy_package/
[ -f "$BUILD_OUTPUT/partitions.bin" ] && cp "$BUILD_OUTPUT/partitions.bin" ./deploy_package/
[ -f "$BUILD_OUTPUT/otadata.bin" ] && cp "$BUILD_OUTPUT/otadata.bin" ./deploy_package/
[ -f "$BUILD_OUTPUT/safeboot.bin" ] && cp "$BUILD_OUTPUT/safeboot.bin" ./deploy_package/
# Copy binaries only if they exist
[ -f "$BUILD_OUTPUT/firmware.bin" ] && cp "$BUILD_OUTPUT/firmware.bin" ./deploy_package/
[ -f "$BUILD_OUTPUT/bootloader.bin" ] && cp "$BUILD_OUTPUT/bootloader.bin" ./deploy_package/
[ -f "$BUILD_OUTPUT/partitions.bin" ] && cp "$BUILD_OUTPUT/partitions.bin" ./deploy_package/
[ -f "$BUILD_OUTPUT/otadata.bin" ] && cp "$BUILD_OUTPUT/otadata.bin" ./deploy_package/
[ -f "$BUILD_OUTPUT/safeboot.bin" ] && cp "$BUILD_OUTPUT/safeboot.bin" ./deploy_package/
# Move flash scripts into the deployment directory
mv flash_esp32_board.sh ./deploy_package/
mv flash_esp32_board.bat ./deploy_package/
# Move flash scripts into the deployment directory
mv flash_esp32_board.sh ./deploy_package/
mv flash_esp32_board.bat ./deploy_package/
# 9. Dynamic Artifact Upload: FoxMesh-branch-date-hash.zip
- name: Upload Compiled Firmware Package
uses: actions/upload-artifact@v7
with:
name: FoxMesh-${{ env.GIT_BRANCH }}-${{ env.CURRENT_DATE }}-${{ env.GIT_HASH }}
path: deploy_package/
retention-days: 10 # Artifacts will be automatically deleted after 10 days
# 9. Dynamic Artifact Upload: FoxMesh-branch-date-hash.zip
- name: Upload Compiled Firmware Package
uses: actions/upload-artifact@v7
with:
name: FoxMesh-${{ env.GIT_BRANCH }}-${{ env.CURRENT_DATE }}-${{ env.GIT_HASH }}
path: deploy_package/
retention-days: 10 # Artifacts will be automatically deleted after 10 days
+3
Datei anzeigen
@@ -22,3 +22,6 @@ sdkconfig.*
# --- Log files ---
build_log.txt
logs/
# PDF files generated
*.pdf
!docs/lora/aprs*.pdf
+3 -2
Datei anzeigen
@@ -28,8 +28,9 @@ The firmware is designed to target popular LoRa development ecosystems, with pri
## 🌐 Project Mirrors / Hosting
To ensure high availability and decentralization, this project is maintained across multiple platforms:
* **Primary Repo (GitHub):** [github.com/tkuschel/FoxMesh](https://github.com/tkuschel/FoxMesh)
* **Self-Hosted Mirror (Gitea):** [git.kuschel.at/tom/FoxMesh](https://git.kuschel.at/tom/FoxMesh)
* **Primary Self-Hosted Repo (Gitea):** [git.kuschel.at/tom/FoxMesh](https://git.kuschel.at/tom/FoxMesh)
* **Mirror Repo (GitHub):** [github.com/tkuschel/FoxMesh](https://github.com/tkuschel/FoxMesh)
*Note: Both repositories are automatically kept synchronized. You can clone, open issues, or contribute on either platform.*
+28
Datei anzeigen
@@ -106,3 +106,31 @@ All public APIs, public structures, and method signatures within header files mu
*/
void updateProfile(const BlinkProfile& profile);
```
## 8. Git Branching and Commit Standards
To ensure traceability and maintain a clean repository history, FoxMesh strictly follows a standardized branching and commit convention based on *Conventional Commits*.
### 8.1 Branch Naming Convention
Developers must prefix every newly created Git branch with a specific scope identifier followed by a slash (`/`), the Gitea issue number, and a short description.
* **Format:** `<type>/<issue-number>-<short-description>`
* **Example:** `docs/123-readme-update` or `feat/45-lora-mesh-ack`
### 8.2 Scope Identifiers
The following prefixes are permitted for branch creation and commit messages:
* `feat/` (or `feature/`): Introduction of new hardware or software functionalities.
* `fix/` (or `bugfix/`): Resolution of bugs, runtime stability issues, or crashes.
* `docs/`: Exclusively reserved for pure documentation updates (Readmes, Doxygen, Wiki).
* `refactor/`: Code reorganization or optimization without changing external behavior.
* `chore/`: Updates to the build system, `platformio.ini`, CI/CD pipelines, or external dependencies.
### 8.3 Commit and Issue Automation
Every commit message should start with the lowercase prefix (e.g., `docs: description`) and must include the targeted Gitea issue number in parentheses at the end to allow automated tracking and timeline linkage:
```bash
git commit -m "docs: added docker installation guide (#123)"
```
When submitting a Pull Request (PR) to the `main` branch, developers must use standard closure keywords (`closes #123`, `resolves #123`) within the PR description to ensure Gitea automatically resolves the ticket upon integration.
Binäre Datei nicht angezeigt.
+107
Datei anzeigen
@@ -0,0 +1,107 @@
APRS SSID Recommendations 6 Feb 2012
---------------------------------------------------------------------
Updated 6 Feb 2012 to clarify the title of this document to be
SSID RECOMMENDATIONS not to imply any kind of
decoding "standard". That original NMEA
decodign standard is covered in the original spec
Updated 9 June 2010 for more flexibility using 1,2,3,4 and 15
Revised 2 June 2004 to add -10, -11, 12 and -15
SSID's have seen two different uses in APRS. Initially as an ICON
indicator back in the early 1990's. But that is obsolete for over
a decade. Now SSID's are used as an informal way of indicating one
of several different typical APRS applications.
Since many small displays for the handheld and mobile operator show
nearby APRS station callsigns that flash up on the screen, it is nice
to have some idea of what type of station or activity might be
involved simply from the callsign SSID without having to push
buttons, search lists, or look at maps to find out more about them.
SSID RECOMMENDATIONS: It is very convenient to other mobile
operators or others looking at callsigns flashing by, to be able to
recognize some common applications at a glance. Here are the
recommendations for the 16 possible SSID's (the limit of 16 comes
from the 4 bits available in the AX.25 protocol. Note, The SSID of
zero is dropped by most display applications. So a callsign with no
SSID has an SSID of 0.
-0 Your primary station usually fixed and message capable
-1 generic additional station, digi, mobile, wx, etc
-2 generic additional station, digi, mobile, wx, etc
-3 generic additional station, digi, mobile, wx, etc
-4 generic additional station, digi, mobile, wx, etc
-5 Other networks (Dstar, Iphones, Androids, Blackberry's etc)
-6 Special activity, Satellite ops, camping or 6 meters, etc
-7 walkie talkies, HT's or other human portable
-8 boats, sailboats, RV's or second main mobile
-9 Primary Mobile (usually message capable)
-10 internet, Igates, echolink, winlink, AVRS, APRN, etc
-11 balloons, aircraft, spacecraft, etc
-12 APRStt, DTMF, RFID, devices, one-way trackers*, etc
-13 Weather stations
-14 Truckers or generally full time drivers
-15 generic additional station, digi, mobile, wx, etc
* One-way trackers should best use the -12 one-way SSID indicator
because the -9's usually mean a ham in full APRS communication
both message and voice. The -9's can be contacted by APRS message
or by Voice on his frequency included in his beacon, or on Voice
Alert if he is in simplex range. The -12's are just moving Icons
on the map and since they have no 2 way communication for ham radio
they are not generally of routine interest to other operators.
OBJECTS or INTERNET: In addition, Objects or internet generated
stations can have any SSID, not just the original 16, since
Objects are not constrained by the AX.25 header and can have a
9 byte name. Here are some common OBJECT/Internet SSID's:
-63 for PSK63 HF stations
-tt for APRS TouchTone users (DTMF)
-ID for RFID
-A through -Z for Dstar
SSID BACKGROUND: Originally, in 1992, we had to use the SSID as
a way of indicating the type of station that transmitted a raw
NMEA-0183 GPS sting. But in the mid 1990's we began indicating
any of the nearly 200 APRS symbols by the setting of the AX.25
TOCALL of "GPSxyz". The "xyz" characters define the symbol from
the standard APRS symbol table www.aprs.org/symbols.html.
The GPSxyz concept worked so well, the original SSID associations
are no longer a required part of the spec. But the conventions
that evolved from those early SSID's are still encouraged as
noted above, for easy recognition of station type or activity by
when only the callsign is seen.
The -1, -2, -3, -4 and -15 are kept generic so that anyone with
as many as 6 digipeaters, or 6 trackers or 6 weather stations or
6 vehicles can still have unique SSID's for each of his stations.
Beyond 6, people will just have to use any SSID that suits their
fancy. In some areas there might be 15 digipeaters all under
one guy's call!
SSID USAGE: The SSID's also might give a hint as to how someone
is getting into APRS whether via satellite, a one-way tracker, a
mobile, an HT or even via DTMF or an RFID device or whether he is
doing something special.
For example, if you are doing something special, change your
SSID to -6 to alert others to your excitement, or to make the
track-history begin and end on site, and not be tied to all
your other -9 travels. Or use -6 SSID for a packet sent
via the ISS or APRS satellite or for a 6 meter test so the
successful packet is preserved and not overwritten by the same
radio the next time you use it not via the ISS on the 144.39
national channel. By using separate SSID's the WEB data bases
will keep statistics and data separate from when you are working
normally on other bands with other SSID's.
So stick to the suggestions above for the obvious applications
where you can. Of course these are not rigid. If you have
more than 6 digipeaters, use any SSID you want. These are only
guidelines to hint at a station's possible application when all
you can see easily is the callsign on a screen or in a list....
de WB4APR, Bob
Binäre Datei nicht angezeigt.
+84
Datei anzeigen
@@ -58,3 +58,87 @@ The frequency block from **433.050 to 434.790 MHz** is shared globally as an **I
The 433 MHz ISM band in Europe is heavily congested by consumer devices like car key fobs, weather stations, and garage door openers. If you want to escape this interference completely, the European standard is to switch to the **868 MHz band**.
* **Note:** MeshCom itself is historically tied to 433 MHz in Austria. Shifting to 868 MHz effectively means migrating away from MeshCom infrastructure into the license-free **Meshtastic** ecosystem, which features a massive, highly dense node network across Austria on 868 MHz.
---
## 🔒 Part 3: LoRa Synchronization Words & Hardware Filtering
The **LoRa Synchronization Word (Sync Word)** is a critical 8-bit hardware filter configured inside register **`0x39` (`RegSyncWord`)** of the Semtech SX1276 transceiver. It functions as a physical RF barrier inside the radio's baseband modem. If an incoming packet's preamble synchronization pattern does not perfectly match the receiver's configured value, the hardware discards the payload instantly on chip-level, preventing the **`RxDone`** flag from firing and keeping the physical interrupt line completely silent.
### Technical Reference Matrix
While register `0x39` accepts any 8-bit value from `0x00` to `0xFF`, specific combinations have established themselves as international operational standards:
| Sync Word | Network Profile | Operational Target / Description |
| :--- | :--- | :--- |
| **`0x34`** | **Public LoRaWAN** | **MeshCom Standard** (ÖVSV standard for amateur radio mesh infrastructures operating at **433.175 MHz**). Triggers advanced hardware preamble correlation rules in the silicon for maximum noise immunity near the thermal noise floor. |
| **`0x24`** | **Tactical Mesh** | **Meshtastic Default** (Civilian off-grid digital communications). Used globally on the license-free 433 MHz ISM and 868 MHz bands. Requires modern low-data-rate optimization configurations to eliminate sensitivity degradation on legacy SX127x silicon steps. |
| **`0x12`** | **Private Network** | **Semtech Factory Default**. Standard baseline initialization value utilized for isolated point-to-point (P2P) bridging outside global networks. |
| **`0x55`** | **Isolated Array A** | **Custom / Experimental**. Alternating bit pattern (`01010101`) providing optimal transition density for the internal clock-data recovery (CDR) PLL loop. |
| **`0xAA`** | **Isolated Array B** | **Custom / Experimental**. Alternating bit pattern (`10101010`) offering optimal signal integrity transitions for proprietary cryptography deployments. |
> ⚠️ **The Preamble Correlation Trap:**
> Custom deployments should strictly avoid sync words with sparse bit transitions (such as `0x00` or `0xFF`). The internal baseband correlation engine relies on distinct signal transitions to sync its timing grid. Continuous sequences of zeros or ones drastically degrade receiver sensitivity.
### MeshCom Co-Existence Layout
To successfully capture live telemetry frames from official ÖVSV MeshCom gateways, the firmware forces a multi-register synchronization sequence during the unified thread-safe constructor pass:
```cpp
// Predefined LoRa Sync Word options for hardware correlation filtering
constexpr uint8_t LORA_SYNC_WORD_PUBLIC_NETWORK_MODE = 0x34; // MeshCom (ÖVSV standard for amateur radio mesh)
constexpr uint8_t LORA_SYNC_WORD_TACTICAL_NETWORK_MODE = 0x24; // Meshtastic (Civilian off-grid tactical mesh network)
constexpr uint8_t LORA_SYNC_WORD_PRIVATE_NETWORK_MODE = 0x12; // Semtech Default (Factory default / Private point-to-point)
constexpr uint8_t LORA_SYNC_WORD_CUSTOM_NETWORK_MODE = 0x55; // Custom / Experimental individual networks (highly isolated)
constexpr uint8_t LORA_SYNC_WORD_EXPERIMENTAL_NETWORK_MODE = 0xAA; // Custom / Experimental individual networks (highly isolated)
// Active hardware synchronization configuration selector
constexpr uint8_t LORA_SYNC_WORD = LORA_SYNC_WORD_PUBLIC_NETWORK_MODE;
```
This variable updates the Semtech baseband modem alongside an expanded preamble length of **12 symbols** to prevent packet dropping during long correlation windows:
```cpp
// Write the dynamic sync word byte to register 0x39
writeRegister(0x39, Config::LORA_SYNC_WORD);
// Expand hardware preamble length to 12 symbols for robust network correlation
writeRegister(0x20, 0x00U); // RegPreambleMsb
writeRegister(0x21, 0x0CU); // RegPreambleLsb (12 = 0x0C)
```
---
## 🛠️ Part 4: Troubleshooting Packet Drops & Telemetry Failures
When listening to live MeshCom grids or experimental point-to-point networks, your terminal might remain silent despite active RF activity in your area. This section details how to diagnose, interpret, and resolve low-level hardware anomalies using the automated register metrics layer.
### 1. The Low-Level Interrupt Diagnostics (Bypassing FreeRTOS Flags)
If your application task does not wake up, you must verify if the Semtech silicon is tracking any signals at all. By reading register **`0x12` (`RegIrqFlags`)**, you can isolate the failure source instantly:
```text
Reg 0x12 Value Hardware Status Root Cause & Resolution
-------------- ----------------------- -------------------------------------------------------------
0x00 No Signal Detected - Transceiver is out of range or frequency calibration mismatch.
- Check if your device is actively in RXCONTINUOUS mode [0x85].
0x20 PayloadCrcError Active - The packet was received but failed the hardware checksum.
- Caused by bad SNR (noise floor interference) or SF mismatch.
0x80 RxTimeout Active - The preamble was detected, but the complete frame timed out.
- Increase your preamble length to 12 symbols (Reg 0x20/0x21).
0x40 RxDone Success - The hardware captured, matched, and validated the packet!
- If terminal stays silent, check your FreeRTOS semaphore handles.
```
---
### 2. Common Integration Failure Modes
#### Symptom A: Zero Output from `readRegister(0x42)` during Boot
* **Root Cause:** The SPI peripheral host is misconfigured, or the hardware reset pin failed to pull low during the unified constructor pass.
* **Resolution:** Verify that `spi_bus_initialize()` is bound to the correct Host controller (`SPI2_HOST` for standard LilyGO TTGO boards). Ensure your initialization logic executes safely on **Core 0** inside the main task context to guarantee uniform driver registration.
#### Symptom B: The First Packet Arrives, then Total Silence
* **Root Cause:** The hardware interrupt flags within register `0x12` were not acknowledged, or the internal read pointer (`RegFifoAddrPtr`) was not explicitly reset to the packet's current base address.
* **Resolution:** You must overwrite register `0x12` with its active bit mask at the **absolute beginning** of your processing chain. This forces the physical `DIO0` line to fall back to logic low, enabling the ESP32 to detect subsequent rising edges (`GPIO_INTR_POSEDGE`). Always call `setModeReceiver()` at the end of the extraction loop to clear baseband modulations.
#### Symptom C: Constant `PayloadCrcError` [0x20] on Local MeshCom Traffic
* **Root Cause:** Inverted IQ configurations or local industrial noise (shared 433 MHz ISM congestion) is corrupting incoming headers.
* **Resolution:** Verify that your local modem target bandwidth exactly mirrors the MeshCom standard of **250 kHz** and **SF11**. For extreme near-field interference, ensure your `calculateTelemetry()` float calculation is active to track if sub-noise SNR regressions are pulling the signal strength below `-120 dBm`.
+91
Datei anzeigen
@@ -0,0 +1,91 @@
# FoxMash - Protocol
English version. In case of doubt, the English text shall be binding.
*[Deutsche Version anzeigen](foxmash-protocol_de.md)*
## Automatic Stations at the ARDF Transmitting Facilities (Foxes)
A fox regularly broadcasts its identifier and telemetry data.
This is done, whenever possible, at times when the fox is not transmitting on the 80m or 2m bands.
The purpose of this is, on the one hand, the time synchronization of adjacent nodes, and on the other hand, the regular official announcement of the call sign.
Usually, the call sign of the organizer or host of the direction-finding competition is used.
## Beacon
Composed based on the modified AX.25, APRS, and MeshCom protocols as follows:
Pseudo-Byte-Stream:
`{OE3TKT-03nnnnnn`
### Structure:
* **Byte 0: `{` (0x7B)**
* **Data Type Identifier (DTI):** Derived from the APRS & MeshCom protocol specification (see [APRS Protocol Reference](https://ui-view.net)).
* **Reasoning:** This data type identifier is currently marked as `[Unused]` in the specification, and the character visually resembles a station icon.
* **Bytes 1–6: `"OE3TKT"` (0x4F 0x45 0x33 0x54 0x4B 0x54)**
* **International Call Sign:** Formatted as a 1–2 character prefix, followed by one or more digits, and a 1–3 letter suffix.
* **Padding:** This field is strictly 6 characters wide. If the call sign is shorter, it must be right-padded with trailing spaces.
* **Bytes 7+: Station Number / SSID Identifier**
* **Standard Notation (IDs 0–99):** Uses a hyphen followed by a two-digit SSID (Bytes 7–9), for example, `"-03"` (0x2D 0x30 0x33).
* **Extended Notation (IDs ≥ 100):** If the station number exceeds 100, you may take any letters as described in https://aprs11/SSIDs.txt (e.g., `OE3TKT-S6` for any special 6th station).
* **Backup / Hot-Standby Nodes:** To designate a station as a replacement node or an immediate hot-standby node, the hyphen is replaced by a tilde symbol (e.g., `OE3TKT~03`).
* **Bytes 10–13: Telemetry Data (`nnnnnn` / Timestamp)**
* **Timestamp:** Signed 32-bit integer representing the number of milliseconds since the start of the competition day.
* **Byte 14: Own Battery Level**
* **Value:** Unsigned 8-bit integer (`unsigned char`) representing the node's own battery capacity in percent (0 to 100%).
A value of `100` (0x64) means fully charged, while `0` means empty.
Values above 100 can be used to indicate charging or mains power (e.g., `255`).
* **Bytes 15+: Heard Neighbor Nodes (Format: `xRSqxRSqxRSq...`)**
* Each neighbor node found in the mesh is represented by a multi-byte block containing its SSID, link quality, and battery status.
* **Bytes 15–16 (x):** Neighbor SSID (16-bit unsigned integer, e.g., `0x0003` for station number 3).
* **Byte 17 (R):** RSSI value + 164 stored as an `unsigned char` (e.g., `0x3A` = 58 ➡️ 58 - 164 = -106 dBm).
* **Byte 18 (S):** SNR value multiplied by 4 stored as a `signed char` (e.g., `0x42` = 66 ➡️ 66 / 4 = +16.5 dB).
* **Byte 19 (q):** Combined Quality and Battery Status Byte:
* **Bits 7–6 (Battery Status of the neighbor):**
* `00` = Normal (Battery OK / Powered)
* `01` = Low (Battery low, intervention recommended)
* `10` = Critical (Battery critical, node might shut down soon)
* `11` = Reserved
* **Bits 5–0 (Counter):** Moving average counter of how many times this station was heard within the last 30 minutes (Max value: 63). If this counter is ≥ 3, the neighbor is considered a stable link. Stations not heard at least once within 30 minutes are dropped from the list.
## Checkpoint Messages (Posten)
As soon as a participant checks in at a checkpoint using their RFID tag or identifier, they are registered at that node. The runner is assigned a timestamp and their ID (runner number). The mapping between the runner number and the RFID code is stored within the system. During the next available time slot, a checkpoint message is transmitted to the destination with QoS 2 (guaranteed packet delivery, duplicate detection, and filtering).
Messages are sent to the destination and optionally to the start node simultaneously. Both nodes must have an SSID < 15, as the remaining bits in the address byte are reserved for protocol flags.
This message packet is designed to be extremely short and efficient. It contains the node number, runner number, capture timestamp, and the current hop count.
Pseudo-Byte-Stream:
`vppMLzzzzRR`
### Structure (Checkpoint Message):
* **Byte 0: `(` (0x40)**
* **Data Type Identifier (DTI):** Special identifier designated for FoxMash checkpoint messages.
* **Bytes 1–2: Own SSID (`pp`)**
* **Value:** 16-bit unsigned integer (`uint16_t`) of the transmitting checkpoint node.
* **Byte 3: `M` (Target SSID, Hop Count & Flags)**
* **Bits 7–5 (Hop Count):** Dynamic routing counter. If the node sees the destination directly with good quality, this value is `0`. If no acknowledgment is received within 5 minutes, the permitted hop value is incremented by one. If the hop value exceeds the defined maximum, the packet is discarded.
* **Bit 4 (ACK Flag):** `0` for a standard data message, `1` for an acknowledgment packet (ACK).
* **Bits 3–0 (Target SSID):** The SSID of the destination node (Supports IDs from 0 to 15).
* **Bytes 4–5: `L` (Runner Number)**
* **Value:** 16-bit unsigned integer (`uint16_t`) for unique participant identification (Supports IDs from 0 to 65,535).
* **Bytes 6–9: `zzzz` (Timestamp)**
* **Value:** Signed 32-bit integer (milliseconds since the start of the day) capturing the exact time the runner checked in at the post. Also used to verify packet validity within the allowed time window.
* **Bytes 10+: `RR` (Routing List / Optional)**
* **Value:** Optional list of intermediate nodes. For example, when utilizing three relay nodes, this field is structured as `RR RR RR`. It remains empty if a direct connection is established.
### Acknowledgments and Retries
The maximum number of retries is configurable (default: 4). Every checkpoint packet must be acknowledged. The acknowledgment uses the exact same format (`vppMLzzzzRR`), but with **Bit 4 in Byte 3 (`M`) set to `1`**. The return routing follows the identical path in the reverse direction based on the route recorded in the packet (source routing). Remit nodes detect and discard duplicate packets by matching the runner's unique timestamp.
+318
Datei anzeigen
@@ -0,0 +1,318 @@
#set page(
paper: "a4",
margin: (x: 2cm, top: 3.5cm, bottom: 3cm),
// Kopfzeile mit dynamischem Platzhalter
header: align(right)[
#text(size: 8pt, fill: luma(120))[FoxMash Protocol v0.1]
]
)
#set text(font: "Libertinus Serif", lang: "en", size: 10.5pt)
#set par(justify: true)
// Styling-Funktion für Tabellen-Header
#let table-header(..cells) = {
cells.pos().map(cell => text(weight: "bold", fill: rgb("1a1a1a"))[#cell])
}
// ==========================================
// DYNAMISCHES HEADER-LOGO (Fuchs + ARDF-Schirm)
// ==========================================
#place(top + left, dy: -2.5cm)[
#grid(
columns: (60pt, 1fr),
gutter: 15pt,
align: horizon,
// Der ARDF-Postenschirm (Orange/Weißes Dreieck) gepaart mit einer Fuchs-Farbe
stack(
spacing: 2pt,
rect(width: 50pt, height: 50pt, stroke: 1pt + rgb("1a1a1a"), fill: white)[
#place(top + left)[
#polygon(
fill: rgb("ff5500"), // ARDF Orange
(0pt, 50pt),
(50pt, 0pt),
(50pt, 50pt)
)
]
// Stilisierte Fuchs-Silhouette (Ohren und Schnauze) ohne Kurven-Fehler
#place(center + horizon, dy: 2pt)[
#polygon(
fill: rgb("1a1a1a"),
(0pt, 0pt), // Linkes Ohr Spitze
(6pt, 8pt), // Kopf Mitte oben
(12pt, 0pt), // Rechtes Ohr Spitze
(12pt, 10pt), // Rechte Wange
(6pt, 18pt), // Schnauze unten
(0pt, 10pt) // Linke Wange
)
]
]
),
[
#text(size: 24pt, weight: "bold", tracking: 1pt, fill: rgb("1a1a1a"))[FoxMash] \
#v(-8pt)
#text(size: 10pt, weight: "medium", fill: rgb("ff5500"))[Amateur Radio Direction Finding Mesh Protocol]
]
)
]
#v(1cm)
= Protocol
English version. In case of doubt, the English text shall be binding. \
#link("foxmash-protocol_de.pdf")[_Deutsche Version anzeigen_]
== Automatic Stations at the ARDF Transmitting Facilities (Foxes)
A fox regularly broadcasts its identifier and telemetry data.
This is done, whenever possible, at times when the fox is not transmitting on the 80m or 2m bands.
The purpose of this is, on the one hand, the time synchronization of adjacent nodes, and on the other hand, the regular official announcement of the call sign.
Usually, the call sign of the organizer or host of the direction-finding competition is used.
== Beacon
Composed based on the modified AX.25, APRS, and MeshCom protocols as follows:
Pseudo-Byte-Stream:
```text
{OE3TKT-03CpnnnnxRSqxRSqxRSq..
```
=== Structure
#table(
columns: (1.5fr, 2fr, 5fr),
align: (center, left, left),
stroke: 0.5pt + luma(150),
fill: (x, y) => if y == 0 { rgb("f0f0f0") } else { none },
..table-header("Byte / Offset", "Field / Format", "Description"),
[Byte 0], [`"{"` `0x7B`], [
*Data Type Identifier (DTI):* Derived from the APRS & MeshCom protocol specification (see #link("https://ui-view.net")[APRS Protocol Reference]). \
*Reasoning:* Currently marked as `[Unused]` in the specification; character visually resembles a station icon.
],
[Bytes 1–6], [`"OE3TKT"` \ (6 Bytes ASCII)], [
*International Call Sign:* Formatted as a 1–2 character prefix, followed by one or more digits, and a 1–3 letter suffix. Strictly 6 characters wide, right-padded with trailing spaces if shorter.
],
[Byte 7], [`"-"` `0x2D` or `"~"` `0x7E`], [
*Node Type Separator:* Hyphen for standard nodes, tilde for backup/hot-standby nodes. Serves as a visual delimiter and structural anchor for the parser.
],
[Bytes 8–9], [`"03"`, or `"S6"` \ (2 Bytes ASCII)], [
*Station SSID Identifier:* Sub-string format representing the node ID. Supports numeric IDs (0–99) or extended notation (e.g., `"S6"`) i.e. any letters as described in #link("https://aprs11/SSIDs.txt")[aprs11/SSIDs.txt] (e.g., `OE3TKT-S6` for any special 6th station).
],
[Byte 10], [Own Battery Level \ (`unsigned char`)], [
*Value:* Capacity in percent (0 to 100%). Values above 100 indicate charging or mains power.
],
[Byte 11], [Protocol / Net Flags \ (`unsigned char`)], [
*Network Flags / Padding:* Reserved for mesh network flags and hop control. Ensures precise 4-byte memory alignment for the subsequent 32-bit timestamp.
],
[Bytes 12–15], [`nnnn` / \ Timestamp \ (`int32_t`, Little-Endian)], [
*Timestamp:* Signed 32-bit integer representing the number of milliseconds since the start of the competition day. Formatted as Little-Endian (LSB first) and aligned to a 4-byte boundary for optimized, native ESP32 pointer access. All official ARDF contest times and logs must be recorded in Coordinated Universal Time (UTC). To convert _[Local Time Name]_ to UTC, subtract/add _[X]_ hours (UTC = _[Local Time]_ +/- _[X]_h). Please pay close attention to date changes for events crossing midnight UTC.
],
[Bytes 15+], [Heard Neighbor Nodes \ (`xRSqxRSqxRSq...`)], [
Multi-byte blocks containing mesh neighbor details (SSID, link quality, battery status). See detailed breakdown below.
]
)
==== Heard Neighbor Nodes Block Breakdown (Bytes 15+)
#table(
columns: (1.5fr, 2fr, 5fr),
align: (center, left, left),
stroke: 0.5pt + luma(150),
fill: (x, y) => if y == 0 { rgb("f0f0f0") } else { none },
..table-header("Byte Offset", "Field / Format", "Description"),
[Bytes 15–16], [Neighbor SSID \ (2 Bytes ASCII)], [Two ASCII characters representing the neighbor's station ID/SSID (e.g., `"03"` or `"S6"`). This matches the exact sub-string format from Bytes 8–9 of the source node for easy string matching.],
[Byte 17], [RSSI Value (`R`) \ (`unsigned char`)], [Stored as RSSI + 164 (e.g., `0x3A` = 58 -> 58 - 164 = -106 dBm).],
[Byte 18], [SNR Value (`S`) \ (`signed char`)], [Stored as SNR multiplied by 4 (e.g., `0x42` = 66 -> 66 / 4 = +16.5 dB).],
[Byte 19], [Status Byte (`q`) \ (`unsigned char`)], [Combined Quality and Battery Status Byte. (See Bit-mapping below).]
)
#v(0.5em)
*Byte 19 (`q`) Bit-mapping:*
#table(
columns: (1.5fr, 1.5fr, 6fr),
align: (center, center, left),
stroke: 0.5pt + luma(150),
fill: (x, y) => if y == 0 { rgb("f5f5f5") } else { none },
..table-header("Bits", "Value", "Definition"),
[Bits 7–6], [`00`], [Normal (Battery OK / Powered)],
[Bits 7–6], [`01`], [Low (Battery low, intervention recommended)],
[Bits 7–6], [`10`], [Critical (Battery critical, node might shut down soon)],
[Bits 7–6], [`11`], [Reserved],
[Bits 5–0], [0 to 63], [Moving average counter of how many times this station was heard within the last 30 minutes. If >= 3, the link is considered stable. Dropped if 0 within 30 minutes.]
)
== Checkpoint Messages (Posten)
As soon as a participant checks in at a checkpoint using their RFID tag or identifier, they are registered at that node. The runner is assigned a timestamp and their ID (runner number). The mapping between the runner number and the RFID code is stored within the system. During the next available time slot, a checkpoint message is transmitted to the destination with QoS 2 (guaranteed packet delivery, duplicate detection, and filtering).
Messages are sent to the destination and optionally to the start node simultaneously. Both nodes must have an SSID < 15, as the remaining bits in the address byte are reserved for protocol flags.
This message packet is designed to be extremely short and efficient. It contains the node number, runner number, capture timestamp, and the current hop count.
Pseudo-Byte-Stream:
```text
(ppMLzzzzRR
```
=== Structure (Checkpoint Message)
#table(
columns: (1.5fr, 2fr, 5fr),
align: (center, left, left),
stroke: 0.5pt + luma(150),
fill: (x, y) => if y == 0 { rgb("f0f0f0") } else { none },
..table-header("Byte / Offset", "Field / Format", "Description"),
[Byte 0], [`(` (0x40)], [
*Data Type Identifier (DTI):* Special identifier designated for FoxMash checkpoint messages.
],
[Bytes 1–2], [Own SSID (`pp`) \ (2 Bytes ASCII)], [
Two ASCII characters representing the transmitting checkpoint node.
],
[Byte 3], [Routing & Flags (`M`) \ (`unsigned char`)], [
Combined Target SSID, Hop Count, and ACK Flags. (See Bit-mapping below).
],
[Bytes 4–7], [`zzzz` (Timestamp) \ (`int32_t`, Little-Endian)], [
*Timestamp:* Signed 32-bit integer representing the number of milliseconds since the start of the competitoin day. Formatted as Little-Endian (LSB first) and aligned to a 4-byte boundery for direct, high-performance ESP32 pointer access.
],
[Bytes 8–9], [Runner Number (`L`) \ (`uint16_t`)], [
Unique participant identification / runner number (Supports IDs from 0 to 65,535).
],
[Bytes 10], [Hop Count / Flags \ (`unsigned char`)], [
Current network hop count and remaing mesh routing flags.
]
)
#v(0.5em)
*Byte 3 (`M`) Bit-mapping:*
#table(
columns: (1.5fr, 1.5fr, 6fr),
align: (center, center, left),
stroke: 0.5pt + luma(150),
fill: (x, y) => if y == 0 { rgb("f5f5f5") } else { none },
..table-header("Bits", "Flag / Field", "Definition"),
[Bits 7–5], [Hop Count], [Dynamic routing counter. `0` if destination is seen directly with good quality. Incremented by 1 if no ACK within 5 minutes. Discarded if it exceeds max hop value.],
[Bit 4], [ACK Flag], [`0` for a standard data message, `1` for an acknowledgment packet (ACK).],
[Bits 3–0], [Target SSID], [The SSID of the destination node (Supports IDs from 0 to 15).]
)
=== Acknowledgments and Retries
The maximum number of retries is configurable (default: 4). Every checkpoint packet must be acknowledged. The acknowledgment uses the exact same format (`vppMLzzzzRR`), but with *Bit 4 in Byte 3 (`M`) set to `1`*. The return routing follows the identical path in the reverse direction based on the route recorded in the packet (source routing). Remit nodes detect and discard duplicate packets by matching the runner's unique timestamp.
== Protocol Optimization Analysis
=== Hardware-Optimized Protocol Design for ESP32 and LoRa
When designing low-power, high-performance mesh networks using *ESP32* microcontrollers over *LoRa* modulation, data structure layout critically impacts both transmission time (Airtime) and CPU efficiency. The updated FoxMash protocol layout restructures fields to achieve an optimal balance between RF efficiency and hardware execution.
==== Why this Layout is Perfect
1. *Native Word Alignment (No Performance Penalties):* The Tensilica Xtensa and RISC-V architectures used in ESP32 chips are natively *32-bit Little-Endian* processors. Accessing a 4-byte integer from a memory address that is not a multiple of 4 (_unaligned access_) causes the hardware to execute multi-cycle fixes, shifting bytes internally. In worst-case scenarios or specific compiler configurations, this causes a `LoadStoreAlignmentCause` hardware crash (Core Dump). By placing the 32-bit timestamp at precisely *Byte 4* (Checkpoint) and *Byte 12* (Beacon), the integer aligns perfectly with the processor's natural word boundaries.
2. *Zero-Copy Pointer Casting:* Because fields are naturally aligned, the firmware does not need to waste clock cycles using `memcpy()` or bit-shifting operators (`<<`, `|`) to reconstruct integers from individual bytes. The application layer can execute direct pointer casting directly out of the LoRa hardware receive buffer.
3. *Zero Airtime Overhead:* Traditional alignment techniques insert dummy padding bytes (`0x00`) to force alignment, which inflates the packet size. In LoRa networks, every additional byte directly increases the Time-on-Air (ToA), resulting in higher energy consumption and a higher probability of packet collisions. This optimized layout achieves perfect alignment solely by *reordering existing essential fields*, keeping the frame size strictly minimal.
4. *Human-Readable Delimiters Maintained:* Keeping the standard hyphen (`"-"`) or tilde (`"~"`) delimiter between the international call sign and the SSID preserves compatibility with legacy APRS/MeshCom parsers and keeps raw hexadecimal logs intuitive during serial debugging.
=== High-Performance C++ Implementation Example
The following code snippet demonstrates how to leverage this memory-aligned layout within an ESP32 application utilizing standard Arduino or ESP-IDF LoRa libraries.
#rect(width: 100%, stroke: 0.5pt + luma(150), fill: luma(245), radius: 4pt)[
```cpp
#include <Arduino.h>
// Ensure the compiler doesn't add structural padding within our structs
#pragma pack(push, 1)
struct BeaconPacket {
char dti; // Offset 0 (0x7B '{')
char callsign[6]; // Offset 1 (e.g., "OE3TKT")
char separator; // Offset 7 (e.g., '-')
char ssid[2]; // Offset 8 (e.g., "03")
uint8_t battery; // Offset 10 (0-100%)
uint8_t net_flags; // Offset 11 (Routing control / Padding)
int32_t timestamp_ms; // Offset 12 -> PERFECTLY 4-BYTE ALIGNED!
uint8_t neighbor_block[]; // Offset 16+ (Dynamic mesh payload)
};
struct CheckpointPacket {
char dti; // Offset 0 (0x28 '(')
uint16_t own_ssid; // Offset 1
uint8_t routing_flags; // Offset 3 (Target / Control Flags)
int32_t timestamp_ms; // Offset 4 -> PERFECTLY 4-BYTE ALIGNED!
uint16_t runner_id; // Offset 8
uint8_t hop_count; // Offset 10
};
#pragma pack(pop)
// Example parser function processing incoming raw LoRa bytes
void processIncomingLoRa(uint8_t* rxBuffer, size_t packetLength) {
if (packetLength < 1) return;
// Check Data Type Identifier (DTI)
char dti = rxBuffer[0];
if (dti == '(') {
// Zero-copy direct cast pointer access
CheckpointPacket* cp = (CheckpointPacket*)rxBuffer;
// Fast native access directly from memory
int32_t time = cp->timestamp_ms;
uint16_t runner = cp->runner_id;
Serial.printf("Runner %d checked in. Local Timestamp: %d ms\n", runner, time);
}
else if (dti == '{') {
BeaconPacket* beacon = (BeaconPacket*)rxBuffer;
// Fast native access directly from memory
int32_t time = beacon->timestamp_ms;
Serial.printf("Beacon received. Network Timestamp: %d ms\n", time);
}
}
```
]
// ==========================================
// GESTALTETER FOOTER (Name & Typst-Hinweis)
// ==========================================
#v(3em)
#line(length: 100%, stroke: 0.5pt + luma(200))
#v(0.5em)
#align(center)[
#text(size: 9pt, fill: luma(100))[
Document maintained and compiled by *[OE3TKT, Tom]* \
#v(2pt)
#text(size: 8pt, style: "italic")[
Typeset beautifully with #link("https://typst.app")[Typst] #box(width: 8pt, image(bytes("<svg viewBox='0 0 24 24' width='8' height='8'><path fill='#ff5500' d='M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z'/></svg>"), format: "svg"))
]
]
]
+106
Datei anzeigen
@@ -0,0 +1,106 @@
# FoxMash - Protokoll
Deutsche Übersetzung. Im Zweifelsfall ist die englischsprachige Version bindend.
*[View English Version](foxmash-protocol.md)*
## Automatische Stationen bei den ARDF-Sendeeinrichtungen (Füchsen)
Ein Fuchs sendet regelmäßig seine Kennung (Identifier) und Telemetriedaten aus.
Dies geschieht, wann immer möglich, zu Zeiten, in denen kein Sendebetrieb des Fuchses im 80m- oder 2m-Band stattfindet.
Dies dient einerseits der Zeitsynchronisation der Nachbarknoten und andererseits der regelmäßigen, behördlich vorgeschriebenen Aussendung des Rufzeichens.
Üblicherweise wird das Rufzeichen des Ausrichters bzw. Veranstalters des Peilwettbewerbs verwendet.
## Bake (Broadcast)
Setzt sich wie folgt aus den modifizierten AX.25-, APRS- und MeshCom-Protokollen zusammen:
Pseudo-Byte-Stream:
`{OE3TKT-03nnnnxxRSqxxRSqxxRSq`
### Struktur:
* **Byte 0: `{` (0x7B)**
* **Data Type Identifier (DTI):** Abgeleitet aus der APRS- & MeshCom-Protokollspezifikation (siehe [APRS Protocol Reference](https://ui-view.net)).
* **Begründung:** Dieser Datentyp-Bezeichner ist in der Spezifikation derzeit noch als `[Unused]` (unbenutzt) markiert, und das Zeichen sieht visuell wie ein Stationssymbol aus.
* **Bytes 1–6: `"OE3TKT"` (0x4F 0x45 0x33 0x54 0x4B 0x54)**
* **Internationales Rufzeichen:** Formatiert als 1–2 Zeichen langes Präfix, gefolgt von einer oder mehreren Ziffern und einem 1–3 Zeichen langen Suffix.
* **Padding (Auffüllung):** Dieses Feld ist strikt 6 Zeichen lang. Wenn das Rufzeichen kürzer ist, muss es rechtsbündig mit Leerzeichen aufgefüllt werden.
* **Bytes 7+: Stationsnummer / SSID-Kennung**
* **Standard-Notation (IDs 0–99):** Verwendet einen Bindestrich, gefolgt von einer zweistelligen SSID (Bytes 7–9), zum Beispiel `"-03"` (0x2D 0x30 0x33).
* **Erweiterte Notation (IDs ≥ 100):** Wenn die Stationsnummer 100 überschreitet, können beliebige Buchstaben verwendet werden, wie unter https://aprs11/SSIDs.txt beschrieben (z. B. `OE3TKT-S6` für eine beliebige spezielle 6. Station).
* **Ersatz- / Hot-Standby-Knoten:** Um eine Station als Ersatzstation oder sofort einsatzbereiten Knoten zu kennzeichnen, wird der Bindestrich durch ein Tilde-Zeichen ersetzt (z. B. `OE3TKT~03`).
* **Bytes 10–13: Telemetriedaten (`nnnnnn` / Zeitstempel)**
* **Zeitstempel:** Vorzeichenbehaftete 32-Bit-Ganzzahl (Signed 32-Bit Integer), die die Millisekunden seit Beginn des Wettbewerbstages darstellt.
Dieser Zeitstempel dient gleichzeitig auch als Nachrichten-ID, und die gesamte Nachricht wird gehasht und damit kann festgestellt werden, ob sie doppelt ist.
* **Byte 14: Eigener Batteriestand**
* **Wert:** Vorzeichenlose 8-Bit-Ganzzahl (`unsigned char`), die die eigene Batteriekapazität in Prozent (0 bis 100 %) darstellt. Ein Wert von `100` (0x64) bedeutet voll geladen, `0` bedeutet leer. Werte über 100 können optional für Netzbetrieb oder Ladevorgang reserviert werden (z. B. `255`).
* **Bytes 15+: Gehörte Nachbarknoten (Format: `xxRSqxxRSqxxRSq...`)**
* Jeder im Mesh gefundene Nachbarknoten wird durch einen Multi-Byte-Block dargestellt, der seine SSID, die Link-Qualität und den Batteriestatus enthält.
* **Bytes 15–16 (x):** Nachbar-SSID (16-Bit Unsigned Integer, z. B. `0x0003` für Stationsnummer 3).
* **Byte 17 (R):** RSSI-Wert + 164 als `unsigned char` (z. B. `0x3A` = 58 ➡️ 58 - 164 = -106 dBm).
* **Byte 18 (S):** Vierfacher SNR-Wert als `signed char` (z. B. `0x42` = 66 ➡️ 66 / 4 = +16.5 dB).
* **Byte 19 (q):** Kombiniertes Qualitäts- und Batteriestatus-Byte:
* **Bits 7–6 (Batteriezustand des Nachbarn):**
* `00` = Normal (Batterie OK / Netzbetrieb)
* `01` = Niedrig (Batterie schwach, Wartung empfohlen)
* `10` = Kritisch (Batterie kritisch, Knoten fällt bald aus)
* `11` = Reserviert
* **Bits 5–0 (Zähler):** Gleitender Zähler, wie oft diese Station in den letzten 30 Minuten gehört wurde (Maximalwert: 63). Wenn dieser Zähler ≥ 3 ist, gilt der Nachbar als stabiler Link. Stationen, die innerhalb von 30 Minuten kein einziges Mal gehört wurden, werden aus der Liste gelöscht.
Falls der eigne Knoten die Ziel/Start-Knoten nicht selbst sehen, so müssen
## Nachrichten von Posten
Sobald eine Person am Postenschirm seine Kennung bekannt gibt wird er an diesem Knoten registriert.
Ein Zeitstempel und seine ID wird zugewiesen (Läufernummer).
Die Läufernummer und die entsprechende RFID-Nummer ist im System hinterlegt und es wird beim nächsten Zeitschlitz eine Posten-Nachricht an das Ziel verschickt.
Das Ziel und der Start-Knoten ist dem System bekannt (muss nicht zwingend SSID 0 haben).
Jedoch: Muss eine SSID < 15 haben, da wir die anderen bits für Flags verwenden.
Eventuell wird an beide Knoten, Ziel und Start gleichzeitig verschickt.
Die QoS ist dabei nicht mehr 0 sondern 2. (Sicherstellung der Paketzustellung und doppelte Pakete werden erkannt und weggeworfen).
Dieses Nachrichtenpaket ist sehr kurz und effizient aufgebaut:
Wir brauchen folgende Informationen: Knotennummer, Läufernummer, Zeitstempel, und der wievielte Hopp es ist.
## Nachrichten von Posten
Sobald sich ein Teilnehmer am Postenschirm per RFID/Kennung anmeldet, wird er an diesem Knoten registriert. Dem Läufer wird ein Zeitstempel und seine ID (Läufernummer) zugewiesen. Die Verknüpfung zwischen Läufernummer und RFID-Code ist im System hinterlegt. Beim nächsten verfügbaren Zeitschlitz wird eine Posten-Nachricht mit der Dienstgüte QoS 2 (Sicherstellung der Paketzustellung, Erkennung und Verwerfen von Duplikaten) an das Ziel übermittelt.
Die Nachrichten werden an das Ziel und optional gleichzeitig an den Start-Knoten gesendet. Beide Knoten müssen eine SSID < 15 besitzen, da die verbleibenden Bits im Adress-Byte für Protokoll-Flags reserviert sind.
Dieses Nachrichtenpaket ist extrem kurz und effizient aufgebaut. Es enthält die Knotennummer, die Läufernummer, den Zeitstempel der Erfassung sowie den aktuellen Hop-Zähler.
Pseudo-Byte-Stream:
`vppMLzzzzRR`
### Struktur (Posten-Nachricht):
* **Byte 0: `(` (0x40)**
* **Data Type Identifier (DTI):** Spezieller Bezeichner für FoxMash-Postenmeldungen.
* **Bytes 1–2: Eigene SSID (`pp`)**
* **Wert:** 16-Bit vorzeichenlose Ganzzahl (`uint16_t`) des sendenden Posten-Knotens.
* **Byte 3: `M` (Ziel-SSID, Hop-Limit / TTL & Flags)**
* **Bits 7–5 (Hop-Limit / TTL):** Startwert für die maximale Anzahl an erlaubten Hops (Time-to-Live). Der Startknoten setzt diesen Wert (z. B. auf 3). Jeder weiterleitende Zwischenknoten verringert (dekrementiert) diesen Wert um 1. Erreicht der Wert `0`, wird das Paket nicht mehr weitergeleitet und verworfen.
* **Bit 4 (ACK-Flag):** `0` für eine normale Nachricht, `1` für ein Bestätigungspaket (ACK).
* **Bits 3–0 (Ziel-SSID):** Die SSID des Zielknotens (Unterstützt IDs von 0 bis 15).
* **Bytes 4–5: `L` (Läufernummer)**
* **Wert:** 16-Bit vorzeichenlose Ganzzahl (`uint16_t`) zur eindeutigen Identifikation des Teilnehmers (Unterstützt IDs von 0 bis 65.535).
* **Bytes 6–9: `zzzz` (Zeitstempel)**
* **Wert:** Vorzeichenbehaftete 32-Bit-Ganzzahl (Millisekunden seit Tagesbeginn), die den exakten Zeitpunkt der Registrierung des Läufers am Posten festhält. Dient auch zur Gültigkeitsprüfung im Zeitrahmen.
* **Bytes 10+: `RR` (Routingliste / Optional)**
* **Wert:** Optionale Liste der Zwischenknoten. Bei der Verwendung von beispielsweise drei Relaisknoten setzt sich das Feld aus `RR RR RR` zusammen. Bleibt leer, wenn eine Direktverbindung besteht.
### Bestätigung und Wiederholungen (Retries)
Die maximale Anzahl an Sendeversuchen (Retries) ist konfigurierbar (Standard: 4). Jedes Posten-Paket muss zwingend bestätigt werden. Die Bestätigung verwendet das exakt gleiche Format (`vppMLzzzzRR`), wobei jedoch **Bit 4 im Byte 3 (`M`) auf `1` gesetzt** ist. Das Antwort-Routing nutzt anhand des im Paket aufgezeichneten Pfades dieselbe Strecke in umgekehrter Richtung (Source Routing). Duplikate werden empfängerseitig anhand des eindeutigen Läufer-Zeitstempels erkannt und verworfen.
+317
Datei anzeigen
@@ -0,0 +1,317 @@
#set page(
paper: "a4",
margin: (x: 2cm, top: 3.5cm, bottom: 3cm),
// Kopfzeile mit dynamischem Platzhalter
header: align(right)[
#text(size: 8pt, fill: luma(120))[FoxMash-Protokoll v0.1]
]
)
#set text(font: "Libertinus Serif", lang: "de", size: 10.5pt)
#set par(justify: true)
// Styling-Funktion für Tabellen-Header
#let table-header(..cells) = {
cells.pos().map(cell => text(weight: "bold", fill: rgb("1a1a1a"))[#cell])
}
// ==========================================
// DYNAMISCHES HEADER-LOGO (Fuchs + ARDF-Schirm)
// ==========================================
#place(top + left, dy: -2.5cm)[
#grid(
columns: (60pt, 1fr),
gutter: 15pt,
align: horizon,
// Der ARDF-Postenschirm (Orange/Weißes Dreieck) gepaart mit einer Fuchs-Farbe
stack(
spacing: 2pt,
rect(width: 50pt, height: 50pt, stroke: 1pt + rgb("1a1a1a"), fill: white)[
#place(top + left)[
#polygon(
fill: rgb("ff5500"), // ARDF Orange
(0pt, 50pt),
(50pt, 0pt),
(50pt, 50pt)
)
]
// Stilisierte Fuchs-Silhouette (Ohren und Schnauze) ohne Kurven-Fehler
#place(center + horizon, dy: 2pt)[
#polygon(
fill: rgb("1a1a1a"),
(0pt, 0pt), // Linkes Ohr Spitze
(6pt, 8pt), // Kopf Mitte oben
(12pt, 0pt), // Rechtes Ohr Spitze
(12pt, 10pt), // Rechte Wange
(6pt, 18pt), // Schnauze unten
(0pt, 10pt) // Linke Wange
)
]
]
),
[
#text(size: 24pt, weight: "bold", tracking: 1pt, fill: rgb("1a1a1a"))[FoxMash] #v(-8pt)
#text(size: 10pt, weight: "medium", fill: rgb("ff5500"))[Amateur Radio Direction Finding Mesh Protocol]
]
)
]
#v(1cm)
= Protokoll
Deutsche Version. Im Zweifelsfall ist der englische Text verbindlich. \
#link("foxmash-protocol.pdf")[_Show English version_]
== Automatische Stationen an den ARDF-Senderanlagen (Füchse)
Ein Fuchs sendet regelmäßig seine Kennung und Telemetriedaten aus.
Dies geschieht, wenn möglich, zu Zeiten, in denen der Fuchs nicht auf den Bändern 80m oder 2m sendet.
Dies dient einerseits der Zeitsynchronisation benachbarter Knoten und andererseits der regelmäßigen offiziellen Nennung des Rufzeichens.
Meist wird das Rufzeichen des Organisators oder Ausrichters des Peilwettbewerbs verwendet.
== Beacon (Bake)
Aufgebaut auf Basis der modifizierten AX.25-, APRS- und MeshCom-Protokolle wie folgt:
Pseudo-Byte-Stream:
```text
{OE3TKT-03CpnnnnxRSqxRSqxRSq..
```
=== Struktur
#table(
columns: (1.5fr, 2fr, 5fr),
align: (center, left, left),
stroke: 0.5pt + luma(150),
fill: (x, y) => if y == 0 { rgb("f0f0f0") } else { none },
..table-header("Byte / Offset", "Feld / Format", "Beschreibung"),
[Byte 0], [`"{"` `0x7B`], [
*Data Type Identifier (DTI):* Abgeleitet aus der APRS- und MeshCom-Protokollspezifikation (siehe #link("https://ui-view.net")[APRS-Protokollreferenz]). \
*Begründung:* Aktuell in der Spezifikation als `[Unused]` markiert; Zeichen ähnelt visuell einem Stationssymbol.
],
[Bytes 1–6], [`"OE3TKT"` \ (6 Bytes ASCII)], [
*Internationales Rufzeichen:* Formatiert als 1–2 Zeichen Präfix, gefolgt von einer oder mehreren Ziffern und einem 1–3 Zeichen Suffix. Strikt 6 Zeichen breit; bei kürzeren Rufen rechts mit Leerzeichen aufgefüllt.
],
[Byte 7], [`"-"` `0x2D` oder `"~"` `0x7E`], [
*Knotentyp-Trennzeichen:* Bindestrich für Standardknoten, Tilde für Backup- / Hot-Standby-Knoten. Dient als visueller Begrenzer und struktureller Anker für den Parser.
],
[Bytes 8–9], [`"03"`, oder `"S6"` \ (2 Bytes ASCII)], [
*Stations-SSID-Kennung:* Teilstring-Format, das die Knoten-ID darstellt. Unterstützt numerische IDs (0–99) oder erweiterte Schreibweisen (z. B. `"S6"`), d. h. alle Zeichen wie in #link("https://aprs11/SSIDs.txt")[aprs11/SSIDs.txt] beschrieben (z. B. `OE3TKT-S6` für eine spezielle 6. Station).
],
[Byte 10], [Eigener Batteriestand \ (`unsigned char`)], [
*Wert:* Kapazität in Prozent (0 bis 100 %). Werte über 100 weisen auf Ladevorgang oder Netzbetrieb hin.
],
[Byte 11], [Protokoll- / Netz-Flags \ (`unsigned char`)], [
*Netzwerk-Flags / Padding:* Reserviert für Mesh-Netzwerk-Flags und Hop-Steuerung. Gewährleistet die präzise 4-Byte-Speicherausrichtung (Alignment) für den nachfolgenden 32-Bit-Zeitstempel.
],
[Bytes 12–15], [`nnnn` / \ Zeitstempel \ (`int32_t`, Little-Endian)], [
*Zeitstempel:* Vorzeichenbehafteter 32-Bit-Integer, der die Anzahl der Millisekunden seit Beginn des Wettbewerbstages darstellt. Formatiert als Little-Endian (LSB zuerst) und auf eine 4-Byte-Grenze ausgerichtet für optimierten, nativen ESP32-Zeigerzugriff. Alle offiziellen ARDF-Wettbewerbszeiten und -Logs müssen in koordinierter Weltzeit (UTC) geführt werden. Um _[Lokaler Zeitzonenname]_ in UTC umzurechnen, subtrahiere/addiere _[X]_ Stunden (UTC = _[Lokale Zeit]_ +/- _[X]_h). Bitte achte genau auf Datumswechsel bei Ereignissen, die UTC-Mitternacht überschreiten.
],
[Bytes 16+], [Gehörte Nachbarknoten \ (`xRSqxRSqxRSq...`)], [
Multi-Byte-Blöcke, die Details zu Mesh-Nachbarknoten enthalten (SSID, Verbindungsqualität, Batteriestatus). Siehe detaillierte Aufschlüsselung unten.
]
)
==== Aufschlüsselung des Blocks der gehörten Nachbarknoten (Bytes 16+)
#table(
columns: (1.5fr, 2fr, 5fr),
align: (center, left, left),
stroke: 0.5pt + luma(150),
fill: (x, y) => if y == 0 { rgb("f0f0f0") } else { none },
..table-header("Byte-Offset", "Feld / Format", "Beschreibung"),
[Bytes 16–17], [Nachbar-SSID \ (2 Bytes ASCII)], [
Zwei ASCII-Zeichen, welche die SSID des Nachbarknotens darstellen (z. B. `"03"` oder `"S6"`). Dies entspricht dem exakten Teilstring-Format aus den Bytes 8–9 des Quellknotens für einfachen String-Abgleich. \
*Padding:* Einstellige Stationen sollten links mit einem Null-Byte (`0x00`) aufgefüllt werden (z. B. `0x00 0x33` für Station 3). \
*Parsing-Regeln:* Die Firmware-Implementierung muss flexibel sein. Der Parser muss führende Null-Bytes (`0x00`), führende Leerzeichen (`0x20`) und führende ASCII-Nullen (`0x30`) unterstützen und fehlerfrei dekodieren.
],
[Byte 18], [RSSI-Wert (`R`) \ (`unsigned char`)], [Gespeichert als RSSI + 164 (z. B. `0x3A` = 58 -> 58 - 164 = -106 dBm).],
[Byte 19], [SNR-Wert (`S`) \ (`signed char`)], [Gespeichert als SNR multipliziert mit 4 (z. B. `0x42` = 66 -> 66 / 4 = +16,5 dB).],
[Byte 20], [Status-Byte (`q`) \ (`unsigned char`)], [Kombiniertes Byte für Qualität und Batteriestatus. (Siehe Bit-Mapping unten).]
)
#v(0.5em)
*Bit-Mapping für Byte 20 (`q`):*
#table(
columns: (1.5fr, 1.5fr, 6fr),
align: (center, center, left),
stroke: 0.5pt + luma(150),
fill: (x, y) => if y == 0 { rgb("f5f5f5") } else { none },
..table-header("Bits", "Wert", "Definition"),
[Bits 7–6], [`00`], [Normal (Battery OK / Powered)],
[Bits 7–6], [`01`], [Low (Battery low, intervention recommended)],
[Bits 7–6], [`10`], [Critical (Battery critical, node might shut down soon)],
[Bits 7–6], [`11`], [Reserved],
[Bits 5–0], [0 to 63], [Moving average counter of how many times this station was heard within the last 30 minutes. If >= 3, the link is considered stable. Dropped if 0 within 30 minutes.]
)
== Kontrollpunkt-Nachrichten (Posten / Checkpoints)
Sobald sich ein Teilnehmer an einem Kontrollpunkt mittels seines RFID-Tags oder seiner Kennung registriert, wird er an diesem Knoten erfasst. Dem Läufer wird ein Zeitstempel und seine ID (Startnummer) zugewiesen. Die Zuordnung zwischen Startnummer und RFID-Code ist systemintern hinterlegt. Während des nächsten verfügbaren Zeitschlitzes wird eine Kontrollpunkt-Nachricht mit QoS 2 (garantierte Paketzustellung, Duplikaterkennung und -filterung) an das Ziel übertragen.
Nachrichten werden gleichzeitig an den Zielknoten und optional an den Startknoten gesendet. Beide Knoten müssen eine SSID < 15 besitzen, da die verbleibenden Bits im Adress-Byte für Protokoll-Flags reserviert sind.
Dieses Nachrichtenpaket ist extrem kurz und effizient konzipiert. Es enthält die Knotennummer, die Startnummer, den Erfassungs-Zeitstempel und den aktuellen Hop-Count.
Pseudo-Byte-Stream:
```text
(ppMzzzzLLR
```
=== Structure (Checkpoint Message)
#table(
columns: (1.5fr, 2fr, 5fr),
align: (center, left, left),
stroke: 0.5pt + luma(150),
fill: (x, y) => if y == 0 { rgb("f0f0f0") } else { none },
..table-header("Byte / Offset", "Feld / Format", "Beschreibung"),
[Byte 0], [`(` (0x28)], [
*Data Type Identifier (DTI):* Spezielle Kennung, die für FoxMash-Kontrollpunkt-Nachrichten vorgesehen ist.
],
[Bytes 1–2], [Eigene SSID (`pp`) \ (`uint16_t`)], [
16-Bit unsigned Integer des sendenden Kontrollpunkt-Knotens.
],
[Byte 3], [Routing & Flags (`M`) \ (`unsigned char`)], [
Kombinierte Ziel-SSID und Protokoll-Steuerungs-Flags. Dient gleichzeitig als 1-Byte-Grenzausrichter (Alignment).
],
[Bytes 4–7], [`zzzz` (Zeitstempel) \ (`int32_t`, Little-Endian)], [
*Zeitstempel:* Vorzeichenbehafteter 32-Bit-Integer, der die Anzahl der Millisekunden seit Beginn des Wettbewerbstages darstellt. Formatiert als Little-Endian (LSB zuerst) und auf eine 4-Byte-Grenze ausgerichtet für direkten, hochperformanten ESP32-Zeigerzugriff.
],
[Bytes 8–9], [Startnummer (`L`) \ (`uint16_t`)], [
Eindeutige Teilnehmeridentifikation / Startnummer (Unterstützt IDs von 0 bis 65.535).
],
[Bytes 10], [Hop-Count / Flags \ (`unsigned char`)], [
Aktueller Netzwerk-Hop-Count und verbleibende Mesh-Routing-Flags.
]
)
#v(0.5em)
*Bit-Mapping für Byte 3 (`M`):*
#table(
columns: (1.5fr, 1.5fr, 6fr),
align: (center, center, left),
stroke: 0.5pt + luma(150),
fill: (x, y) => if y == 0 { rgb("f5f5f5") } else { none },
..table-header("Bits", "Flag / Feld", "Definition"),
[Bits 7–5], [Hop-Count], [Dynamischer Routing-Zähler. `0`, wenn das Ziel direkt mit guter Qualität sichtbar ist. Wird um 1 erhöht, wenn innerhalb von 5 Minuten kein ACK erfolgt. Wird verworfen, wenn der maximale Hop-Wert überschritten wird.],
[Bit 4], [ACK-Flag], [`0` für eine Standard-Datennachricht, `1` für ein Bestätigungspaket (ACK).],
[Bits 3–0], [Ziel-SSID], [Die SSID des Zielknotens (Unterstützt IDs von 0 bis 15).]
)
=== Bestätigungen (Acknowledgments) und Wiederholungen
Die maximale Anzahl der Sendeversuche ist konfigurierbar (Standard: 4). Jedes Kontrollpunkt-Paket muss quittiert werden. Die Bestätigung verwendet exakt dasselbe Format (`vppMLzzzzRR`), jedoch mit *auf `1` gesetztem Bit 4 in Byte 3 (`M`)*. Das Rückrouting erfolgt auf dem identischen Pfad in umgekehrter Richtung basierend auf der im Paket aufgezeichneten Route (Source-Routing). Weiterleitende Knoten erkennen und verwerfen Duplikate durch Abgleich des eindeutigen Zeitstempels des Läufers.
== Protokoll-Optimierungsanalyse
=== Hardware-optimiertes Protokolldesign für ESP32 und LoRa
Beim Entwurf von stromsparenden, hochperformanten Mesh-Netzwerken mit *ESP32*-Mikrocontrollern über *LoRa*-Modulation beeinflusst das Layout der Datenstrukturen sowohl die Übertragungszeit (Airtime) als auch die CPU-Effizienz maßgeblich. Das aktualisierte FoxMash-Protokolllayout strukturiert die Felder so um, dass ein optimales Gleichgewicht zwischen HF-Effizienz und Hardware-Ausführung erreicht wird.
==== Warum dieses Layout perfekt ist
1. *Native Wortausrichtung (Keine Performance-Verluste):* Die in ESP32-Chips verwendeten Tensilica Xtensa- und RISC-V-Architekturen sind von Natur aus *32-Bit Little-Endian*-Prozessoren. Der Zugriff auf einen 4-Byte-Integer von einer Speicheradresse, die kein Vielfaches von 4 is (_unaligned access_), zwingt die Hardware zu speicherintensiven Korrekturen durch internes Byte-Shifting. In ungünstigen Fällen oder spezifischen Compiler-Konfigurationen führt dies zu einem Hardware-Absturz (`LoadStoreAlignmentCause` Core Dump). Durch Platzierung des 32-Bit-Zeitstempels an exakt *Byte 4* (Kontrollpunkt) und *Byte 12* (Beacon) fügt sich der Integer perfekt in die natürlichen Wortgrenzen des Prozessors ein.
2. *Zero-Copy Zeiger-Casting:* Da die Felder nativ ausgerichtet sind, muss die Firmware keine Taktzyklen für `memcpy()` oder Bit-Shifting-Operatoren (`<<`, `|`) verschwenden, um Integer-Werte aus einzelnen Bytes zu rekonstruieren. Die Anwendungsschicht kann direkt aus dem LoRa-Hardware-Empfangspuffer ein Zero-Copy-Pointer-Casting durchführen.
3. *Null Airtime-Overhead:* Traditionelle Alignment-Techniken fügen künstliche Füllbytes (_Padding_, `0x00`) ein, um die Ausrichtung zu erheben, was das Paket vergrößert. In LoRa-Netzwerken erhöht jedes zusätzliche Byte direkt die Time-on-Air (ToA), was zu höherem Energieverbrauch und einer größeren Wahrscheinlichkeit von Paketkollisionen führt. Dieses optimierte Layout erreicht die perfekte Ausrichtung rein durch die *Umordnung bestehender, essenzieller Felder*, wodurch die Frame-Größe minimal bleibt.
4. *Menschlich lesbare Trennzeichen beibehalten:* Das Beibehalten des Standard-Bindestrichs (`"-"`) oder der Tilde (`"~"`) als Trennzeichen zwischen dem internationalen Rufzeichen und der SSID wahrt die Kompatibilität mit bestehenden APRS-/MeshCom-Parsern und hält rohe Hex-Logs beim seriellen Debuggen intuitiv lesbar.
=== Hochperformantes C++ Implementierungsbeispiel
Der folgende Code-Ausschnitt zeigt, wie dieses speicheroptimierte Layout in einer ESP32-Anwendung unter Verwendung von Standard-Arduino- oder ESP-IDF-LoRa-Bibliotheken genutzt werden kann.
#rect(width: 100%, stroke: 0.5pt + luma(150), fill: luma(245), radius: 4pt)[
```cpp
#include <Arduino.h>
// Sicherstellen, dass der Compiler kein strukturelles Padding in den Structs hinzufügt
#pragma pack(push, 1)
struct BeaconPacket {
char dti; // Offset 0 (0x7B '{')
char callsign[6]; // Offset 1 (z. B. "OE3TKT")
char separator; // Offset 7 (z. B. '-')
char ssid[2]; // Offset 8 (z. B. "03")
uint8_t battery; // Offset 10 (0-100%)
uint8_t net_flags; // Offset 11 (Routing-Steuerung / Padding)
int32_t timestamp_ms; // Offset 12 -> PERFEKT AUF 4-BYTE AUSGERICHTET!
uint8_t neighbor_block[]; // Offset 16+ (Dynamische Mesh-Nutzlast)
};
struct CheckpointPacket {
char dti; // Offset 0 (0x28 '(')
uint16_t own_ssid; // Offset 1
uint8_t routing_flags; // Offset 3 (Ziel- / Steuerungs-Flags)
int32_t timestamp_ms; // Offset 4 -> PERFEKT AUF 4-BYTE AUSGERICHTET!
uint16_t runner_id; // Offset 8
uint8_t hop_count; // Offset 10
};
#pragma pack(pop)
// Beispiel-Parserfunktion zur Verarbeitung eingehender roher LoRa-Bytes
void processIncomingLoRa(uint8_t* rxBuffer, size_t packetLength) {
if (packetLength < 1) return;
// Data Type Identifier (DTI) prüfen
char dti = rxBuffer[0];
if (dti == '(') {
// Zero-copy Direktzugriff über Zeiger-Cast
CheckpointPacket* cp = (CheckpointPacket*)rxBuffer;
// Schneller nativer Zugriff direkt aus dem Speicher
int32_t time = cp->timestamp_ms;
uint16_t runner = cp->runner_id;
Serial.printf("Läufer %d erfasst. Lokaler Zeitstempel: %d ms\n", runner, time);
}
else if (dti == '{') {
BeaconPacket* beacon = (BeaconPacket*)rxBuffer;
// Schneller nativer Zugriff direkt aus dem Speicher
int32_t time = beacon->timestamp_ms;
Serial.printf("Beacon empfangen. Netzwerk-Zeitstempel: %d ms\n", time);
}
}
```
]
// ==========================================
// GESTALTETER FOOTER (Name & Typst-Hinweis)
// ==========================================
#v(3em)
#line(length: 100%, stroke: 0.5pt + luma(200))
#v(0.5em)
#align(center)[
#text(size: 9pt, fill: luma(100))[
Dokument gepflegt und kompiliert von *[OE3TKT, Tom]* \
#v(2pt)
#text(size: 8pt, style: "italic")[
Wunderschön gesetzt mit #link("https://typst.app")[Typst] #box(width: 8pt, image(bytes("<svg viewBox='0 0 24 24' width='8' height='8'><path fill='#ff5500' d='M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z'/></svg>"), format: "svg"))
]
]
]
+50
Datei anzeigen
@@ -0,0 +1,50 @@
# Protocol MeshCom 4.0
## Stream - Example
```
:~1~9~SP3TUT-1,OE1XAR-12,OE3TKT-12>*:z okolic Pi~~y kto~~ tu jest?~~~~~(~a~
S
Hex: 3A BB 31 F3 39 93 53 50 33 54 55 54 2D 31 2C 4F 45 31 58 41 52 2D 31 32 2C 4F 45 33 54 4B 54 2D 31 32 3E 2A 3A 7A 20 6F 6B 6F 6C 69 63 20 50 69 C5 82 79 20 6B 74 6F C5 9B 20 74 75 20 6A 65 73 74 3F 00 03 88 16 C5 28 8C 61 7E
| =========== | ===================================================================================
| | | |
| | | > APRS Address Header (Source,Path, Destination) - "SP3TUT-1": Source Callsign of the transmitting amateur radio station from Poland, using SSID 1.
| | | > -
| | | >
| | > Flags & Hops [7]
B02 | > Message Identifier "~1~9" 0xBB31F339 - The unique 32-bit Message ID transmitted in Little-Endian format (least significant byte first).
| It is used by the mesh network to track packets and prevent duplication loops.
B01 > APRS Data Type Identifier ":" 0x3A - APRS Text Message, used for person-to-person Direct Messages (DMs), group messages, or general announcements.
Following this character, the parser expects a destination callsign, a tracking number, and the message content.
"@" 0x40 - APRS Position with Timestamp, used for automated position reports that include a precise time (such as weather node beacons or sttic trackers).
It tells the terminal to parse fixed-length chunks for latitude, longitude, and elevation.
"!" 0x21 - APRS Position without Timestamp, used for real-time mobile tracking (e.g., a node) with an integrated GPS sending live movement updates).
It skips the time fields to reduce airtime over LoRa.
";" 0x3B - APRS Object Report, used to map an "Object" rather than a station.
For example, a node can broadcast the location of a local hamfest, emergency shelter, or repeater.
It is followed by a fixed 9-character object name.
">" 0x3E - APRS Status Report: Used to transmit a station's current operating state or hardware information rather than geographic coordinates.
"$" 0x24 - APRS Raw NMEA Sentence: A legacy data type that allows raw GPS string passthrough (like $GPRMC or $GPGGA) directly from a serial GPS mouse.
Other DTI(Data Type Identifiers)
"=" 0x3D - Position with compressed APRS capability, a standard position beacon, but formatted in Base-91 compression to save airtime over busy RF networks.
"/" 0x2F - Mic-E Format Data, Microphone Encoder telemetry. Used by commercial radios (like Yaesu or Kenwood) to embed a GPS burst into a standard voice transmission PTT release.
"`" 0x60 - Mic-E Old Format, Legacy Microphone Encoder burst, rarely used by modern trackers but still supported for backward compatibility.
"_" 0x5F - Positionless Weather Report, used by automated weather stations (like fine-offset units or CWOP stations) to transmit pure wind speed, temperature, and barometric values without re-sending latitude/longitude.
")" 0x29 - Item Report, identical to a map Object (;), but used for temporary or small Points of Interest (POIs) with a much shorter name (1–9 characters) and no timestamp.
"<" 0x3C - Station Capabilities, sent by intelligent digipeaters and internet gateways (iGates) to report what features they support (e.g., whether they handle messaging or special path routing).
"T" 0x54 - Raw Telemetry Data, used for automated analog data streams. Frequently found on high-altitude balloon payloads or weather satellites transmitting five channels of changing mathematical numbers.
"S" 0x53 - Telemetry Structural Definition, defines the scale and offsets of raw numbers sent in the T packet (so client maps like aprs.fi know if a raw value of 142 means volts, millibars, or degrees).
"?" 0x3F - Query/Poll Command, a packet explicitly asking another station or a server to respond. For instance, sending ?APRSP asks surrounding nodes for their current paths.
"{" 0x7B - User-Defined / Proprietary, reserved entirely for experimental protocols or internal applications. MeshCom and similar experimental LoRa platforms utilize these spaces to run custom network setups.
```
## Callsign
```
Every callsign is build with [Personal Amateur Radio Callsign]-[SSID], SSID is a number between 1 and 99.
• -0 (or no suffix): Standard home station / fixed station (usually avoided by MeshCom for mobile operation).
• -1 to -3: Additional stations with potential special functions (e.g., digipeaters).
• -7: Portable stations, handheld radios (e.g., a Heltec or T-Echo node carried in a pocket).
• -9: Mobile stations (vehicles).
• -10: Gateways, digipeaters, or Winlink nodes.
• -11: Pure client stations without gateway functionality.
• -12: Gateway stations including mesh routing functionality (very common for permanently running master nodes with an internet/Wi-Fi connection).
• -13: Weather stations or telemetry sensors.
```
+9 -1
Datei anzeigen
@@ -23,8 +23,13 @@ build_flags =
-D CCACHE_ENABLE=1
; Treats ESP-IDF libc extensions headers as system headers to suppress external warnings
; -isystem /home/tom/.platformio/packages/framework-espidf/components/esp_libc/platform_include
-Wno-include-next
; --- STRICT MODE FOR PROJECT APPLICATION CODE ONLY ---
#-DGIT_HASH=\"\"${sysenv.GIT_HASH}\"\"
!echo -DGIT_HASH=\\\"$(git rev-parse --short HEAD)\\\"
!echo -DGIT_TAG=\\\"$(git describe --tags --always)\\\"
; These flags are strictly applied ONLY to C++ files inside the src/ directory.
build_src_flags =
; Enables all standard compiler warning flags
@@ -39,5 +44,8 @@ build_src_flags =
-Wconversion
; Warns if a local variable shadows another variable in scope
-Wshadow
-Wno-include-next
; Promotes all warnings to hard compilation errors to guarantee clean code
;-Werror
;-D MATH_COMPARE=1
Datei-Diff unterdrückt, da er zu groß ist. Diff laden
+27 -11
Datei anzeigen
@@ -19,6 +19,7 @@
#include "esp_log.h"
#include "config.hpp"
#include "lora.hpp"
#include "display.hpp"
static const char* TAG = "CLI";
static i2c* i2c_bus = nullptr;
@@ -91,9 +92,11 @@ void process_command(const char* cmd) {
printf(" freq <hz> - Set dynamic radio frequency (433 / 868 MHz bounds)\r\n");
printf(" bw <62|125|250|500>- Set dynamic LoRa bandwidth in kHz\r\n");
printf(" i2c - Scan the configured I2C bus infrastructure\r\n");
printf(" test-display - Run a comprehensive visual test suite\r\n");
}
else if (strcmp(cmd, "version") == 0) {
printf("%s System - Running Version: %s\r\n", Config::PROJECT_TITLE, Config::PROJECT_VERSION);
printf("%s System - Running Version: %s Tag: " GIT_TAG "\r\n", Config::PROJECT_TITLE, Config::PROJECT_VERSION);
printf("Build Date: " __DATE__ ", Git-Hash: " GIT_HASH "\r\n");
}
else if (strcmp(cmd, "status") == 0) {
printf("--- %s Status ---\r\n", Config::PROJECT_TITLE);
@@ -101,6 +104,23 @@ void process_command(const char* cmd) {
printf("Mute Timer Period : %lu seconds\r\n", (unsigned long)current_mute_time_sec);
printf("Target CPU Board : ESP32\r\n");
printf("ESP-IDF Version : %s\r\n", esp_get_idf_version());
if (lora_system != nullptr) {
// atomar, thread-safe copy of telemtry data (stack allocation)
LoraStateSnapshot snapshot = lora_system->getSnapshot();
printf("LoRa Hardware : %s\r\n", snapshot.isInitialized ? "INITIALIZED" : "FAILED / UNINITIALIZED");
printf("LoRa Transceiver : Mode [%s]\r\n", snapshot.currentModeStr);
printf("LoRa Frequency : %lu Hz\r\n", (unsigned long)snapshot.frequencyHz);
printf("LoRa Bandwidth : %lu kHz\r\n", (unsigned long)snapshot.bandwidthKhz);
printf("LoRa SpreadingFact: SF%u\r\n", snapshot.spreadingFactor);
printf("LoRa Coding Rate : 4/%u\r\n", snapshot.codingRate);
printf("Packets Received : %lu\r\n", (unsigned long)snapshot.totalPacketsReceived);
printf("Packets Transmit : %lu\r\n", (unsigned long)snapshot.totalPacketsTransmitted);
printf("History In Memory : %zu Packets\r\n", snapshot.currentHistoryCount);
} else {
printf("LoRa Snapshot : Error (LoraController Handle uninitialized)\r\n");
}
}
else if (strncmp(cmd, "mute ", 5) == 0) {
int parsed_time = atoi(cmd + 5);
@@ -163,16 +183,7 @@ void process_command(const char* cmd) {
if (valid_433 || valid_868) {
if (lora_system != nullptr) {
// SX1276 formula: Frf = Ffreq * 2^19 / 32000000
// Using double to prevent truncation during division before bit masking
double fraction = (static_cast<double>(target_freq_hz) * 524288.0) / 32000000.0;
uint32_t frf = static_cast<uint32_t>(fraction);
uint8_t msb = static_cast<uint8_t>((frf >> 16) & 0xFFU);
uint8_t mid = static_cast<uint8_t>((frf >> 8) & 0xFFU);
uint8_t lsb = static_cast<uint8_t>(frf & 0xFFU);
lora_system->setFrequency(msb, mid, lsb);
lora_system->setFrequency(target_freq_hz);
printf("Success: Frequency shifted to %lu Hz.\r\n", (unsigned long)target_freq_hz);
} else {
printf("Error: LoraController handle is uninitialized.\r\n");
@@ -229,6 +240,11 @@ void process_command(const char* cmd) {
printf("Error: I2C Bus not initialized in CLI!\r\n");
}
}
else if (strcmp(cmd, "test-display") == 0) {
printf("[CLI] Triggering asynchronous OLED font verification...\n");
run_display_font_test(oled_display);
printf("[CLI] Test completed.\n");
}
else {
printf("Unknown command: '%s'. Type 'help' for options.\r\n", cmd);
}
+36 -8
Datei anzeigen
@@ -22,7 +22,7 @@
namespace Config {
// Compile-time constants with strict type enforcement
constexpr const char* PROJECT_TITLE = "FoxMesh";
constexpr const char* PROJECT_VERSION = "1.0.3";
constexpr const char* PROJECT_VERSION = "0.1";
constexpr const char* PROJECT_AUTHORS = "Tom OE3TKT";
// CLI Mute Configuration
@@ -45,16 +45,44 @@ namespace Config {
constexpr gpio_num_t LORA_CS_PIN = GPIO_NUM_18;
constexpr gpio_num_t LORA_RST_PIN = GPIO_NUM_23; // Adjusted configuration for T3_V1.6 hardware revision
constexpr gpio_num_t LORA_IRQ_PIN = GPIO_NUM_26;
constexpr gpio_num_t LORA_DIO1_PIN = GPIO_NUM_33; // Important: PLL lock & Timeout in RX mode
// Semtech SX1276 LoRa Configuration Parameters
// 433.175 MHz is calculated by: 433175000 / (32000000 / 2^19) = 7097139.2 = 0x6c4b33 // wrong: 0x06C4CD
constexpr uint8_t LORA_FRF_MSB = 0x6C;
constexpr uint8_t LORA_FRF_MID = 0x4B;
constexpr uint8_t LORA_FRF_LSB = 0x33;
constexpr uint32_t LORA_DEFAULT_FREQ_HZ = 433175000;
constexpr uint8_t LORA_FRF_MSB = 0x6C; // @TODO: will be removed in v1.1
constexpr uint8_t LORA_FRF_MID = 0x4B; // @TODO: will be removed in v1.1
constexpr uint8_t LORA_FRF_LSB = 0x33; // @TODO: will be removed in v1.1
constexpr uint32_t LORA_DEFAULT_BW_KHZ = 250;
constexpr uint8_t LORA_DEFAULT_SF = 11;
constexpr uint8_t LORA_DEFAULT_CR = 6; // CR 4/6 -> Denominator is 6
// Modem Config 1: BW 250kHz (0x80), CR 4/6 (0x04), Explicit Header (0x00) -> 0x84
constexpr uint8_t LORA_MODEM_CONFIG_1 = 0x84;
// Predefined LoRa Sync Word options for hardware correlation filtering, see docs/lora/device-settings.md
constexpr uint8_t LORA_SYNC_WORD_PUBLIC_NETWORK_MODE = 0x34; // MeshCom (ÖVSV standard for amateur radio mesh),
// The Things Network, Helium
constexpr uint8_t LORA_SYNC_WORD_TACTICAL_NETWORK_MODE = 0x24; // Meshtastic (Civilian off-grid tactical mesh network)
constexpr uint8_t LORA_SYNC_WORD_PRIVATE_NETWORK_MODE = 0x12; // Semtech Default (Factory default / Private point-to-point)
constexpr uint8_t LORA_SYNC_WORD_ISOLATED_ARRAY_A = 0x55; // Custom / Experimental individual networks (highly isolated)
constexpr uint8_t LORA_SYNC_WORD_ISOLATED_ARRAY_B = 0xAA; // Custom / Experimental individual networks (highly isolated)
// Modem Config 2: SF11 (0xB0), TX Continuous off (0x00), CRC On (0x04) -> 0xB4
constexpr uint8_t LORA_MODEM_CONFIG_2 = 0xB4;
// Active hardware synchronization configuration selector
constexpr uint8_t LORA_SYNC_WORD = LORA_SYNC_WORD_PUBLIC_NETWORK_MODE; // Selected default synchronization profile
// Expand hardware preamble length to 12 symbols for robust network correlation (MeshCom value)
constexpr uint8_t LORA_PREAMBLE_LENGTH_MSB = 0x00;
constexpr uint8_t LORA_PREAMBLE_LENGTH_LSB = 0x0C; // Semtech Factor Default is 0x08, MeshCom with 0x0C, i.e. 12 symbols
// --- DYNAMIC COMPILE-TIME BIT-SHIFTING EXPRESSIONS ---
// Helper to map bandwidth kHz to Semtech Register Bits (7-4 of RegModemConfig1)
constexpr uint8_t LORA_BW_REG_VAL = (LORA_DEFAULT_BW_KHZ == 62) ? 0x06 :
(LORA_DEFAULT_BW_KHZ == 125) ? 0x07 :
(LORA_DEFAULT_BW_KHZ == 250) ? 0x08 :
(LORA_DEFAULT_BW_KHZ == 500) ? 0x09 : 0x08;
// RegModemConfig1 (0x1D): Bandwidth (Bits 7-4), Coding Rate (Bits 3-1), ImplicitHeader (Bit 0 = 0)
constexpr uint8_t LORA_MODEM_CONFIG_1 = static_cast<uint8_t>((LORA_BW_REG_VAL << 4) | ((LORA_DEFAULT_CR - 4) << 1));
// RegModemConfig2 (0x1E): Spreading Factor (Bits 7-4), TxContinuous (Bit 3 = 0), RxPayloadCrcOn (Bit 2 = 1)
constexpr uint8_t LORA_MODEM_CONFIG_2 = static_cast<uint8_t>((LORA_DEFAULT_SF << 4) | 0x04U);
}
+243 -60
Datei anzeigen
@@ -116,26 +116,137 @@ static const uint8_t font_5x7[][5] = {
{0x00, 0x00, 0x77, 0x00, 0x00}, // 124 |
{0x00, 0x41, 0x36, 0x08, 0x00}, // 125 }
{0x08, 0x08, 0x2a, 0x1c, 0x08}, // 126 ~
// German Umlaute mappings
// {0x7d, 0x11, 0x11, 0x11, 0x7d}, // 127 Ä
// {0x3c, 0x41, 0x41, 0x41, 0x3c}, // 128 Ö
// {0x3d, 0x40, 0x40, 0x40, 0x3d}, // 129 Ü
// {0x20, 0x55, 0x54, 0x54, 0x78}, // 130 ä
// {0x38, 0x54, 0x54, 0x54, 0x38}, // 131 ö
// {0x3c, 0x40, 0x40, 0x20, 0x7c}, // 132 ü
// {0x7f, 0x49, 0x49, 0x36, 0x20} // 133 ß
// new German Umlaute mapping (5x7/5x8 Font Matrix)
{0,0,0,0,0}, //127 not allowed
{0x7d, 0x13, 0x13, 0x13, 0x7d}, // 128 Ä
{0x3c, 0x43, 0x43, 0x43, 0x3c}, // 129 Ö
{0x3d, 0x42, 0x42, 0x42, 0x3d}, // 130 Ü
{0x20, 0x55, 0x54, 0x54, 0x78}, // 131 ä
{0x38, 0x55, 0x55, 0x55, 0x38}, // 132 ö
{0x3c, 0x42, 0x42, 0x22, 0x7c}, // 133 ü
{0x7f, 0x49, 0x49, 0x36, 0x00}, // 134 ß (Letzte Spalte auf 0x00 bereinigt)
// {0x3c, 0x42, 0x51, 0x4a, 0x34} // 134 © Copyright
// {0x3c, 0x42, 0x4d, 0x4d, 0x32} // 135
{0x3c, 0x42, 0x95, 0x95, 0x52}
{0x00, 0x00, 0x00, 0x00, 0x00}, // 127 DEL (Control character)
// --- UTF-8 0xC2 RANGE (ISO-8859-1 Indizes 128 to 191) ---
{0x00, 0x00, 0x00, 0x00, 0x00}, // 128 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 129 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 130 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 131 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 132 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 133 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 134 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 135 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 136 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 137 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 138 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 139 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 140 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 141 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 142 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 143 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 144 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 145 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 146 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 147 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 148 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 149 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 150 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 151 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 152 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 153 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 154 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 155 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 156 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 157 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 158 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 159 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 160 Non-breaking space
{0x00, 0x00, 0x5f, 0x00, 0x00}, // 161 Inverted exclamation mark (¡)
{0x24, 0x2a, 0x7f, 0x2a, 0x12}, // 162 Cent sign (¢)
{0x10, 0x7e, 0x24, 0x44, 0x28}, // 163 Pound sign (£)
{0x6c, 0x28, 0x28, 0x28, 0x6c}, // 164 Currency sign (¤)
{0x08, 0x3e, 0x49, 0x3e, 0x08}, // 165 Yen sign (¥)
{0x00, 0x00, 0x5f, 0x00, 0x00}, // 166 Broken bar (¦) (Alternativ durchgezogen)
{0x22, 0x49, 0x49, 0x49, 0x22}, // 167 Section sign / Paragraph (§)
{0x00, 0x03, 0x00, 0x03, 0x00}, // 168 Diaeresis (¨)
{0x3e, 0x41, 0x5d, 0x49, 0x3e}, // 169 Copyright sign (©)
{0x44, 0x28, 0x3e, 0x44, 0x3e}, // 170 Feminine ordinal indicator (ª)
{0x08, 0x14, 0x22, 0x00, 0x00}, // 171 Left-pointing double-angle quotation mark («)
{0x04, 0x04, 0x04, 0x04, 0x07}, // 172 Not sign (¬)
{0x04, 0x04, 0x04, 0x04, 0x04}, // 173 Soft hyphen
{0x3e, 0x41, 0x5d, 0x55, 0x3e}, // 174 Registered sign (®)
{0x01, 0x01, 0x01, 0x01, 0x01}, // 175 Macron (¯)
{0x00, 0x06, 0x09, 0x06, 0x00}, // 176 Degree sign (°)
{0x08, 0x08, 0x3e, 0x08, 0x08}, // 177 Plus-minus sign (±) (Unterstrich wird dynamisch generiert/Zweit-Byte)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 178 Superscript two (²) -> Oft custom generiert
{0x00, 0x00, 0x00, 0x00, 0x00}, // 179 Superscript three (³) -> Oft custom generiert
{0x00, 0x01, 0x02, 0x04, 0x00}, // 180 Acute accent (´)
{0x3c, 0x0a, 0x0a, 0x0a, 0x02}, // 181 Micro sign (µ)
{0x06, 0x7f, 0x4b, 0x4b, 0x3a}, // 182 Pilcrow sign / Paragraph sign (¶)
{0x00, 0x00, 0x0c, 0x0c, 0x00}, // 183 Middle dot (·)
{0x10, 0x20, 0x00, 0x00, 0x00}, // 184 Cedilla (¸)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 185 Superscript one (¹)
{0x38, 0x44, 0x44, 0x44, 0x38}, // 186 Masculine ordinal indicator (º)
{0x00, 0x00, 0x22, 0x14, 0x08}, // 187 Right-pointing double-angle quotation mark (»)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 188 Vulgar fraction one quarter (¼)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 189 Vulgar fraction one half (½)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 190 Vulgar fraction three quarters (¾)
{0x30, 0x48, 0x4a, 0x40, 0x20}, // 191 Inverted question mark (¿)
// --- UTF-8 0xC3 RANGE (ISO-8859-1 Indizes 192 to 255) ---
{0x7c, 0x13, 0x11, 0x10, 0x7c}, // 192 Capital A with grave (À)
{0x7c, 0x10, 0x11, 0x13, 0x7c}, // 193 Capital A with acute (Á)
{0x7c, 0x12, 0x15, 0x12, 0x7c}, // 194 Capital A with circumflex (Â)
{0x7c, 0x12, 0x15, 0x12, 0x7c}, // 195 Capital A with tilde (Ã)
{0x7c, 0x11, 0x11, 0x11, 0x7c}, // 196 Capital A with umlaut (Ä)
{0x7c, 0x11, 0x11, 0x11, 0x7c}, // 197 Capital A with ring above (Å)
{0x7f, 0x49, 0x7f, 0x41, 0x63}, // 198 Capital AE ligature (Æ)
{0x3e, 0x41, 0x41, 0x22, 0x18}, // 199 Capital C with cedilla (Ç)
{0x7f, 0x4a, 0x4a, 0x42, 0x42}, // 200 Capital E with grave (È)
{0x7f, 0x42, 0x4a, 0x4a, 0x42}, // 201 Capital E with acute (É)
{0x7f, 0x43, 0x4d, 0x43, 0x41}, // 202 Capital E with circumflex (Ê)
{0x7f, 0x41, 0x41, 0x41, 0x41}, // 203 Capital E with umlaut (Ë)
{0x00, 0x42, 0x7d, 0x40, 0x00}, // 204 Capital I with grave (Ì)
{0x00, 0x40, 0x7d, 0x42, 0x00}, // 205 Capital I with acute (Í)
{0x00, 0x42, 0x7d, 0x42, 0x00}, // 206 Capital I with circumflex (Î)
{0x00, 0x41, 0x7d, 0x41, 0x00}, // 207 Capital I with umlaut (Ï)
{0x7f, 0x41, 0x41, 0x22, 0x1c}, // 208 Capital Eth (Ð)
{0x7f, 0x09, 0x05, 0x03, 0x7f}, // 209 Capital N with tilde (Ñ)
{0x3e, 0x43, 0x41, 0x41, 0x3e}, // 210 Capital O with grave (Ò)
{0x3e, 0x41, 0x41, 0x43, 0x3e}, // 211 Capital O with acute (Ó)
{0x3e, 0x42, 0x45, 0x42, 0x3e}, // 212 Capital O with circumflex (Ô)
{0x3e, 0x42, 0x45, 0x42, 0x3e}, // 213 Capital O with tilde (Õ)
{0x3e, 0x41, 0x41, 0x41, 0x3e}, // 214 Capital O with umlaut (Ö)
{0x08, 0x14, 0x22, 0x14, 0x08}, // 215 Multiplication sign (×)
{0x3e, 0x61, 0x51, 0x4b, 0x3e}, // 216 Capital O with stroke (Ø)
{0x3f, 0x42, 0x40, 0x40, 0x3f}, // 217 Capital U with grave (Ù)
{0x3f, 0x40, 0x40, 0x42, 0x3f}, // 218 Capital U with acute (Ú)
{0x3f, 0x42, 0x44, 0x42, 0x3f}, // 219 Capital U with circumflex (Û)
{0x3f, 0x41, 0x40, 0x41, 0x3f}, // 220 Capital U with umlaut (Ü)
{0x03, 0x04, 0x78, 0x04, 0x03}, // 221 Capital Y with acute (Ý)
{0x7f, 0x49, 0x49, 0x36, 0x00}, // 222 Capital Thorn (Þ)
{0x3e, 0x49, 0x49, 0x49, 0x32}, // 223 Small sharp s / Eszett (ß)
{0x20, 0x55, 0x55, 0x54, 0x78}, // 224 Small a with grave (à)
{0x20, 0x54, 0x55, 0x55, 0x78}, // 225 Small a with acute (á)
{0x20, 0x56, 0x55, 0x56, 0x78}, // 226 Small a with circumflex (â)
{0x20, 0x56, 0x55, 0x56, 0x78}, // 227 Small a with tilde (ã)
{0x20, 0x55, 0x54, 0x55, 0x78}, // 228 Small a with umlaut (ä)
{0x20, 0x55, 0x54, 0x55, 0x78}, // 229 Small a with ring above (å)
{0x24, 0x54, 0x78, 0x54, 0x5c}, // 230 Small ae ligature (æ)
{0x38, 0x44, 0x44, 0x28, 0x10}, // 231 Small c with cedilla (ç)
{0x38, 0x55, 0x55, 0x54, 0x18}, // 232 Small e with grave (è)
{0x38, 0x54, 0x55, 0x55, 0x18}, // 233 Small e with acute (é)
{0x38, 0x56, 0x55, 0x56, 0x18}, // 234 Small e with circumflex (ê)
{0x38, 0x55, 0x54, 0x55, 0x18}, // 235 Small e with umlaut (ë)
{0x00, 0x45, 0x7c, 0x40, 0x00}, // 236 Small i with grave (ì)
{0x00, 0x40, 0x7c, 0x45, 0x00}, // 237 Small i with acute (í)
{0x00, 0x46, 0x7c, 0x46, 0x00}, // 238 Small i with circumflex (î)
{0x00, 0x45, 0x7c, 0x45, 0x00}, // 239 Small i with umlaut (ï)
{0x38, 0x44, 0x44, 0x48, 0x7f}, // 240 Small eth (ð)
{0x7c, 0x05, 0x05, 0x04, 0x78}, // 241 Small n with tilde (ñ)
{0x38, 0x45, 0x45, 0x44, 0x38}, // 242 Small o with grave (ò)
{0x38, 0x44, 0x45, 0x45, 0x38}, // 243 Small o with acute (ó)
{0x38, 0x46, 0x45, 0x46, 0x38}, // 244 Small o with circumflex (ô)
{0x38, 0x46, 0x45, 0x46, 0x38}, // 245 Small o with tilde (õ)
{0x38, 0x45, 0x44, 0x45, 0x38}, // 246 Small o with umlaut (ö)
{0x08, 0x08, 0x2a, 0x08, 0x08}, // 247 Division sign (÷)
{0x38, 0x64, 0x54, 0x4c, 0x38}, // 248 Small o with stroke (ø)
{0x3c, 0x41, 0x40, 0x20, 0x7c}, // 249 Small u with grave (ù)
{0x3c, 0x40, 0x40, 0x21, 0x7c}, // 250 Small u with acute (ú)
{0x3c, 0x42, 0x40, 0x22, 0x7c}, // 251 Small u with circumflex (û)
{0x3c, 0x41, 0x40, 0x21, 0x7c}, // 252 Small u with umlaut (ü)
{0x4c, 0x30, 0x30, 0x30, 0x7c}, // 253 Small y with acute (ý)
{0x7f, 0x24, 0x24, 0x18, 0x00}, // 254 Small thorn (þ)
{0x4c, 0x31, 0x30, 0x31, 0x7c} // 255 Small y with umlaut (ÿ)
};
/**
@@ -264,14 +375,16 @@ void DisplayController::startTask(uint32_t priority) {
}
/**
* @brief Safe cross-core interface dispatching a text block onto the internal processing pipeline buffer.
* @param line Selected hardware page index mapping from 0 to 7 (Bit 7 disables Auto-Clear).
* @param text Pointer to a standard null-terminated sequence of tracking information.
* @param line Selected hardware page index mapping from 0 to 7.
* @param startX Starting column coordinate index on the horizontal axis (0 to 127).
*/
void DisplayController::printMessage(uint8_t line, const char* text) {
void DisplayController::printMessage(uint8_t line, const char* text, uint8_t startX) {
if (m_msgQueue == nullptr || text == nullptr) return;
DisplayMessage msg = {};
msg.targetLine = line;
msg.startX = (startX > 127) ? 127 : startX; // Clamp to maximum physical display width
// Thread-safe copy buffer sanitization sequence
strncpy(msg.content, text, sizeof(msg.content) - 1);
@@ -311,56 +424,73 @@ void DisplayController::run() {
while (true) {
// Asynchronous structural block yield: consumes zero CPU until a new message package arrives
if (xQueueReceive(m_msgQueue, &incoming, portMAX_DELAY) == pdTRUE) {
if (incoming.targetLine > 7) incoming.targetLine = 7;
// Positioning the internal hardware pointer inside the target page boundary
sendCommand(static_cast<uint8_t>(0xB0 + incoming.targetLine)); // Set page address boundary
sendCommand(0x00); // Set lower column pointer
sendCommand(0x10); // Set higher column pointer
uint8_t totalBytesWritten = 0;
// Extract the Auto-Clear suppression flag from the MSB (Bit 7) and mask the line index
const bool disableAutoClear = incoming.targetLine & 0x80;
const uint8_t cleanLine = incoming.targetLine & 0x07; // Native 0-7 clamp using bitmask
// Basic built-in 5x7 ASCII micro character font generation layer
// This prevents adding bloating third party render dependencies into your tight stack space
size_t textLength = strlen(incoming.content);
// POSITIONING THE HARDWARE POINTER
sendCommand(static_cast<uint8_t>(0xB0 + cleanLine)); // Set page address boundary
// Set the horizontal column start pointer (startX) elegantly without magic 0x00 OR-mask
sendCommand(static_cast<uint8_t>(incoming.startX & 0x0F));
sendCommand(static_cast<uint8_t>(0x10 | (incoming.startX >> 4)));
// Track absolute pixel position starting from the custom X coordinate
uint8_t totalBytesWritten = incoming.startX;
const size_t textLength = strlen(incoming.content);
for (size_t i = 0; i < textLength; i++) {
uint8_t c_primary = static_cast<uint8_t>(incoming.content[i]);
const uint8_t c_primary = static_cast<uint8_t>(incoming.content[i]);
size_t fontIndex = 0;
bool validCharacter = false;
const size_t current_char_idx = i; // Store original index for connection logic
// Basic UTF-8 multi-byte decoder for native German Umlaute translation
if (c_primary == 0xC3 && (i + 1) < textLength) {
uint8_t c_secondary = static_cast<uint8_t>(incoming.content[i + 1]);
i++; // Advance the pointer to consume the compound UTF-8 token pair
// Multi-byte UTF-8 decoder targeting the complete ISO-8859-1 (Latin-1) spectrum
if ((c_primary == 0xC3 || c_primary == 0xC2) && (i + 1) < textLength) {
const uint8_t c_secondary = static_cast<uint8_t>(incoming.content[++i]);
if (c_secondary == 0x84) { fontIndex = 96; validCharacter = true; } // Ä
if (c_secondary == 0x96) { fontIndex = 97; validCharacter = true; } // Ö
if (c_secondary == 0x9C) { fontIndex = 98; validCharacter = true; } // Ü
if (c_secondary == 0xA4) { fontIndex = 99; validCharacter = true; } // ä
if (c_secondary == 0xB6) { fontIndex = 100; validCharacter = true; } // ö
if (c_secondary == 0xBC) { fontIndex = 101; validCharacter = true; } // ü
// Mathematical reconstruction of the raw Latin-1 byte position
// 0xC3 sequence maps to 0xC0-0xFF (c_secondary + 0x40)
// 0xC2 sequence maps directly to 0x80-0xBF
uint8_t iso_char = (c_primary == 0xC3) ? (c_secondary + 0x40) : c_secondary;
// Subtract table offset since font array starts at index 32 (Space)
fontIndex = static_cast<size_t>(iso_char - 32);
validCharacter = true;
}
else if (c_primary == 0xC2 && (i + 1) < textLength) {
uint8_t c_secondary = static_cast<uint8_t>(incoming.content[i + 1]);
i++;
if (c_secondary == 0x9F) { fontIndex = 102; validCharacter = true; } // ß
if (c_secondary == 0xA9) { fontIndex = 103; validCharacter = true; } // ©
}
else if ((c_primary >= 32 && c_primary <= 126) || (c_primary > 127 && c_primary <= 135)) {
// Handle standard printable 7-bit ASCII characters
else if (c_primary >= 32 && c_primary <= 126) {
fontIndex = static_cast<size_t>(c_primary - 32);
validCharacter = true;
}
if (validCharacter && totalBytesWritten < 122) {
uint8_t txBuffer[7] = {0};
uint8_t txBuffer[7];
// copy all 5 font columns in a single, hardware-accelerated burst
txBuffer[0] = 0x40; // Data identifier
// txBuffer[1] = font_5x7[fontIndex][0];
// txBuffer[2] = font_5x7[fontIndex][1];
// txBuffer[3] = font_5x7[fontIndex][2];
// txBuffer[4] = font_5x7[fontIndex][3];
// txBuffer[5] = font_5x7[fontIndex][4];
std::memcpy(&txBuffer[1], font_5x7[fontIndex], 5);
txBuffer[1] = font_5x7[fontIndex][0];
txBuffer[2] = font_5x7[fontIndex][1];
txBuffer[3] = font_5x7[fontIndex][2];
txBuffer[4] = font_5x7[fontIndex][3];
txBuffer[5] = font_5x7[fontIndex][4];
txBuffer[6] = 0x00; // 1-pixel spacing between characters
// Check if current character is a potential line segment
if (c_primary == '-' || c_primary == '_' || c_primary == '=') {
// Look behind: Direct boolean evaluation in a single line
bool connect = (current_char_idx > 0 && incoming.content[current_char_idx - 1] == static_cast<char>(c_primary));
// Look ahead: Only checks if the look-behind didn't already establish a connection
if (!connect && (i + 1) < textLength && incoming.content[i + 1] == static_cast<char>(c_primary)) {
connect = true;
}
// Seamlessly bridge the 1-pixel gap if part of a multi-character line sequence
txBuffer[6] = connect ? txBuffer[5] : 0x00;
} else {
txBuffer[6] = 0x00; // Standard 1-pixel spacing for regular text elements
}
if (m_devHandle != nullptr) {
i2c_master_transmit(m_devHandle, txBuffer, sizeof(txBuffer), 50);
@@ -369,14 +499,13 @@ void DisplayController::run() {
}
}
// AUTO-CLEAR: Fill the remaining horizontal page width with blank pixels to flush out trailing leftovers
if (totalBytesWritten < 128) {
uint8_t remainingBytes = static_cast<uint8_t>(128 - totalBytesWritten);
// AUTO-CLEAR: Fill the remaining horizontal page width with blank pixels only if flag is NOT active
if (!disableAutoClear && totalBytesWritten < 128) {
const uint8_t remainingBytes = static_cast<uint8_t>(128 - totalBytesWritten);
// Allocate a burst buffer with dynamic sizing plus control byte room
uint8_t flushBuffer[129] = {0};
flushBuffer[0] = 0x40; // Data mode identifier
if (m_devHandle != nullptr) {
// Send out the precise amount of zeros required to wipe the right margin clean
i2c_master_transmit(m_devHandle, flushBuffer, static_cast<size_t>(remainingBytes + 1), 50);
@@ -385,3 +514,57 @@ void DisplayController::run() {
}
}
}
/**
* @brief Run a comprehensive visual test suite and clear the screen after 5 seconds.
* @param display Pointer to the active DisplayController instance.
*/
void run_display_font_test(DisplayController* display) {
if (display == nullptr) return;
// Test 1: Standard printable ASCII characters (Lines 0 and 1)
display->printMessage(0, " ",0); // clear line
display->printMessage(0, "=== FONT TEST ===", 12);
display->printMessage(1, "ASCII: abcXYZ 123!?", 0);
// Test 2: Native German Umlaute via multi-byte UTF-8 (Lines 2, 3 and 4)
display->printMessage(2, "Umlaute: AE-OE-UE-ss", 0);
display->printMessage(3, "Outputs: Ä Ö Ü ß", 0);
display->printMessage(4, "Outputs: ä ö ü", 0);
// Test 3: Newly supported Latin-1 symbols (Line 5)
display->printMessage(5, "Symbols: 23°C © 2026", 0);
// Test 4: Dynamic line-segment connection logic (Lines 6 and 7)
display->printMessage(6, "--------------------", 0);
display->printMessage(7, "Line test: ___ === _", 0);
// Let the user look at the test pattern for 60 seconds
vTaskDelay(pdMS_TO_TICKS(60000));
// Display löschen oder Zeilen vorbereiten, falls nötig
for (uint8_t line = 0; line < 8; line++) {
display->printMessage(line, " "); // Zeile leeren
}
int c = 32;
for ( uint8_t line = 0; line < 8 ; line++) {
for ( uint8_t i = 0; i < 22 ; i++ ) {
if (c > 255) break;
char buf[2] = { (char)c, '\0' };
display->appendMessage(line, buf , i);
c++; //next char
}
}
vTaskDelay(pdMS_TO_TICKS(60000));
// Print a quick shutdown notice to the CLI console before restarting
printf("\r\n[SYSTEM] Visual test completed. Triggering hardware reset now...\n");
vTaskDelay(pdMS_TO_TICKS(1000));
// Perform a clean software-controlled hardware restart of the ESP32
esp_restart();
// Clear all lines by printing empty strings (triggers Auto-Clear)
// for (uint8_t line = 0; line < 8; line++) {
// display->printMessage(line, "", 0);
// }
}
+22 -1
Datei anzeigen
@@ -33,13 +33,27 @@ public:
* @brief Thread-safe non-blocking method to print a message to the OLED display.
* @param line Destination line identifier index (0 to 7 for page addressing mode).
* @param text Null-terminated string message (maximum 20 characters for simple lines).
* @param startX Starting column coordinate index on the horizontal axis (0 to 127). Defaults to 0.
*/
void printMessage(uint8_t line, const char* text);
void printMessage(uint8_t line, const char* text, uint8_t startX = 0);
/**
* @brief Appends text to a line starting at a specific X coordinate WITHOUT clearing the rest of the row.
* Perfect for adding dynamic values next to static labels.
* @param line Selected hardware page index mapping from 0 to 7.
* @param text Pointer to a standard null-terminated sequence of tracking information.
* @param startX Starting column coordinate index on the horizontal axis (0 to 127).
*/
inline void appendMessage(uint8_t line, const char* text, uint8_t startX) {
// Automatically injects the 0x80 bit to tell the consumer task to disable auto-clear
printMessage(static_cast<uint8_t>(line | 0x80), text, startX);
}
private:
// Internal structure mapping the safe asynchronous data pipeline
struct DisplayMessage {
uint8_t targetLine;
uint8_t startX;
char content[24];
};
@@ -54,3 +68,10 @@ private:
esp_err_t sendCommand(uint8_t cmd) const;
esp_err_t initializeHardware();
};
/**
* @brief Run a comprehensive visual test suite and clear the screen after 5 seconds.
* Note: This is a global helper function decoupled from the main class interface.
* @param display Pointer to the active DisplayController instance.
*/
void run_display_font_test(DisplayController* display);
+314 -41
Datei anzeigen
@@ -16,10 +16,16 @@
const char* LoraController::TAG = "LORA";
/**
* @brief Constructor initializing core FreeRTOS synchronization primitives.
*/
LoraController::LoraController() {
m_isrSemaphore = xSemaphoreCreateBinary();
if (m_isrSemaphore == nullptr) {
ESP_LOGE(TAG, "Fatal: Failed to allocate LoRa ISR synchronization semaphore.");
m_stateMutex = xSemaphoreCreateMutex();
if (m_isrSemaphore == nullptr || m_stateMutex == nullptr) {
ESP_LOGE(TAG, "Fatal: Failed to allocate LoRa synchronization primitives.");
return;
}
}
@@ -30,8 +36,16 @@ LoraController::~LoraController() {
if (m_isrSemaphore != nullptr) {
vSemaphoreDelete(m_isrSemaphore);
}
if (m_stateMutex != nullptr) {
vSemaphoreDelete(m_stateMutex);
}
}
/**
* @brief Low-level driver method to extract a byte value from a transceiver configuration register.
* @param address The hardware register memory space tracking coordinate byte.
* @return The data register value content read back via the SPI transactions stack.
*/
uint8_t LoraController::readRegister(uint8_t address) {
if (m_spiHandle == nullptr) return 0;
@@ -49,6 +63,11 @@ uint8_t LoraController::readRegister(uint8_t address) {
return rxData[1];
}
/**
* @brief Low-level driver method to override an option byte parameter inside a transceiver hardware register.
* @param address The hardware register memory space tracking coordinate byte.
* @param value The option layout bits context to be committed directly into the transceiver.
*/
void LoraController::writeRegister(uint8_t address, uint8_t value) {
if (m_spiHandle == nullptr) return;
@@ -76,6 +95,10 @@ void IRAM_ATTR LoraController::gpioIsrHandler(void* arg) {
}
#pragma GCC diagnostic pop
/**
* @brief Orchestrates base interface configurations, system pin assignments, and checks silicon identities.
* @return ESP_OK upon passing functional signature checks, alternative error frameworks upon link drops.
*/
esp_err_t LoraController::initializeHardware() {
esp_err_t err;
@@ -110,48 +133,161 @@ esp_err_t LoraController::initializeHardware() {
}
ESP_LOGI(TAG, "SX1276 Handshake success. Silicon version confirmed: 0x12");
// Invoke the modular modem configuration
applyModemConfig();
err = gpio_install_isr_service(0);
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
return err;
}
// Configure interrupt triggering on the rising edge (PosEdge)
err = gpio_set_intr_type(Config::LORA_IRQ_PIN, GPIO_INTR_POSEDGE);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to set GPIO interrupt type: %s", esp_err_to_name(err));
return err;
}
// Bind the static ISR handler to the physical pin context
err = gpio_isr_handler_add(Config::LORA_IRQ_PIN, LoraController::gpioIsrHandler, m_isrSemaphore);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to register GPIO ISR handler: %s", esp_err_to_name(err));
return err;
}
return err;
// MARK STATE MATRIX READY (Thread-Safe Status Update)
// We use a safe timeout here. If the mutex is permanently blocked during boot,
// we must escalate this as a system fault instead of returning ESP_OK.
if (xSemaphoreTake(m_stateMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
m_state.isInitialized = true;
m_state.frequencyHz = Config::LORA_DEFAULT_FREQ_HZ;
xSemaphoreGive(m_stateMutex);
} else {
ESP_LOGE(TAG, "Fatal: State mutex acquisition timed out during hardware registration.");
return ESP_ERR_TIMEOUT; // Escalate the mutex block as a hard boot failure
}
ESP_LOGI(TAG, "SX1276 LoRa transceiver attached successfully via SPI (Version: 0x%02X).", version);
return ESP_OK;
}
void LoraController::setModeReceiver() {
writeRegister(0x01, 0x80U); // Sleep + LoRa Mode
// DIO0 auf RxDone mappen (Bit 7-6 auf 00)
writeRegister(0x40, 0x00U);
// In den kontinuierlichen Empfangsmodus schalten
writeRegister(0x01, 0x85U);
ESP_LOGD(TAG, "Radio state changed to RXCONTINUOUS");
}
/**
* @brief Applies the modem profile, RF carrier frequency, and network sync word.
* Note: The transceiver must transition through SLEEP mode to apply these alterations safely.
*/
void LoraController::applyModemConfig() {
// Force Sleep + LoRa mode to unlock configuration registers
writeRegister(0x01, 0x80U);
vTaskDelay(pdMS_TO_TICKS(10));
// Apply MeshCom network profile: 433.175 MHz, BW 250kHz, SF11, CR 4/6
// Apply RF carrier center frequency (e.g., MeshCom 433.175 MHz)
writeRegister(0x06, Config::LORA_FRF_MSB);
writeRegister(0x07, Config::LORA_FRF_MID);
writeRegister(0x08, Config::LORA_FRF_LSB);
writeRegister(0x1D, Config::LORA_MODEM_CONFIG_1); // RegModemConfig1
writeRegister(0x1E, Config::LORA_MODEM_CONFIG_2); // RegModemConfig2
writeRegister(0x40, 0x00U); // DIO0 map onto RxDone
writeRegister(0x01, 0x85U); // RXCONTINUOUS
ESP_LOGI(TAG, "Radio configured: 433.175 MHz | BW 250kHz | SF11 | CR 4/6");
}
// just write the m_state frequency....
m_state.frequencyHz = Config::LORA_DEFAULT_FREQ_HZ; // @TODO: remove this line after implementation of the doubly linked list
void LoraController::setFrequency(uint8_t msb, uint8_t mid, uint8_t lsb) {
if (m_spiHandle == nullptr) return;
// Safely force transceiver to Standby to update hardware registers safely
// Configure modem bandwidth, spreading factor, and coding rate
writeRegister(0x1D, Config::LORA_MODEM_CONFIG_1);
writeRegister(0x1E, Config::LORA_MODEM_CONFIG_2);
// Set the network sync word (0x34 for open/public amateur networks like MeshCom)
writeRegister(0x39, Config::LORA_SYNC_WORD);
// Set the preamble length (0x0C instead of 0x08 for more stability, see docs/lora/device-settings.md)
writeRegister(0x20, Config::LORA_PREAMBLE_LENGTH_MSB);
writeRegister(0x21, Config::LORA_PREAMBLE_LENGTH_LSB);
// Transition to STANDBY to allow the internal PLL to lock and stabilize
writeRegister(0x01, 0x81U);
vTaskDelay(pdMS_TO_TICKS(5));
writeRegister(0x06, msb);
writeRegister(0x07, mid);
writeRegister(0x08, lsb);
ESP_LOGI(TAG, "Radio profile applied: 433.175 MHz | SF11 | BW 250kHz | SyncWord 0x34");
}
// Re-engage Continous Reception Mode
writeRegister(0x01, 0x85U);
ESP_LOGI(TAG, "Frequency dynamically shifted to: 0x%02X%02X%02X", msb, mid, lsb);
#ifndef MATH_COMPARE
#define MATH_COMPARE 0
#endif
void LoraController::setFrequency(uint32_t freq_hz) {
if (m_spiHandle == nullptr) return;
if (xSemaphoreTake(m_stateMutex, portMAX_DELAY) == pdTRUE) {
m_state.frequencyHz = freq_hz;
// 1. Fetch current mode and shift transceiver into Standby (0x01)
// Changing RF carrier registers is strictly prohibited while actively radiating or sampling
const uint8_t previousMode = readRegister(0x01); // RegOpMode
writeRegister(0x01, static_cast<uint8_t>((previousMode & 0xF8U) | 0x01U));
vTaskDelay(pdMS_TO_TICKS(2));
// 2. High-speed fractional frequency calculation
// Formula optimized from: (freq_hz * 2^19) / 32,000,000 b/c 32,000,000 can be seen as 2^11 * 5^6:
// we see freq_hz * 2^(19-11)/5^6 which is: freq_hz * 2^8 / 15625, i.e.
// reduced to exact fraction: 256 / 15,625
// or even more using shift operation, we can switch on a compare of all three methods with #MATH_COMPARE 1
#if (MATH_COMPARE == 1)
// =================================================================
// BENCHMARK MODE (Runs all 3 variants and measures CPU cycles)
// =================================================================
uint32_t start_cycles = esp_cpu_get_cycle_count();
[[maybe_unused]] volatile uint32_t frf_long = static_cast<uint32_t>((static_cast<uint64_t>(freq_hz) << 19) / 32000000ULL);
uint32_t cycles_long = esp_cpu_get_cycle_count() - start_cycles;
start_cycles = esp_cpu_get_cycle_count();
[[maybe_unused]] volatile uint32_t frf_64bit = static_cast<uint32_t>((static_cast<uint64_t>(freq_hz) * 256ULL) / 15625ULL);
uint32_t cycles_64bit = esp_cpu_get_cycle_count() - start_cycles;
start_cycles = esp_cpu_get_cycle_count();
volatile uint32_t frf_shift = static_cast<uint32_t>((static_cast<uint64_t>(freq_hz) * 1125899907ULL) >> 36);
uint32_t cycles_shift = esp_cpu_get_cycle_count() - start_cycles;
printf("[MATH_COMPARE] Freq: %lu Hz -> long 64bit: %lu cycles | 64bit: %lu cycles | Shift: %lu cycles\n",
freq_hz, cycles_long, cycles_64bit, cycles_shift);
const uint32_t frf = frf_shift; // Fallback to the safe bit-shift variant
#else
// =================================================================
// PRODUCTION MODE (Highly optimized, zero benchmark overhead)
// =================================================================
const uint32_t frf = static_cast<uint32_t>((static_cast<uint64_t>(freq_hz) * 1125899907ULL) >> 36);
#endif
// 3. Shift the 24-bit frequency tracking token across the three consecutive hardware registers
writeRegister(0x06, static_cast<uint8_t>((frf >> 16) & 0xFFU)); // RegFrfMsb
writeRegister(0x07, static_cast<uint8_t>((frf >> 8) & 0xFFU)); // RegFrfMid
writeRegister(0x08, static_cast<uint8_t>(frf & 0xFFU)); // RegFrfLsb
// 4. Restore previous operational transceiver path configuration
writeRegister(0x01, previousMode);
xSemaphoreGive(m_stateMutex);
// 5. Force state synchronization and PLL locking verification
setModeReceiver();
// High-priority diagnosis logging bypasses CLI lock buffers uncensored
printf("\r\n[LoRa HW] Frequency successfully shifted to %lu Hz (FRF Word: 0x%06X)\r\n",
(unsigned long)freq_hz, (unsigned int)(frf & 0xFFFFFFU));
fflush(stdout);
}
}
void LoraController::setBandwidth(uint32_t bw_khz) {
@@ -204,7 +340,51 @@ void LoraController::taskTrampoline(void* pvParameters) {
vTaskDelete(nullptr);
}
/**
* @brief Computes high-precision calibrated RSSI and SNR configurations based on frequency bands.
* @note Now uses high-precision float conversions to preserve the full 0.25 dB native hardware steps.
* @param rawRssi Unprocessed signal strength readout byte straight from RegRssiValue (0x1A).
* @param rawSnr Unprocessed signed signal-to-noise readout byte from RegPktSnrValue (0x1B).
* @param[out] outRssi Reference capturing the floating-point calibrated signal power in dBm.
* @param[out] outSnr Reference capturing the floating-point calibrated signal-to-noise ratio in dB.
*/
void LoraController::calculateTelemetry(uint8_t rawRssi, uint8_t rawSnr, float& outRssi, float& outSnr) {
uint32_t currentFreq = 0;
// Securely pull the active runtime frequency configuration
if (xSemaphoreTake(m_stateMutex, portMAX_DELAY) == pdTRUE) {
currentFreq = m_state.frequencyHz;
xSemaphoreGive(m_stateMutex);
}
// 1. Precise SNR calculation (preserve native 0.25 dB resolution step by step)
float actualSnr = static_cast<float>(static_cast<int8_t>(rawSnr)) * 0.25f;
outSnr = actualSnr;
// 2. Select calibrated RSSI Offset base (Low-Band <= 525MHz vs High-Band > 525MHz)
float rssiOffset = (currentFreq <= 525000000) ? -164.0f : -157.0f;
// 3. Compute accurate RSSI factoring sub-noise floor channel regressions
if (actualSnr < 0.0f) {
outRssi = rssiOffset + static_cast<float>(rawRssi) + actualSnr;
} else {
outRssi = rssiOffset + static_cast<float>(rawRssi);
}
}
void LoraController::run() {
// Lambda Object:
auto utf8_advance = [](const std::string& str, size_t start_byte, size_t num_chars) -> size_t {
size_t i = start_byte;
size_t chars_counted = 0;
while (i < str.length() && chars_counted < num_chars) {
if (((unsigned char)str[i] & 0xC0) != 0x80) chars_counted++;
i++;
}
while (i < str.length() && ((unsigned char)str[i] & 0xC0) == 0x80) i++;
return i;
};
if (initializeHardware() != ESP_OK) {
ESP_LOGE(TAG, "Critical: LoRa init failed. Thread aborted.");
return;
@@ -212,15 +392,19 @@ void LoraController::run() {
setModeReceiver();
extern DisplayController* display; // Reference the global display singleton
static uint32_t packetCounter = 0; // overflow in abt. 272 years (1 packet every 2 seconds)
ESP_LOGI(TAG, "LoRa RX was initialized and is running");
while (true) {
if (xSemaphoreTake(m_isrSemaphore, portMAX_DELAY) == pdTRUE) {
// CHANGE: wait a maximum of 500 ms instead of infinity long
if (xSemaphoreTake(m_isrSemaphore, pdMS_TO_TICKS(500)) == pdTRUE) {
// if (xSemaphoreTake(m_isrSemaphore, portMAX_DELAY) == pdTRUE) {
uint32_t irqFlags = readRegister(0x12);
writeRegister(0x12, static_cast<uint8_t>(irqFlags));
if ((irqFlags & 0x40U) != 0U) { // RxDone Flag
packetCounter++;
LoraPacket packet = {};
packet.length = readRegister(0x13);
@@ -231,38 +415,74 @@ void LoraController::run() {
packet.payload[i] = readRegister(0x00);
}
// Calculate the telemetry data
packet.rssi = static_cast<int8_t>(static_cast<int>(readRegister(0x1A)) - 157);
packet.snr = static_cast<int8_t>(static_cast<int>(readRegister(0x1B)) / 4);
// packet.rssi = static_cast<int8_t>(static_cast<int>(readRegister(0x1A)) - 157);
// packet.snr = static_cast<int8_t>(static_cast<int>(readRegister(0x1B)) / 4);
// Read raw values from transceiver registers
uint8_t rawRssiReg = readRegister(0x1A);
uint8_t rawSnrReg = readRegister(0x1B);
// LIVE-AUSGABE DIREKT AUF DIE CONSOLE (Garantiert un-gemutet!)
// Dynamically process telemetry parameters with floating point precision
calculateTelemetry(rawRssiReg, rawSnrReg, packet.rssi, packet.snr);
// Formatted terminal trace using fixed precision formatting (.1f)
printf("\r\n--------------------------------------------------\r\n");
printf("[LoRa RX] Paket erfolgreich empfangen!\r\n");
printf(" Laenge: %u Bytes\r\n", packet.length);
printf(" RSSI : %d dBm (Signalstaerke)\r\n", packet.rssi);
printf(" SNR : %d dB (Rauschabstand)\r\n", packet.snr);
//ESP_LOGI(TAG, "Incoming frame packet received! Size: %u bytes, RSSI: %d dBm, SNR: %d dB",
// packet.length, packet.rssi, packet.snr);
// ASCII-Klartext-Ausgabe für das Terminal formatiert
printf(" Inhalt: \"");
printf("[LoRa RX] Packet #%lu successfully received!\r\n", (unsigned long)packetCounter);
// printf(" Database ID : %lu\r\n", (unsigned long)nodeDbId);
printf(" Length : %u Bytes\r\n", packet.length);
printf(" RSSI : %.1f dBm (Signal Strength)\r\n", packet.rssi);
printf(" SNR : %.1f dB (Signal-to-Noise Ratio)\r\n", packet.snr);
printf(" Content: \"");
for (uint8_t i = 0; i < packet.length; i++) {
char c = static_cast<char>(packet.payload[i]);
// Nur druckbare Zeichen ausgeben, um das Terminal nicht zu verwirren
if (c >= 32 && c <= 126) {
// Only print printable characters to avoid confusing the terminal
if (c >= 32 && c <= 126)
printf("%c", c);
} else {
printf("."); // Punkt für nicht-druckbare Binärbytes
}
else
printf("~"); // not printable chars
}
printf("\"\r\n--------------------------------------------------\r\n");
printf("%s # ", Config::PROJECT_TITLE); // CLI-Prompt wiederherstellen
printf("\"\r\n Hex : ");
for (uint8_t i = 0; i < packet.length; i++)
printf("%02X ", packet.payload[i]);
printf("\r\n--------------------------------------------------\r\n");
printf("%s # ", Config::PROJECT_TITLE); // CLI prompt
fflush(stdout);
// FIXED: Uses the new parameter order (line first, text second)
if (display != nullptr) {
display->printMessage(1, "LoRa Msg Received");
char displayBuffer[85]; // worst case 21 chars with each 4 bytes + Nullbyte.
snprintf(displayBuffer, sizeof(displayBuffer), "#%03u %4.1fdBm %2.1fdB",
(unsigned int)(packetCounter % 1000),
packet.rssi,
packet.snr);
display->printMessage(2, displayBuffer);
// we don't care about UTF-8 for now
std::string text(reinterpret_cast<const char*>(packet.payload), static_cast<size_t>(packet.length));
size_t byte_pos = 0;
uint8_t linenr = 3; // start at line 3
while ( byte_pos < text.length() && linenr < 8) {
// The '::' ensures that the global/local function is called.
size_t next_byte_pos = utf8_advance(text, byte_pos, 21);
std::string line = text.substr(byte_pos, next_byte_pos - byte_pos);
std::strncpy(displayBuffer, line.c_str(), sizeof(displayBuffer ) - 1);
displayBuffer[sizeof(displayBuffer) - 1] ='\0';
display->printMessage(linenr++, displayBuffer);
byte_pos = next_byte_pos;
}
}
}
// New: after work or after 500ms delay, the chip has usually changed its mode, we update the state for the m_state:
uint8_t currentHardwareMode = readRegister(0x01);
if (xSemaphoreTake(m_stateMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
m_state.currentMode = currentHardwareMode;
xSemaphoreGive(m_stateMutex);
}
}
}
}
@@ -278,3 +498,56 @@ void LoraController::startTask(uint32_t priority) {
1
);
}
/**
* @brief Generates an atomic, high-performance runtime snapshot of configuration parameters and metrics.
*/
LoraStateSnapshot LoraController::getSnapshot() {
LoraStateSnapshot snapshot = {};
// Sichere die thread-übergreifende Konsistenz mittels Mutex ab
if (xSemaphoreTake(m_stateMutex, portMAX_DELAY) == pdTRUE) {
// Kopiere die primitiven Werte aus dem internen Zustandstabellen-Struct (m_state)
snapshot.frequencyHz = m_state.frequencyHz;
snapshot.bandwidthKhz = m_state.bandwidthKhz;
snapshot.totalPacketsReceived = m_state.totalPacketsReceived;
snapshot.totalPacketsTransmitted = m_state.totalPacketsTransmitted;
snapshot.spreadingFactor = m_state.spreadingFactor;
snapshot.codingRate = m_state.codingRate;
snapshot.isInitialized = m_state.isInitialized;
// Zähle die Elemente in der Liste, ohne die schweren Payload-Vektoren zu kopieren
// TODO install the package list:
// snapshot.currentHistoryCount = m_state.packetList.size();
// Read Register 0x01 ONLY ONCE to avoid SPI race conditions
// uint8_t rohofmode = readRegister(0x01);
// uint8_t rawMode = rohofmode & 0x07U;
// use the cached mode from RAM instead live register read
uint8_t rawMode = m_state.currentMode & 0x07U;
// Print the real, unmanipulated hex value safely
//printf("Readregister actual raw hex: 0x%02X\r\n", rohofmode);
//fflush(stdout);
static const char* const MODE_STRINGS[] = {
"SLEEP", // 0x00
"STANDBY", // 0x01
"FSTX", // 0x02
"TX", // 0x03
"FSRX", // 0x04
"RXCONTINUOUS", // 0x05
"RXSINGLE", // 0x06
"CAD" // 0x07
};
strncpy(snapshot.currentModeStr, MODE_STRINGS[rawMode], sizeof(snapshot.currentModeStr) - 1);
snapshot.currentModeStr[sizeof(snapshot.currentModeStr) - 1] = '\0'; // guarantee NULL termination
// Release mutex immediately
xSemaphoreGive(m_stateMutex);
}
return snapshot;
}
+55 -8
Datei anzeigen
@@ -1,6 +1,6 @@
/**
* @file lora.hpp
* @brief Thread-safe asynchronous Semtech SX1276 LoRa transceiver driver for FoxMesh
* @brief Thread-safe asynchronous Semtech SX1276 LoRa transceiver driver interface.
* @author OM Tom (OE3TKT)
*
* @license SPDX-License-Identifier: MIT
@@ -15,19 +15,29 @@
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "freertos/task.h"
#include "lora_types.hpp"
#include <iostream>
#include <string>
#include <cstring>
/**
* @brief Controller class orchestrating hardware configurations, data transfers, and metrics collection.
*/
class LoraController {
public:
/**
* @brief Structure defining the localized packet buffer space.
* @brief Simple internal abstraction layer mirroring the raw incoming packet payload.
*/
struct LoraPacket {
uint8_t payload[256]; ///< Raw data payload frame matching max SX1276 FIFO allocation
uint8_t length; ///< Total number of active bytes captured inside the frame
int8_t rssi; ///< Received Signal Strength Indicator metric in dBm
int8_t snr; ///< Signal-to-Noise Ratio metric in dB
float rssi; /**< High-precision calibrated received signal power in dBm. */
float snr; /**< High-precision calibrated signal-to-noise ratio in dB. */
uint8_t length; /**< Payload size explicitly specified in bytes. */
uint8_t payload[256]; /**< Fixed internal memory buffer capturing raw ingress bytes. Max SX1276 FIFO is 256. */
};
explicit LoraController();
virtual ~LoraController();
@@ -55,7 +65,13 @@ public:
* @param mid Middle Significant Byte of the frequency fraction.
* @param lsb Least Significant Byte of the frequency fraction.
*/
void setFrequency(uint8_t msb, uint8_t mid, uint8_t lsb);
// void setFrequency(uint8_t msb, uint8_t mid, uint8_t lsb);
/**
* @brief Thread-safely updates the operational state tracking variables and overrides transceiver registers.
* @param freq_hz Core RF targeting frequency structured in absolute Hertz scale units.
*/
void setFrequency(uint32_t freq_hz);
/**
* @brief Dynamically updates the LoRa signal bandwidth at runtime.
@@ -63,12 +79,27 @@ public:
*/
void setBandwidth(uint32_t bw_khz);
/**
* @brief Generates an atomic, high-performance runtime snapshot of configuration parameters and metrics.
* @details This routine performs a rapid, thread-safe memory copy of the volatile tracking state variables.
* Unlike getPacketListCopy(), this method explicitly excludes the heavy heap-allocated packet list data,
* making it exceptionally lightweight and highly suitable for high-frequency UI display refreshes,
* CLI updates, or periodic remote health diagnostics.
* @note Acquires the internal synchronization mutex lock momentarily to guarantee structural telemetry alignment.
* @return A self-contained snapshot structure mapping the current system hardware configurations and operational counts.
*/
LoraStateSnapshot getSnapshot();
private:
static const char* TAG;
spi_device_handle_t m_spiHandle = nullptr;
TaskHandle_t m_taskHandle = nullptr;
SemaphoreHandle_t m_isrSemaphore = nullptr;
static const char* TAG;
LoraStateTable m_state; ///< Internal volatile tracking record data context.
SemaphoreHandle_t m_stateMutex = nullptr; ///< Mutex primitive enforcing mutual exclusion over m_state modifications.
static void taskTrampoline(void* pvParameters);
static void IRAM_ATTR gpioIsrHandler(void* arg);
@@ -79,4 +110,20 @@ private:
esp_err_t initializeHardware();
void setModeReceiver();
void applyModemConfig();
/**
* @brief Evaluates raw register readings into calibrated RSSI and SNR telemetry metrics.
* @details Automatically compensates the RSSI offset base based on the active carrier frequency
* band (Low-Band vs High-Band boundary at 525 MHz) as well as sub-noise SNR conditions.
* @param rawRssi The raw digital output read from RegRssiValue (0x1A).
* @param rawSnr The raw digital twos-complement output read from RegPktSnrValue (0x1B).
* @param[out] outRssi Reference destination pointer receiving the calibrated RSSI value in dBm.
* @param[out] outSnr Reference destination pointer receiving the calibrated SNR value in dB.
*/
void calculateTelemetry(uint8_t rawRssi, uint8_t rawSnr, float& outRssi, float& outSnr);
size_t utf8_advance(const std::string& str, size_t start_byte, size_t num_chars);
};
+47
Datei anzeigen
@@ -0,0 +1,47 @@
/**
* @file lora_types.hpp
* @brief Definition of data structures, nodes, and state tables for the LoRa driver.
* @author OM Tom (OE3TKT)
*
* @license SPDX-License-Identifier: MIT
* @copyright Copyright (c) 2026 OM Tom (OE3TKT)
* For the full license terms, see the LICENSE file in the project root.
*/
/**
* @brief Runtime operational state table monitoring configurations and historical packet data.
* @note Optimized using "largest-to-smallest" data type ordering to completely eliminate memory padding.
*/
struct LoraStateTable {
// std::list<LoraPacketNode> packetList; ///< Central database component built on a doubly-linked list for versatile node operations. (Contains pointers)
size_t maxHistorySize = 10; ///< Soft-limit threshold capping the maximum number of history records stored before auto-purge. (4 or 8 Bytes)
uint32_t frequencyHz = 433175000; ///< RF carrier operating frequency configured in Hertz. (4 Bytes)
uint32_t totalPacketsReceived = 0; ///< Accumulator metric track-counting successful ingress frames. (4 Bytes)
uint32_t totalPacketsTransmitted = 0; ///< Accumulator metric track-counting egress frames transmitted. (4 Bytes)
uint32_t nextDatabaseId = 1; ///< Identity sequencer pointer controlling the next node's databaseId field. (4 Bytes)
uint32_t bandwidthKhz = 250; ///< Modem bandwidth performance target configured in kHz. (4 Bytes)
uint8_t currentMode = 0; ///< Current operationl modem mode (1 Byte)
uint8_t spreadingFactor = 11; ///< LoRa Spreading Factor configuration (typically ranges between 6 and 12). (1 Byte)
uint8_t codingRate = 6; ///< Coding Rate denominator setting (e.g., value 6 yields a 4/6 coding protection overhead). (1 Byte)
bool isInitialized = false; ///< True if hardware was successfully validated and attached during initialization. (1 Byte)
// The three 1-byte variables at the bottom align perfectly into a 32-bit slot, leaving minimal trailing padding.
};
/**
* @brief Thread-isolated telemetry snapshot for UI displays and remote diagnostics.
* @note Optimized using "largest-to-smallest" data type ordering to minimize memory padding.
*/
struct LoraStateSnapshot {
size_t currentHistoryCount; ///< Soft-limit threshold capping the maximum number of history records stored before auto-purge. (4 or 8 Bytes)
uint32_t frequencyHz; ///< RF carrier operating frequency configured in Hertz. (4 Bytes)
uint32_t bandwidthKhz; ///< Modem bandwidth performance target configured in kHz. (4 Bytes)
uint32_t totalPacketsReceived; ///< Accumulator metric track-counting successful ingress frames. (4 Bytes)
uint32_t totalPacketsTransmitted; ///< Accumulator metric track-counting egress frames transmitted. (4 Bytes)
char currentModeStr[16]; ///< Human-readable string representation of the current operational modem mode. (16 Bytes)
uint8_t spreadingFactor; ///< LoRa Spreading Factor configuration (typically ranges between 6 and 12). (1 Byte)
uint8_t codingRate; ///< Coding Rate denominator setting (e.g., value 6 yields a 4/6 coding protection overhead). (1 Byte)
bool isInitialized; ///< True if hardware was successfully validated and attached during initialization. (1 Byte)
// Elements are strictly organized by size alignment requirements to completely prevent intermittent alignment gaps.
};
+9 -4
Datei anzeigen
@@ -8,6 +8,10 @@
* For the full license terms, see the LICENSE file in the project root.
*/
#ifndef GIT_TAG
#define GIT_TAG "unknown"
#endif
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
@@ -127,10 +131,10 @@ extern "C" {
// Trigger synchronous welcome splash screen frame
if (display != nullptr) {
char msg[32];
//display->printMessage(0, "FoxMesh " Config::PROJECT_VERSION " \x86" "OE3TKT");
snprintf(msg, sizeof(msg), "FoxMesh %s OE3TKT", Config::PROJECT_VERSION);
display->printMessage(1, msg);
//display->printMessage(2, "Bärbel frühstückt ©");
snprintf(msg, sizeof(msg), "FoxMesh " GIT_TAG);
display->printMessage(0, msg);
// display->appendMessage(0, GIT_TAG, 12*6);
display->printMessage(1, "----------------------");
}
// Main system watchdog and status monitor loop
while (true) {
@@ -138,6 +142,7 @@ extern "C" {
Config::PROJECT_TITLE,
Config::PROJECT_VERSION,
(unsigned long)esp_get_free_heap_size());
// ESP_LOGI(TAG, "Git-Hash: %s", GIT_HASH);
vTaskDelay(pdMS_TO_TICKS(5000));
}