Commits vergleichen
23
Commits
| Autor | SHA1 | Datum | |
|---|---|---|---|
|
|
0a1c781f0d | ||
|
|
b54a9fc714 | ||
|
|
1a56c456f6 | ||
|
|
fb0e096e4e | ||
|
|
618ff87d5c | ||
|
|
256fb9cdac | ||
|
|
4823f519b2 | ||
|
|
1d22bcb4ce | ||
|
|
1616bb25f0 | ||
|
|
aab6f8de43 | ||
|
|
f2e4df9cc5 | ||
|
|
6b43bd8dbb | ||
|
|
f8088d3793 | ||
|
|
5c90b419bf | ||
|
|
13b9df523d | ||
|
|
398cf135a4 | ||
|
|
ae747dc0cc | ||
|
|
e175568531 | ||
|
|
c0a94ce627 | ||
|
|
9124f55b32 | ||
|
|
95e74a960a | ||
|
|
8b5145bc30 | ||
|
|
f16a243e3f |
Keine Dateien entsprechen Ihrer Suche
@@ -1,9 +1,10 @@
|
|||||||
name: Automated CI Build of FoxMesh
|
name: Build of FoxMesh
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: [ main ]
|
||||||
- main
|
pull_request:
|
||||||
|
branches: [ main ] # Trigger the workflow on pull request events targeting the main branch
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
target_branch:
|
target_branch:
|
||||||
@@ -19,113 +20,123 @@ jobs:
|
|||||||
contents: write # We want also create official Releases
|
contents: write # We want also create official Releases
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
|
- 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 "🐧 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 }}."
|
- 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 }}
|
|
||||||
|
|
||||||
# 2. Extract Git and Date Metadata for the dynamic artifact filename
|
# 1. Checkout the target branch, with fetch-depth: 0 and fetch-tags: true -> loading all tags
|
||||||
- name: Generate Dynamic Artifact Metadata
|
- name: Checkout Target branch
|
||||||
id: meta
|
uses: actions/checkout@v7
|
||||||
run: |
|
with:
|
||||||
# Get current date in YYYYMMDD format
|
ref: ${{ github.event.inputs.target_branch || github.ref }}
|
||||||
echo "CURRENT_DATE=$(date +'%Y%m%d')" >> $GITHUB_ENV
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
# Get short 7-character git commit hash
|
# 2. Extract Git and Date Metadata for the dynamic artifact filename
|
||||||
echo "GIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
- 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)
|
# Get short 7-character git commit hash
|
||||||
RAW_BRANCH=${{ github.event.inputs.target_branch || github.ref_name }}
|
echo "GIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||||
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
|
|
||||||
|
|
||||||
# 3. Set up Python environment
|
# Extract and sanitize branch name (replace slashes and special characters with dashes)
|
||||||
- name: Set up Python 3.x
|
RAW_BRANCH=${{ github.event.inputs.target_branch || github.ref_name }}
|
||||||
uses: actions/setup-python@v7
|
CLEAN_BRANCH=$(echo "$RAW_BRANCH" | sed 's/[^a-zA-Z0-9.\-]/_§/g' | sed 's|§|-|g' | sed 's|_||g')
|
||||||
with:
|
echo "GIT_BRANCH=$CLEAN_BRANCH" >> $GITHUB_ENV
|
||||||
python-version: '3.x'
|
|
||||||
|
|
||||||
# 4. Cache global PlatformIO directories, uses a dynamic key based on platformio.ini
|
#3 Python alread installed
|
||||||
- name: Cache PlatformIO
|
- name: Show Python Version
|
||||||
uses: actions/cache@v6
|
run: python --version
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cache/pio
|
|
||||||
~/.platformio
|
|
||||||
.pio
|
|
||||||
key: ${{ runner.os }}-pio-${{ hashFiles('**/platformio.ini') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pio-
|
|
||||||
|
|
||||||
# 5. Install PlatformIO Core and the required intelhex extension in a single, quiet command
|
# 4. Cache global PlatformIO directories, uses a dynamic key based on platformio.ini
|
||||||
- name: Install PlatformIO Core
|
- name: Cache PlatformIO
|
||||||
run: python -m pip install --quiet --upgrade pip platformio intelhex
|
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: PlatformIO in venv installieren
|
||||||
- name: Install ESP32 platform
|
run: |
|
||||||
run: pio pkg install --silent --global --platform espressif32
|
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
|
# 5. Install PlatformIO Core and the required intelhex extension in a single, quiet command
|
||||||
- name: Compile the Project
|
# - name: Install PlatformIO Core
|
||||||
run: pio run
|
# run: python -m pip install --quiet --upgrade pip platformio intelhex
|
||||||
|
|
||||||
# 8. Dynamically create the smart flashing scripts next to the T-Beam binary
|
# 6. Install ESP32 Platform SDK
|
||||||
# with optional bootloader detection and group check
|
# - name: Install ESP32 platform
|
||||||
# the table of flash addresses usually found at /partitions-4MB-safeboot.csv
|
# run: pio pkg install --silent --global --platform espressif32
|
||||||
- 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"
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# 7. Compile the Project
|
||||||
# 1. Universal Linux Shell Script
|
- name: Compile the Project
|
||||||
# ----------------------------------------------------------------------
|
run: pio run
|
||||||
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}"
|
# 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 -e "${RED}Error: esptool is not installed.${NC}"
|
||||||
echo "Please install it via your package manager or pip: 'pip install esptool'"
|
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'"
|
echo "Option Linux Arch: 'sudo pacman -Sy && sudo pacman -S extra-testing/esptool'"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FIRMWARE="firmware.bin"
|
FIRMWARE="firmware.bin"
|
||||||
|
|
||||||
|
|
||||||
# Firmware File
|
# Firmware File
|
||||||
if [ ! -f "$FIRMWARE" ]; then
|
if [ ! -f "$FIRMWARE" ]; then
|
||||||
echo -e "${RED}Error: No matching firmware binary found in this folder!${NC}"
|
echo -e "${RED}Error: No matching firmware binary found in this folder!${NC}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "${GREEN}Selected Firmware:${NC} $FIRMWARE ($BOARD_NAME)"
|
echo -e "${GREEN}Selected Firmware:${NC} $FIRMWARE ($BOARD_NAME)"
|
||||||
PORT=$(ls /dev/ttyUSB* /dev/ttyACM* 2>/dev/null | head -n 1)
|
PORT=$(ls /dev/ttyUSB* /dev/ttyACM* 2>/dev/null | head -n 1)
|
||||||
if [ -z "$PORT" ]; then
|
if [ -z "$PORT" ]; then
|
||||||
echo -e "${RED}Error: No connected ESP32 Board (ttyUSB/ttyACM) found!${NC}"
|
echo -e "${RED}Error: No connected ESP32 Board (ttyUSB/ttyACM) found!${NC}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo -e "${GREEN}Detected Board on port:${NC} $PORT"
|
echo -e "${GREEN}Detected Board on port:${NC} $PORT"
|
||||||
|
|
||||||
# Dynamic Permission Check: Determine required group for the specific TTY device
|
# Dynamic Permission Check: Determine required group for the specific TTY device
|
||||||
REQUIRED_GROUP=$(stat -c '%G' "$PORT")
|
REQUIRED_GROUP=$(stat -c '%G' "$PORT")
|
||||||
if [ "$REQUIRED_GROUP" != "root" ]; then
|
if [ "$REQUIRED_GROUP" != "root" ]; then
|
||||||
if ! groups "$USER" | grep -q "\b${REQUIRED_GROUP}\b"; 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 "${RED}Error: You do not have permissions to access $PORT.${NC}"
|
||||||
echo -e "The device belongs to the group: ${YELLOW}$REQUIRED_GROUP${NC}"
|
echo -e "The device belongs to the group: ${YELLOW}$REQUIRED_GROUP${NC}"
|
||||||
@@ -134,107 +145,107 @@ jobs:
|
|||||||
echo -e "After running the command, please log out and log back in for the changes to take effect."
|
echo -e "After running the command, please log out and log back in for the changes to take effect."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build flash arguments from the address table (only include files that exist)
|
# Build flash arguments from the address table (only include files that exist)
|
||||||
FLASH_ARGS=""
|
FLASH_ARGS=""
|
||||||
if [ -f "bootloader.bin" ]; then
|
if [ -f "bootloader.bin" ]; then
|
||||||
FLASH_ARGS="$FLASH_ARGS __BOOTLOADER_ADDR__ bootloader.bin"
|
FLASH_ARGS="$FLASH_ARGS __BOOTLOADER_ADDR__ bootloader.bin"
|
||||||
fi
|
fi
|
||||||
if [ -f "partitions.bin" ]; then
|
if [ -f "partitions.bin" ]; then
|
||||||
FLASH_ARGS="$FLASH_ARGS __PARTITIONS_ADDR__ partitions.bin"
|
FLASH_ARGS="$FLASH_ARGS __PARTITIONS_ADDR__ partitions.bin"
|
||||||
fi
|
fi
|
||||||
if [ -f "otadata.bin" ]; then
|
if [ -f "otadata.bin" ]; then
|
||||||
FLASH_ARGS="$FLASH_ARGS __OTADATA_ADDR__ otadata.bin"
|
FLASH_ARGS="$FLASH_ARGS __OTADATA_ADDR__ otadata.bin"
|
||||||
fi
|
fi
|
||||||
if [ -f "safeboot.bin" ]; then
|
if [ -f "safeboot.bin" ]; then
|
||||||
FLASH_ARGS="$FLASH_ARGS __SAFEBOOT_ADDR__ safeboot.bin"
|
FLASH_ARGS="$FLASH_ARGS __SAFEBOOT_ADDR__ safeboot.bin"
|
||||||
fi
|
fi
|
||||||
FLASH_ARGS="$FLASH_ARGS __FIRMWARE_ADDR__ $FIRMWARE"
|
FLASH_ARGS="$FLASH_ARGS __FIRMWARE_ADDR__ $FIRMWARE"
|
||||||
|
|
||||||
echo -e "${GREEN}Flashing:${NC}$FLASH_ARGS"
|
echo -e "${GREEN}Flashing:${NC}$FLASH_ARGS"
|
||||||
esptool --chip esp32 --port "$PORT" --baud 921600 write-flash -z $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}"
|
echo -e "${GREEN}=== Success! $BOARD_NAME successfully updated ===${NC}"
|
||||||
else
|
else
|
||||||
echo -e "${RED}=== Flash failed! ===${NC}"
|
echo -e "${RED}=== Flash failed! ===${NC}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Substitute address placeholders with configured hex values
|
# Substitute address placeholders with configured hex values
|
||||||
sed -i \
|
sed -i \
|
||||||
-e "s|__BOOTLOADER_ADDR__|${FLASH_BOOTLOADER_ADDR}|g" \
|
-e "s|__BOOTLOADER_ADDR__|${FLASH_BOOTLOADER_ADDR}|g" \
|
||||||
-e "s|__PARTITIONS_ADDR__|${FLASH_PARTITIONS_ADDR}|g" \
|
-e "s|__PARTITIONS_ADDR__|${FLASH_PARTITIONS_ADDR}|g" \
|
||||||
-e "s|__OTADATA_ADDR__|${FLASH_OTADATA_ADDR}|g" \
|
-e "s|__OTADATA_ADDR__|${FLASH_OTADATA_ADDR}|g" \
|
||||||
-e "s|__SAFEBOOT_ADDR__|${FLASH_SAFEBOOT_ADDR}|g" \
|
-e "s|__SAFEBOOT_ADDR__|${FLASH_SAFEBOOT_ADDR}|g" \
|
||||||
-e "s|__FIRMWARE_ADDR__|${FLASH_FIRMWARE_ADDR}|g" \
|
-e "s|__FIRMWARE_ADDR__|${FLASH_FIRMWARE_ADDR}|g" \
|
||||||
flash_esp32_board.sh
|
flash_esp32_board.sh
|
||||||
chmod +x flash_esp32_board.sh
|
chmod +x flash_esp32_board.sh
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
# 2. Universal Windows Batch Script
|
# 2. Universal Windows Batch Script
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
cat << 'EOF' > flash_esp32_board.bat
|
cat << 'EOF' > flash_esp32_board.bat
|
||||||
@echo off
|
@echo off
|
||||||
setlocal enabledelayedexpansion
|
setlocal enabledelayedexpansion
|
||||||
echo ===================================================
|
echo ===================================================
|
||||||
echo === FoxMesh Universal ESP32 Windows Flashing ======
|
echo === FoxMesh Universal ESP32 Windows Flashing ======
|
||||||
echo ===================================================
|
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!
|
echo [ERROR] No matching firmware binary found in this folder!
|
||||||
pause
|
pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
echo [INFO] Selected Firmware: %FIRMWARE% (%BOARD_NAME%)
|
echo [INFO] Selected Firmware: %FIRMWARE% (%BOARD_NAME%)
|
||||||
|
|
||||||
set "COMPORT="
|
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"
|
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%"=="" (
|
if "%COMPORT%"=="" (
|
||||||
for /f "tokens=2 delims=()" %%A in ('wmic path Win32_PnPEntity get Name ^| findstr /i "(COM"') do set "COMPORT=%%A"
|
for /f "tokens=2 delims=()" %%A in ('wmic path Win32_PnPEntity get Name ^| findstr /i "(COM"') do set "COMPORT=%%A"
|
||||||
)
|
)
|
||||||
if "%COMPORT%"=="" (
|
if "%COMPORT%"=="" (
|
||||||
echo [ERROR] No ESP32 Board or USB-UART bridge detected!
|
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
|
pause
|
||||||
exit /b 1
|
EOF
|
||||||
)
|
|
||||||
echo [INFO] Detected Board on port: %COMPORT%
|
|
||||||
|
|
||||||
set "FLASH_ARGS="
|
# Substitute address placeholders with configured hex values
|
||||||
if exist "bootloader.bin" (
|
sed -i \
|
||||||
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 \
|
|
||||||
-e "s|__BOOTLOADER_ADDR__|${FLASH_BOOTLOADER_ADDR}|g" \
|
-e "s|__BOOTLOADER_ADDR__|${FLASH_BOOTLOADER_ADDR}|g" \
|
||||||
-e "s|__PARTITIONS_ADDR__|${FLASH_PARTITIONS_ADDR}|g" \
|
-e "s|__PARTITIONS_ADDR__|${FLASH_PARTITIONS_ADDR}|g" \
|
||||||
-e "s|__OTADATA_ADDR__|${FLASH_OTADATA_ADDR}|g" \
|
-e "s|__OTADATA_ADDR__|${FLASH_OTADATA_ADDR}|g" \
|
||||||
@@ -242,27 +253,27 @@ jobs:
|
|||||||
-e "s|__FIRMWARE_ADDR__|${FLASH_FIRMWARE_ADDR}|g" \
|
-e "s|__FIRMWARE_ADDR__|${FLASH_FIRMWARE_ADDR}|g" \
|
||||||
flash_esp32_board.bat
|
flash_esp32_board.bat
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# 3. Gather binaries and scripts into target directory for structural packaging
|
# 3. Gather binaries and scripts into target directory for structural packaging
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
mkdir -p deploy_package
|
mkdir -p deploy_package
|
||||||
BUILD_OUTPUT=".pio/build/ttgo-lora32-v21"
|
BUILD_OUTPUT=".pio/build/ttgo-lora32-v21"
|
||||||
|
|
||||||
# Copy binaries only if they exist
|
# Copy binaries only if they exist
|
||||||
[ -f "$BUILD_OUTPUT/firmware.bin" ] && cp "$BUILD_OUTPUT/firmware.bin" ./deploy_package/
|
[ -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/bootloader.bin" ] && cp "$BUILD_OUTPUT/bootloader.bin" ./deploy_package/
|
||||||
[ -f "$BUILD_OUTPUT/partitions.bin" ] && cp "$BUILD_OUTPUT/partitions.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/otadata.bin" ] && cp "$BUILD_OUTPUT/otadata.bin" ./deploy_package/
|
||||||
[ -f "$BUILD_OUTPUT/safeboot.bin" ] && cp "$BUILD_OUTPUT/safeboot.bin" ./deploy_package/
|
[ -f "$BUILD_OUTPUT/safeboot.bin" ] && cp "$BUILD_OUTPUT/safeboot.bin" ./deploy_package/
|
||||||
|
|
||||||
# Move flash scripts into the deployment directory
|
# Move flash scripts into the deployment directory
|
||||||
mv flash_esp32_board.sh ./deploy_package/
|
mv flash_esp32_board.sh ./deploy_package/
|
||||||
mv flash_esp32_board.bat ./deploy_package/
|
mv flash_esp32_board.bat ./deploy_package/
|
||||||
|
|
||||||
# 9. Dynamic Artifact Upload: FoxMesh-branch-date-hash.zip
|
# 9. Dynamic Artifact Upload: FoxMesh-branch-date-hash.zip
|
||||||
- name: Upload Compiled Firmware Package
|
- name: Upload Compiled Firmware Package
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: FoxMesh-${{ env.GIT_BRANCH }}-${{ env.CURRENT_DATE }}-${{ env.GIT_HASH }}
|
name: FoxMesh-${{ env.GIT_BRANCH }}-${{ env.CURRENT_DATE }}-${{ env.GIT_HASH }}
|
||||||
path: deploy_package/
|
path: deploy_package/
|
||||||
retention-days: 10 # Artifacts will be automatically deleted after 10 days
|
retention-days: 10 # Artifacts will be automatically deleted after 10 days
|
||||||
Binäre Datei nicht angezeigt.
@@ -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.
@@ -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**.
|
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.
|
* **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`.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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
@@ -23,8 +23,13 @@ build_flags =
|
|||||||
-D CCACHE_ENABLE=1
|
-D CCACHE_ENABLE=1
|
||||||
; Treats ESP-IDF libc extensions headers as system headers to suppress external warnings
|
; 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
|
; -isystem /home/tom/.platformio/packages/framework-espidf/components/esp_libc/platform_include
|
||||||
|
-Wno-include-next
|
||||||
; --- STRICT MODE FOR PROJECT APPLICATION CODE ONLY ---
|
; --- 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.
|
; These flags are strictly applied ONLY to C++ files inside the src/ directory.
|
||||||
build_src_flags =
|
build_src_flags =
|
||||||
; Enables all standard compiler warning flags
|
; Enables all standard compiler warning flags
|
||||||
@@ -39,5 +44,8 @@ build_src_flags =
|
|||||||
-Wconversion
|
-Wconversion
|
||||||
; Warns if a local variable shadows another variable in scope
|
; Warns if a local variable shadows another variable in scope
|
||||||
-Wshadow
|
-Wshadow
|
||||||
|
|
||||||
|
-Wno-include-next
|
||||||
; Promotes all warnings to hard compilation errors to guarantee clean code
|
; Promotes all warnings to hard compilation errors to guarantee clean code
|
||||||
;-Werror
|
;-Werror
|
||||||
|
;-D MATH_COMPARE=1
|
||||||
Datei-Diff unterdrückt, da er zu groß ist.
Diff laden
+27
-11
@@ -19,6 +19,7 @@
|
|||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "config.hpp"
|
#include "config.hpp"
|
||||||
#include "lora.hpp"
|
#include "lora.hpp"
|
||||||
|
#include "display.hpp"
|
||||||
|
|
||||||
static const char* TAG = "CLI";
|
static const char* TAG = "CLI";
|
||||||
static i2c* i2c_bus = nullptr;
|
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(" 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(" bw <62|125|250|500>- Set dynamic LoRa bandwidth in kHz\r\n");
|
||||||
printf(" i2c - Scan the configured I2C bus infrastructure\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) {
|
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) {
|
else if (strcmp(cmd, "status") == 0) {
|
||||||
printf("--- %s Status ---\r\n", Config::PROJECT_TITLE);
|
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("Mute Timer Period : %lu seconds\r\n", (unsigned long)current_mute_time_sec);
|
||||||
printf("Target CPU Board : ESP32\r\n");
|
printf("Target CPU Board : ESP32\r\n");
|
||||||
printf("ESP-IDF Version : %s\r\n", esp_get_idf_version());
|
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) {
|
else if (strncmp(cmd, "mute ", 5) == 0) {
|
||||||
int parsed_time = atoi(cmd + 5);
|
int parsed_time = atoi(cmd + 5);
|
||||||
@@ -163,16 +183,7 @@ void process_command(const char* cmd) {
|
|||||||
|
|
||||||
if (valid_433 || valid_868) {
|
if (valid_433 || valid_868) {
|
||||||
if (lora_system != nullptr) {
|
if (lora_system != nullptr) {
|
||||||
// SX1276 formula: Frf = Ffreq * 2^19 / 32000000
|
lora_system->setFrequency(target_freq_hz);
|
||||||
// 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);
|
|
||||||
printf("Success: Frequency shifted to %lu Hz.\r\n", (unsigned long)target_freq_hz);
|
printf("Success: Frequency shifted to %lu Hz.\r\n", (unsigned long)target_freq_hz);
|
||||||
} else {
|
} else {
|
||||||
printf("Error: LoraController handle is uninitialized.\r\n");
|
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");
|
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 {
|
else {
|
||||||
printf("Unknown command: '%s'. Type 'help' for options.\r\n", cmd);
|
printf("Unknown command: '%s'. Type 'help' for options.\r\n", cmd);
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-8
@@ -22,7 +22,7 @@
|
|||||||
namespace Config {
|
namespace Config {
|
||||||
// Compile-time constants with strict type enforcement
|
// Compile-time constants with strict type enforcement
|
||||||
constexpr const char* PROJECT_TITLE = "FoxMesh";
|
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";
|
constexpr const char* PROJECT_AUTHORS = "Tom OE3TKT";
|
||||||
|
|
||||||
// CLI Mute Configuration
|
// CLI Mute Configuration
|
||||||
@@ -45,16 +45,44 @@ namespace Config {
|
|||||||
constexpr gpio_num_t LORA_CS_PIN = GPIO_NUM_18;
|
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_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_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
|
// Semtech SX1276 LoRa Configuration Parameters
|
||||||
// 433.175 MHz is calculated by: 433175000 / (32000000 / 2^19) = 7097139.2 = 0x6c4b33 // wrong: 0x06C4CD
|
// 433.175 MHz is calculated by: 433175000 / (32000000 / 2^19) = 7097139.2 = 0x6c4b33 // wrong: 0x06C4CD
|
||||||
constexpr uint8_t LORA_FRF_MSB = 0x6C;
|
constexpr uint32_t LORA_DEFAULT_FREQ_HZ = 433175000;
|
||||||
constexpr uint8_t LORA_FRF_MID = 0x4B;
|
constexpr uint8_t LORA_FRF_MSB = 0x6C; // @TODO: will be removed in v1.1
|
||||||
constexpr uint8_t LORA_FRF_LSB = 0x33;
|
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
|
// Predefined LoRa Sync Word options for hardware correlation filtering, see docs/lora/device-settings.md
|
||||||
constexpr uint8_t LORA_MODEM_CONFIG_1 = 0x84;
|
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
|
// Active hardware synchronization configuration selector
|
||||||
constexpr uint8_t LORA_MODEM_CONFIG_2 = 0xB4;
|
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);
|
||||||
}
|
}
|
||||||
+227
-60
@@ -116,26 +116,139 @@ static const uint8_t font_5x7[][5] = {
|
|||||||
{0x00, 0x00, 0x77, 0x00, 0x00}, // 124 |
|
{0x00, 0x00, 0x77, 0x00, 0x00}, // 124 |
|
||||||
{0x00, 0x41, 0x36, 0x08, 0x00}, // 125 }
|
{0x00, 0x41, 0x36, 0x08, 0x00}, // 125 }
|
||||||
{0x08, 0x08, 0x2a, 0x1c, 0x08}, // 126 ~
|
{0x08, 0x08, 0x2a, 0x1c, 0x08}, // 126 ~
|
||||||
// German Umlaute mappings
|
{0x00, 0x00, 0x00, 0x00, 0x00}, // 127 DEL (Control character)
|
||||||
// {0x7d, 0x11, 0x11, 0x11, 0x7d}, // 127 Ä
|
|
||||||
// {0x3c, 0x41, 0x41, 0x41, 0x3c}, // 128 Ö
|
// --- UTF-8 0xC2 RANGE (ISO-8859-1 Indizes 128 to 191) ---
|
||||||
// {0x3d, 0x40, 0x40, 0x40, 0x3d}, // 129 Ü
|
{0x00, 0x00, 0x00, 0x00, 0x00}, // 128 Padding (Control character)
|
||||||
// {0x20, 0x55, 0x54, 0x54, 0x78}, // 130 ä
|
{0x00, 0x00, 0x00, 0x00, 0x00}, // 129 Padding (Control character)
|
||||||
// {0x38, 0x54, 0x54, 0x54, 0x38}, // 131 ö
|
{0x00, 0x00, 0x00, 0x00, 0x00}, // 130 Padding (Control character)
|
||||||
// {0x3c, 0x40, 0x40, 0x20, 0x7c}, // 132 ü
|
{0x00, 0x00, 0x00, 0x00, 0x00}, // 131 Padding (Control character)
|
||||||
// {0x7f, 0x49, 0x49, 0x36, 0x20} // 133 ß
|
{0x00, 0x00, 0x00, 0x00, 0x00}, // 132 Padding (Control character)
|
||||||
// new German Umlaute mapping (5x7/5x8 Font Matrix)
|
{0x00, 0x00, 0x00, 0x00, 0x00}, // 133 Padding (Control character)
|
||||||
{0,0,0,0,0}, //127 not allowed
|
{0x00, 0x00, 0x00, 0x00, 0x00}, // 134 Padding (Control character)
|
||||||
{0x7d, 0x13, 0x13, 0x13, 0x7d}, // 128 Ä
|
{0x00, 0x00, 0x00, 0x00, 0x00}, // 135 Padding (Control character)
|
||||||
{0x3c, 0x43, 0x43, 0x43, 0x3c}, // 129 Ö
|
{0x00, 0x00, 0x00, 0x00, 0x00}, // 136 Padding (Control character)
|
||||||
{0x3d, 0x42, 0x42, 0x42, 0x3d}, // 130 Ü
|
{0x00, 0x00, 0x00, 0x00, 0x00}, // 137 Padding (Control character)
|
||||||
{0x20, 0x55, 0x54, 0x54, 0x78}, // 131 ä
|
{0x00, 0x00, 0x00, 0x00, 0x00}, // 138 Padding (Control character)
|
||||||
{0x38, 0x55, 0x55, 0x55, 0x38}, // 132 ö
|
{0x00, 0x00, 0x00, 0x00, 0x00}, // 139 Padding (Control character)
|
||||||
{0x3c, 0x42, 0x42, 0x22, 0x7c}, // 133 ü
|
{0x00, 0x00, 0x00, 0x00, 0x00}, // 140 Padding (Control character)
|
||||||
{0x7f, 0x49, 0x49, 0x36, 0x00}, // 134 ß (Letzte Spalte auf 0x00 bereinigt)
|
{0x00, 0x00, 0x00, 0x00, 0x00}, // 141 Padding (Control character)
|
||||||
// {0x3c, 0x42, 0x51, 0x4a, 0x34} // 134 © Copyright
|
{0x00, 0x00, 0x00, 0x00, 0x00}, // 142 Padding (Control character)
|
||||||
// {0x3c, 0x42, 0x4d, 0x4d, 0x32} // 135
|
{0x00, 0x00, 0x00, 0x00, 0x00}, // 143 Padding (Control character)
|
||||||
{0x3c, 0x42, 0x95, 0x95, 0x52}
|
{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, 0x2f, 0x00, 0x00}, // 161 Inverted exclamation mark (¡)
|
||||||
|
{0x43, 0x4c, 0x78, 0x4c, 0x43}, // 162 Cent sign (¢)
|
||||||
|
{0x18, 0x7c, 0x14, 0x3c, 0x54}, // 163 Pound sign (£)
|
||||||
|
{0x6c, 0x6c, 0x6c, 0x6c, 0x6c}, // 164 Currency sign (¤)
|
||||||
|
{0x30, 0x78, 0x4d, 0x78, 0x30}, // 165 Yen sign (¥)
|
||||||
|
{0x00, 0x00, 0x77, 0x00, 0x00}, // 166 Broken bar (¦)
|
||||||
|
{0x00, 0x4d, 0x4d, 0x6c, 0x00}, // 167 Section sign / Paragraph (§)
|
||||||
|
{0x00, 0x00, 0x01, 0x00, 0x00}, // 168 Diaeresis (¨)
|
||||||
|
{0x00, 0x15, 0x16, 0x7c, 0x14}, // 169 Copyright sign (©)
|
||||||
|
{0x00, 0x42, 0x7b, 0x42, 0x42}, // 170 Feminine ordinal indicator (ª)
|
||||||
|
{0x08, 0x14, 0x2a, 0x14, 0x22}, // 171 Left-pointing double-angle quotation mark («)
|
||||||
|
{0x04, 0x04, 0x04, 0x04, 0x1c}, // 172 Not sign (¬)
|
||||||
|
{0x00, 0x08, 0x08, 0x08, 0x00}, // 173 Soft hyphen
|
||||||
|
{0x00, 0x14, 0x14, 0x14, 0x14}, // 174 Registered sign (®)
|
||||||
|
{0x00, 0x02, 0x02, 0x02, 0x02}, // 175 Macron (¯)
|
||||||
|
{0x00, 0x03, 0x03, 0x00, 0x00}, // 176 Degree sign (°)
|
||||||
|
{0x44, 0x44, 0x5f, 0x44, 0x44}, // 177 Plus-minus sign (±)
|
||||||
|
{0x00, 0x00, 0x07, 0x04, 0x04}, // 178 Superscript two (²)
|
||||||
|
{0x00, 0x00, 0x05, 0x05, 0x07}, // 179 Superscript three (³)
|
||||||
|
{0x02, 0x01, 0x02, 0x04, 0x00}, // 180 Acute accent (´)
|
||||||
|
{0x30, 0x48, 0x30, 0x08, 0x06}, // 181 Micro sign (µ)
|
||||||
|
{0x00, 0x6e, 0x6e, 0x6e, 0x00}, // 182 Pilcrow sign / Paragraph sign (¶)
|
||||||
|
{0x00, 0x00, 0x04, 0x00, 0x00}, // 183 Middle dot (·)
|
||||||
|
{0x00, 0x40, 0x44, 0x40, 0x40}, // 184 Cedilla (¸)
|
||||||
|
{0x00, 0x00, 0x01, 0x00, 0x00}, // 185 Superscript one (¹)
|
||||||
|
{0x00, 0x44, 0x7d, 0x40, 0x00}, // 186 Masculine ordinal indicator (º)
|
||||||
|
{0x22, 0x14, 0x2a, 0x14, 0x08}, // 187 Right-pointing double-angle quotation mark (»)
|
||||||
|
{0x1c, 0x22, 0x49, 0x49, 0x30}, // 188 Vulgar fraction one quarter (¼)
|
||||||
|
{0x38, 0x44, 0x54, 0x24, 0x18}, // 189 Vulgar fraction one half (½)
|
||||||
|
{0x00, 0x00, 0x00, 0x00, 0x00}, // 190 Vulgar fraction three quarters (¾)
|
||||||
|
{0x40, 0x00, 0x45, 0x06, 0x00}, // 191 Inverted question mark (¿)
|
||||||
|
|
||||||
|
// --- UTF-8 0xC3 RANGE (ISO-8859-1 Indizes 192 to 255) ---
|
||||||
|
{0x7c, 0x12, 0x11, 0x12, 0x7c}, // 192 Capital A with grave (À)
|
||||||
|
{0x7c, 0x11, 0x12, 0x14, 0x78}, // 193 Capital A with acute (Á)
|
||||||
|
{0x7c, 0x15, 0x16, 0x14, 0x7c}, // 194 Capital A with circumflex (Â)
|
||||||
|
{0x7c, 0x16, 0x19, 0x16, 0x7c}, // 195 Capital A with tilde (Ã)
|
||||||
|
{0x7c, 0x11, 0x13, 0x11, 0x7c}, // 196 Capital A with umlaut (Ä)
|
||||||
|
{0x7c, 0x11, 0x15, 0x11, 0x7c}, // 197 Capital A with ring above (Å)
|
||||||
|
{0x7e, 0x51, 0x7f, 0x41, 0x73}, // 198 Capital AE ligature (Æ)
|
||||||
|
{0x3e, 0x41, 0x49, 0x22, 0x10}, // 199 Capital C with cedilla (Ç)
|
||||||
|
{0x7f, 0x4a, 0x49, 0x4a, 0x41}, // 200 Capital E with grave (È)
|
||||||
|
{0x7f, 0x49, 0x4a, 0x4c, 0x40}, // 201 Capital E with acute (É)
|
||||||
|
{0x7f, 0x4d, 0x4e, 0x4d, 0x41}, // 202 Capital E with circumflex (Ê)
|
||||||
|
{0x7f, 0x49, 0x4b, 0x49, 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, 0x46, 0x7d, 0x46, 0x00}, // 206 Capital I with circumflex (Î)
|
||||||
|
{0x00, 0x45, 0x7d, 0x45, 0x00}, // 207 Capital I with umlaut (Ï)
|
||||||
|
{0x7f, 0x41, 0x41, 0x22, 0x1c}, // 208 Capital Eth (Ð)
|
||||||
|
{0x7f, 0x05, 0x06, 0x0c, 0x7f}, // 209 Capital N with tilde (Ñ)
|
||||||
|
{0x3e, 0x42, 0x41, 0x42, 0x3e}, // 210 Capital O with grave (Ò)
|
||||||
|
{0x3e, 0x40, 0x41, 0x42, 0x3c}, // 211 Capital O with acute (Ó)
|
||||||
|
{0x3e, 0x44, 0x45, 0x44, 0x3e}, // 212 Capital O with circumflex (Ô)
|
||||||
|
{0x3e, 0x45, 0x46, 0x45, 0x3e}, // 213 Capital O with tilde (Õ)
|
||||||
|
{0x3e, 0x41, 0x43, 0x41, 0x3e}, // 214 Capital O with umlaut (Ö)
|
||||||
|
{0x22, 0x14, 0x08, 0x14, 0x22}, // 215 Multiplication sign (×)
|
||||||
|
{0x2e, 0x41, 0x41, 0x41, 0x3a}, // 216 Capital O with stroke (Ø)
|
||||||
|
{0x3f, 0x42, 0x41, 0x40, 0x3f}, // 217 Capital U with grave (Ù)
|
||||||
|
{0x3f, 0x40, 0x41, 0x42, 0x3e}, // 218 Capital U with acute (Ú)
|
||||||
|
{0x3f, 0x44, 0x45, 0x44, 0x3f}, // 219 Capital U with circumflex (Û)
|
||||||
|
{0x3f, 0x41, 0x43, 0x41, 0x3f}, // 220 Capital U with umlaut (Ü)
|
||||||
|
{0x03, 0x08, 0x70, 0x0c, 0x03}, // 221 Capital Y with acute (Ý)
|
||||||
|
{0x7f, 0x49, 0x09, 0x09, 0x06}, // 222 Capital Thorn (Þ)
|
||||||
|
{0x7f, 0x49, 0x49, 0x36, 0x00}, // 223 Small sharp s / Eszett (ß)
|
||||||
|
{0x20, 0x56, 0x55, 0x56, 0x78}, // 224 Small a with grave (à)
|
||||||
|
{0x20, 0x55, 0x56, 0x58, 0x70}, // 225 Small a with acute (á)
|
||||||
|
{0x20, 0x59, 0x5a, 0x58, 0x78}, // 226 Small a with circumflex (â)
|
||||||
|
{0x20, 0x5a, 0x5d, 0x5a, 0x78}, // 227 Small a with tilde (ã)
|
||||||
|
{0x20, 0x55, 0x57, 0x55, 0x78}, // 228 Small a with umlaut (ä)
|
||||||
|
{0x20, 0x55, 0x59, 0x55, 0x78}, // 229 Small a with ring above (å)
|
||||||
|
{0x24, 0x54, 0x78, 0x54, 0x5c}, // 230 Small ae ligature (æ)
|
||||||
|
{0x38, 0x44, 0x4c, 0x54, 0x24}, // 231 Small c with cedilla (ç)
|
||||||
|
{0x38, 0x56, 0x55, 0x56, 0x18}, // 232 Small e with grave (è)
|
||||||
|
{0x38, 0x55, 0x56, 0x58, 0x10}, // 233 Small e with acute (é)
|
||||||
|
{0x38, 0x59, 0x5a, 0x58, 0x18}, // 234 Small e with circumflex (ê)
|
||||||
|
{0x38, 0x55, 0x57, 0x55, 0x18}, // 235 Small e with umlaut (ë)
|
||||||
|
{0x00, 0x45, 0x7e, 0x40, 0x00}, // 236 Small i with grave (ì)
|
||||||
|
{0x00, 0x40, 0x7e, 0x45, 0x00}, // 237 Small i with acute (í)
|
||||||
|
{0x00, 0x49, 0x7e, 0x49, 0x00}, // 238 Small i with circumflex (î)
|
||||||
|
{0x00, 0x46, 0x7e, 0x46, 0x00}, // 239 Small i with umlaut (ï)
|
||||||
|
{0x38, 0x44, 0x44, 0x48, 0x7f}, // 240 Small eth (ð)
|
||||||
|
{0x7c, 0x09, 0x0a, 0x04, 0x78}, // 241 Small n with tilde (ñ)
|
||||||
|
{0x38, 0x46, 0x45, 0x46, 0x38}, // 242 Small o with grave (ò)
|
||||||
|
{0x38, 0x45, 0x46, 0x48, 0x30}, // 243 Small o with acute (ó)
|
||||||
|
{0x38, 0x4a, 0x4b, 0x4a, 0x38}, // 244 Small o with circumflex (ô)
|
||||||
|
{0x38, 0x4b, 0x4c, 0x4b, 0x38}, // 245 Small o with tilde (õ)
|
||||||
|
{0x38, 0x45, 0x47, 0x45, 0x38}, // 246 Small o with umlaut (ö)
|
||||||
|
{0x08, 0x08, 0x2a, 0x08, 0x08}, // 247 Division sign (÷)
|
||||||
|
{0x38, 0x44, 0x44, 0x44, 0x38}, // 248 Small o with stroke (ø)
|
||||||
|
{0x3c, 0x42, 0x41, 0x20, 0x7c}, // 249 Small u with grave (ù)
|
||||||
|
{0x3c, 0x40, 0x41, 0x22, 0x78}, // 250 Small u with acute (ú)
|
||||||
|
{0x3c, 0x44, 0x45, 0x24, 0x7c}, // 251 Small u with circumflex (û)
|
||||||
|
{0x3c, 0x41, 0x43, 0x21, 0x7c}, // 252 Small u with umlaut (ü)
|
||||||
|
{0x0c, 0x50, 0x52, 0x50, 0x3c}, // 253 Small y with acute (ý)
|
||||||
|
{0x7f, 0x14, 0x14, 0x08, 0x04}, // 254 Small thorn (þ)
|
||||||
|
{0x0c, 0x51, 0x53, 0x51, 0x3c} // 255 Small y with umlaut (ÿ)
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -264,14 +377,16 @@ void DisplayController::startTask(uint32_t priority) {
|
|||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* @brief Safe cross-core interface dispatching a text block onto the internal processing pipeline buffer.
|
* @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 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;
|
if (m_msgQueue == nullptr || text == nullptr) return;
|
||||||
|
|
||||||
DisplayMessage msg = {};
|
DisplayMessage msg = {};
|
||||||
msg.targetLine = line;
|
msg.targetLine = line;
|
||||||
|
msg.startX = (startX > 127) ? 127 : startX; // Clamp to maximum physical display width
|
||||||
|
|
||||||
// Thread-safe copy buffer sanitization sequence
|
// Thread-safe copy buffer sanitization sequence
|
||||||
strncpy(msg.content, text, sizeof(msg.content) - 1);
|
strncpy(msg.content, text, sizeof(msg.content) - 1);
|
||||||
@@ -311,56 +426,73 @@ void DisplayController::run() {
|
|||||||
while (true) {
|
while (true) {
|
||||||
// Asynchronous structural block yield: consumes zero CPU until a new message package arrives
|
// Asynchronous structural block yield: consumes zero CPU until a new message package arrives
|
||||||
if (xQueueReceive(m_msgQueue, &incoming, portMAX_DELAY) == pdTRUE) {
|
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
|
// Extract the Auto-Clear suppression flag from the MSB (Bit 7) and mask the line index
|
||||||
sendCommand(static_cast<uint8_t>(0xB0 + incoming.targetLine)); // Set page address boundary
|
const bool disableAutoClear = incoming.targetLine & 0x80;
|
||||||
sendCommand(0x00); // Set lower column pointer
|
const uint8_t cleanLine = incoming.targetLine & 0x07; // Native 0-7 clamp using bitmask
|
||||||
sendCommand(0x10); // Set higher column pointer
|
|
||||||
uint8_t totalBytesWritten = 0;
|
|
||||||
|
|
||||||
// Basic built-in 5x7 ASCII micro character font generation layer
|
// POSITIONING THE HARDWARE POINTER
|
||||||
// This prevents adding bloating third party render dependencies into your tight stack space
|
sendCommand(static_cast<uint8_t>(0xB0 + cleanLine)); // Set page address boundary
|
||||||
size_t textLength = strlen(incoming.content);
|
|
||||||
|
// 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++) {
|
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;
|
size_t fontIndex = 0;
|
||||||
bool validCharacter = false;
|
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
|
// Multi-byte UTF-8 decoder targeting the complete ISO-8859-1 (Latin-1) spectrum
|
||||||
if (c_primary == 0xC3 && (i + 1) < textLength) {
|
if ((c_primary == 0xC3 || c_primary == 0xC2) && (i + 1) < textLength) {
|
||||||
uint8_t c_secondary = static_cast<uint8_t>(incoming.content[i + 1]);
|
const uint8_t c_secondary = static_cast<uint8_t>(incoming.content[++i]);
|
||||||
i++; // Advance the pointer to consume the compound UTF-8 token pair
|
|
||||||
|
|
||||||
if (c_secondary == 0x84) { fontIndex = 96; validCharacter = true; } // Ä
|
// Mathematical reconstruction of the raw Latin-1 byte position
|
||||||
if (c_secondary == 0x96) { fontIndex = 97; validCharacter = true; } // Ö
|
// 0xC3 sequence maps to 0xC0-0xFF (c_secondary + 0x40)
|
||||||
if (c_secondary == 0x9C) { fontIndex = 98; validCharacter = true; } // Ü
|
// 0xC2 sequence maps directly to 0x80-0xBF
|
||||||
if (c_secondary == 0xA4) { fontIndex = 99; validCharacter = true; } // ä
|
uint8_t iso_char = (c_primary == 0xC3) ? (c_secondary + 0x40) : c_secondary;
|
||||||
if (c_secondary == 0xB6) { fontIndex = 100; validCharacter = true; } // ö
|
|
||||||
if (c_secondary == 0xBC) { fontIndex = 101; validCharacter = true; } // ü
|
// 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) {
|
// Handle standard printable 7-bit ASCII characters
|
||||||
uint8_t c_secondary = static_cast<uint8_t>(incoming.content[i + 1]);
|
else if (c_primary >= 32 && c_primary <= 126) {
|
||||||
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)) {
|
|
||||||
fontIndex = static_cast<size_t>(c_primary - 32);
|
fontIndex = static_cast<size_t>(c_primary - 32);
|
||||||
validCharacter = true;
|
validCharacter = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validCharacter && totalBytesWritten < 122) {
|
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[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];
|
// Check if current character is a potential line segment
|
||||||
txBuffer[2] = font_5x7[fontIndex][1];
|
if (c_primary == '-' || c_primary == '_' || c_primary == '=') {
|
||||||
txBuffer[3] = font_5x7[fontIndex][2];
|
// Look behind: Direct boolean evaluation in a single line
|
||||||
txBuffer[4] = font_5x7[fontIndex][3];
|
bool connect = (current_char_idx > 0 && incoming.content[current_char_idx - 1] == static_cast<char>(c_primary));
|
||||||
txBuffer[5] = font_5x7[fontIndex][4];
|
|
||||||
txBuffer[6] = 0x00; // 1-pixel spacing between characters
|
// 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) {
|
if (m_devHandle != nullptr) {
|
||||||
i2c_master_transmit(m_devHandle, txBuffer, sizeof(txBuffer), 50);
|
i2c_master_transmit(m_devHandle, txBuffer, sizeof(txBuffer), 50);
|
||||||
@@ -369,14 +501,13 @@ void DisplayController::run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AUTO-CLEAR: Fill the remaining horizontal page width with blank pixels to flush out trailing leftovers
|
// AUTO-CLEAR: Fill the remaining horizontal page width with blank pixels only if flag is NOT active
|
||||||
if (totalBytesWritten < 128) {
|
if (!disableAutoClear && totalBytesWritten < 128) {
|
||||||
uint8_t remainingBytes = static_cast<uint8_t>(128 - totalBytesWritten);
|
const uint8_t remainingBytes = static_cast<uint8_t>(128 - totalBytesWritten);
|
||||||
|
|
||||||
// Allocate a burst buffer with dynamic sizing plus control byte room
|
// Allocate a burst buffer with dynamic sizing plus control byte room
|
||||||
uint8_t flushBuffer[129] = {0};
|
uint8_t flushBuffer[129] = {0};
|
||||||
flushBuffer[0] = 0x40; // Data mode identifier
|
flushBuffer[0] = 0x40; // Data mode identifier
|
||||||
|
|
||||||
if (m_devHandle != nullptr) {
|
if (m_devHandle != nullptr) {
|
||||||
// Send out the precise amount of zeros required to wipe the right margin clean
|
// 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);
|
i2c_master_transmit(m_devHandle, flushBuffer, static_cast<size_t>(remainingBytes + 1), 50);
|
||||||
@@ -385,3 +516,39 @@ 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, "=== 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));
|
||||||
|
// 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
@@ -33,13 +33,27 @@ public:
|
|||||||
* @brief Thread-safe non-blocking method to print a message to the OLED display.
|
* @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 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 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:
|
private:
|
||||||
// Internal structure mapping the safe asynchronous data pipeline
|
// Internal structure mapping the safe asynchronous data pipeline
|
||||||
struct DisplayMessage {
|
struct DisplayMessage {
|
||||||
uint8_t targetLine;
|
uint8_t targetLine;
|
||||||
|
uint8_t startX;
|
||||||
char content[24];
|
char content[24];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,3 +68,10 @@ private:
|
|||||||
esp_err_t sendCommand(uint8_t cmd) const;
|
esp_err_t sendCommand(uint8_t cmd) const;
|
||||||
esp_err_t initializeHardware();
|
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
@@ -16,10 +16,16 @@
|
|||||||
|
|
||||||
const char* LoraController::TAG = "LORA";
|
const char* LoraController::TAG = "LORA";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Constructor initializing core FreeRTOS synchronization primitives.
|
||||||
|
*/
|
||||||
LoraController::LoraController() {
|
LoraController::LoraController() {
|
||||||
m_isrSemaphore = xSemaphoreCreateBinary();
|
m_isrSemaphore = xSemaphoreCreateBinary();
|
||||||
if (m_isrSemaphore == nullptr) {
|
m_stateMutex = xSemaphoreCreateMutex();
|
||||||
ESP_LOGE(TAG, "Fatal: Failed to allocate LoRa ISR synchronization semaphore.");
|
|
||||||
|
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) {
|
if (m_isrSemaphore != nullptr) {
|
||||||
vSemaphoreDelete(m_isrSemaphore);
|
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) {
|
uint8_t LoraController::readRegister(uint8_t address) {
|
||||||
if (m_spiHandle == nullptr) return 0;
|
if (m_spiHandle == nullptr) return 0;
|
||||||
|
|
||||||
@@ -49,6 +63,11 @@ uint8_t LoraController::readRegister(uint8_t address) {
|
|||||||
return rxData[1];
|
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) {
|
void LoraController::writeRegister(uint8_t address, uint8_t value) {
|
||||||
if (m_spiHandle == nullptr) return;
|
if (m_spiHandle == nullptr) return;
|
||||||
|
|
||||||
@@ -76,6 +95,10 @@ void IRAM_ATTR LoraController::gpioIsrHandler(void* arg) {
|
|||||||
}
|
}
|
||||||
#pragma GCC diagnostic pop
|
#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 LoraController::initializeHardware() {
|
||||||
esp_err_t err;
|
esp_err_t err;
|
||||||
|
|
||||||
@@ -110,48 +133,161 @@ esp_err_t LoraController::initializeHardware() {
|
|||||||
}
|
}
|
||||||
ESP_LOGI(TAG, "SX1276 Handshake success. Silicon version confirmed: 0x12");
|
ESP_LOGI(TAG, "SX1276 Handshake success. Silicon version confirmed: 0x12");
|
||||||
|
|
||||||
|
// Invoke the modular modem configuration
|
||||||
|
applyModemConfig();
|
||||||
|
|
||||||
err = gpio_install_isr_service(0);
|
err = gpio_install_isr_service(0);
|
||||||
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Configure interrupt triggering on the rising edge (PosEdge)
|
||||||
err = gpio_set_intr_type(Config::LORA_IRQ_PIN, GPIO_INTR_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);
|
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() {
|
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));
|
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(0x06, Config::LORA_FRF_MSB);
|
||||||
writeRegister(0x07, Config::LORA_FRF_MID);
|
writeRegister(0x07, Config::LORA_FRF_MID);
|
||||||
writeRegister(0x08, Config::LORA_FRF_LSB);
|
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
|
// just write the m_state frequency....
|
||||||
writeRegister(0x01, 0x85U); // RXCONTINUOUS
|
m_state.frequencyHz = Config::LORA_DEFAULT_FREQ_HZ; // @TODO: remove this line after implementation of the doubly linked list
|
||||||
ESP_LOGI(TAG, "Radio configured: 433.175 MHz | BW 250kHz | SF11 | CR 4/6");
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
writeRegister(0x01, 0x81U);
|
||||||
vTaskDelay(pdMS_TO_TICKS(5));
|
vTaskDelay(pdMS_TO_TICKS(5));
|
||||||
|
|
||||||
writeRegister(0x06, msb);
|
ESP_LOGI(TAG, "Radio profile applied: 433.175 MHz | SF11 | BW 250kHz | SyncWord 0x34");
|
||||||
writeRegister(0x07, mid);
|
}
|
||||||
writeRegister(0x08, lsb);
|
|
||||||
|
|
||||||
// Re-engage Continous Reception Mode
|
|
||||||
writeRegister(0x01, 0x85U);
|
#ifndef MATH_COMPARE
|
||||||
ESP_LOGI(TAG, "Frequency dynamically shifted to: 0x%02X%02X%02X", msb, mid, lsb);
|
#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) {
|
void LoraController::setBandwidth(uint32_t bw_khz) {
|
||||||
@@ -204,7 +340,51 @@ void LoraController::taskTrampoline(void* pvParameters) {
|
|||||||
vTaskDelete(nullptr);
|
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() {
|
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) {
|
if (initializeHardware() != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Critical: LoRa init failed. Thread aborted.");
|
ESP_LOGE(TAG, "Critical: LoRa init failed. Thread aborted.");
|
||||||
return;
|
return;
|
||||||
@@ -212,15 +392,19 @@ void LoraController::run() {
|
|||||||
|
|
||||||
setModeReceiver();
|
setModeReceiver();
|
||||||
extern DisplayController* display; // Reference the global display singleton
|
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");
|
ESP_LOGI(TAG, "LoRa RX was initialized and is running");
|
||||||
|
|
||||||
while (true) {
|
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);
|
uint32_t irqFlags = readRegister(0x12);
|
||||||
writeRegister(0x12, static_cast<uint8_t>(irqFlags));
|
writeRegister(0x12, static_cast<uint8_t>(irqFlags));
|
||||||
|
|
||||||
if ((irqFlags & 0x40U) != 0U) { // RxDone Flag
|
if ((irqFlags & 0x40U) != 0U) { // RxDone Flag
|
||||||
|
packetCounter++;
|
||||||
LoraPacket packet = {};
|
LoraPacket packet = {};
|
||||||
packet.length = readRegister(0x13);
|
packet.length = readRegister(0x13);
|
||||||
|
|
||||||
@@ -231,38 +415,74 @@ void LoraController::run() {
|
|||||||
packet.payload[i] = readRegister(0x00);
|
packet.payload[i] = readRegister(0x00);
|
||||||
}
|
}
|
||||||
// Calculate the telemetry data
|
// Calculate the telemetry data
|
||||||
packet.rssi = static_cast<int8_t>(static_cast<int>(readRegister(0x1A)) - 157);
|
// 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.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("\r\n--------------------------------------------------\r\n");
|
||||||
printf("[LoRa RX] Paket erfolgreich empfangen!\r\n");
|
printf("[LoRa RX] Packet #%lu successfully received!\r\n", (unsigned long)packetCounter);
|
||||||
printf(" Laenge: %u Bytes\r\n", packet.length);
|
// printf(" Database ID : %lu\r\n", (unsigned long)nodeDbId);
|
||||||
printf(" RSSI : %d dBm (Signalstaerke)\r\n", packet.rssi);
|
printf(" Length : %u Bytes\r\n", packet.length);
|
||||||
printf(" SNR : %d dB (Rauschabstand)\r\n", packet.snr);
|
printf(" RSSI : %.1f dBm (Signal Strength)\r\n", packet.rssi);
|
||||||
//ESP_LOGI(TAG, "Incoming frame packet received! Size: %u bytes, RSSI: %d dBm, SNR: %d dB",
|
printf(" SNR : %.1f dB (Signal-to-Noise Ratio)\r\n", packet.snr);
|
||||||
// packet.length, packet.rssi, packet.snr);
|
printf(" Content: \"");
|
||||||
|
|
||||||
// ASCII-Klartext-Ausgabe für das Terminal formatiert
|
|
||||||
printf(" Inhalt: \"");
|
|
||||||
for (uint8_t i = 0; i < packet.length; i++) {
|
for (uint8_t i = 0; i < packet.length; i++) {
|
||||||
char c = static_cast<char>(packet.payload[i]);
|
char c = static_cast<char>(packet.payload[i]);
|
||||||
// Nur druckbare Zeichen ausgeben, um das Terminal nicht zu verwirren
|
// Only print printable characters to avoid confusing the terminal
|
||||||
if (c >= 32 && c <= 126) {
|
if (c >= 32 && c <= 126)
|
||||||
printf("%c", c);
|
printf("%c", c);
|
||||||
} else {
|
else
|
||||||
printf("."); // Punkt für nicht-druckbare Binärbytes
|
printf("~"); // not printable chars
|
||||||
}
|
|
||||||
}
|
}
|
||||||
printf("\"\r\n--------------------------------------------------\r\n");
|
printf("\"\r\n Hex : ");
|
||||||
printf("%s # ", Config::PROJECT_TITLE); // CLI-Prompt wiederherstellen
|
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);
|
fflush(stdout);
|
||||||
|
|
||||||
// FIXED: Uses the new parameter order (line first, text second)
|
// FIXED: Uses the new parameter order (line first, text second)
|
||||||
if (display != nullptr) {
|
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
|
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
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* @file lora.hpp
|
* @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)
|
* @author OM Tom (OE3TKT)
|
||||||
*
|
*
|
||||||
* @license SPDX-License-Identifier: MIT
|
* @license SPDX-License-Identifier: MIT
|
||||||
@@ -15,19 +15,29 @@
|
|||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/semphr.h"
|
#include "freertos/semphr.h"
|
||||||
#include "freertos/task.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 {
|
class LoraController {
|
||||||
public:
|
public:
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Structure defining the localized packet buffer space.
|
* @brief Simple internal abstraction layer mirroring the raw incoming packet payload.
|
||||||
*/
|
*/
|
||||||
struct LoraPacket {
|
struct LoraPacket {
|
||||||
uint8_t payload[256]; ///< Raw data payload frame matching max SX1276 FIFO allocation
|
float rssi; /**< High-precision calibrated received signal power in dBm. */
|
||||||
uint8_t length; ///< Total number of active bytes captured inside the frame
|
float snr; /**< High-precision calibrated signal-to-noise ratio in dB. */
|
||||||
int8_t rssi; ///< Received Signal Strength Indicator metric in dBm
|
uint8_t length; /**< Payload size explicitly specified in bytes. */
|
||||||
int8_t snr; ///< Signal-to-Noise Ratio metric in dB
|
uint8_t payload[256]; /**< Fixed internal memory buffer capturing raw ingress bytes. Max SX1276 FIFO is 256. */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
explicit LoraController();
|
explicit LoraController();
|
||||||
virtual ~LoraController();
|
virtual ~LoraController();
|
||||||
|
|
||||||
@@ -55,7 +65,13 @@ public:
|
|||||||
* @param mid Middle Significant Byte of the frequency fraction.
|
* @param mid Middle Significant Byte of the frequency fraction.
|
||||||
* @param lsb Least 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.
|
* @brief Dynamically updates the LoRa signal bandwidth at runtime.
|
||||||
@@ -63,12 +79,27 @@ public:
|
|||||||
*/
|
*/
|
||||||
void setBandwidth(uint32_t bw_khz);
|
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:
|
private:
|
||||||
|
static const char* TAG;
|
||||||
|
|
||||||
spi_device_handle_t m_spiHandle = nullptr;
|
spi_device_handle_t m_spiHandle = nullptr;
|
||||||
TaskHandle_t m_taskHandle = nullptr;
|
TaskHandle_t m_taskHandle = nullptr;
|
||||||
SemaphoreHandle_t m_isrSemaphore = 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 taskTrampoline(void* pvParameters);
|
||||||
static void IRAM_ATTR gpioIsrHandler(void* arg);
|
static void IRAM_ATTR gpioIsrHandler(void* arg);
|
||||||
@@ -79,4 +110,20 @@ private:
|
|||||||
|
|
||||||
esp_err_t initializeHardware();
|
esp_err_t initializeHardware();
|
||||||
void setModeReceiver();
|
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);
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -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
@@ -8,6 +8,10 @@
|
|||||||
* For the full license terms, see the LICENSE file in the project root.
|
* 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 <stdio.h>
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
@@ -127,10 +131,10 @@ extern "C" {
|
|||||||
// Trigger synchronous welcome splash screen frame
|
// Trigger synchronous welcome splash screen frame
|
||||||
if (display != nullptr) {
|
if (display != nullptr) {
|
||||||
char msg[32];
|
char msg[32];
|
||||||
//display->printMessage(0, "FoxMesh " Config::PROJECT_VERSION " \x86" "OE3TKT");
|
snprintf(msg, sizeof(msg), "FoxMesh " GIT_TAG);
|
||||||
snprintf(msg, sizeof(msg), "FoxMesh %s OE3TKT", Config::PROJECT_VERSION);
|
display->printMessage(0, msg);
|
||||||
display->printMessage(1, msg);
|
// display->appendMessage(0, GIT_TAG, 12*6);
|
||||||
//display->printMessage(2, "Bärbel frühstückt ©");
|
display->printMessage(1, "----------------------");
|
||||||
}
|
}
|
||||||
// Main system watchdog and status monitor loop
|
// Main system watchdog and status monitor loop
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -138,6 +142,7 @@ extern "C" {
|
|||||||
Config::PROJECT_TITLE,
|
Config::PROJECT_TITLE,
|
||||||
Config::PROJECT_VERSION,
|
Config::PROJECT_VERSION,
|
||||||
(unsigned long)esp_get_free_heap_size());
|
(unsigned long)esp_get_free_heap_size());
|
||||||
|
// ESP_LOGI(TAG, "Git-Hash: %s", GIT_HASH);
|
||||||
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||||
}
|
}
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren