Commits vergleichen

..
16 Commits
Autor SHA1 Nachricht Datum
tom 1b6bb7ef32 .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Successful in 6s
Build of FoxMesh / build-and-test (push) Successful in 2m28s
2026-10-22 23:53:34 +02:00
tom ea9639bca0 .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Canceled after 4s
Build of FoxMesh / build-and-test (push) Canceled after 11s
2026-10-22 23:42:39 +02:00
tom 807621b92c .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Failing after 2s
Build of FoxMesh / build-and-test (push) Canceled after 2m14s
2026-10-22 23:40:56 +02:00
tom 7c19f0663b .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Failing after 3s
Build of FoxMesh / build-and-test (push) Canceled after 11s
2026-10-22 23:38:30 +02:00
tom 5f035381f1 .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Failing after 3s
Build of FoxMesh / build-and-test (push) Successful in 2m25s
2026-10-22 23:21:08 +02:00
tom ca09806376 .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Failing after 3s
Build of FoxMesh / build-and-test (push) Canceled after 11s
2026-10-22 23:07:58 +02:00
tom 326b602a99 .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Failing after 3s
Build of FoxMesh / build-and-test (push) Successful in 2m25s
2026-10-22 23:01:42 +02:00
tom a7e356ce04 .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Failing after 3s
Build of FoxMesh / build-and-test (push) Successful in 2m26s
2026-10-22 22:46:01 +02:00
tom 2f67c1df97 typo: foxmesh-protocol
Build and Release Typst PDF / build-pdf (push) Failing after 3s
Build of FoxMesh / build-and-test (push) Successful in 2m25s
2026-10-22 22:35:58 +02:00
tom 6b4f711c40 .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Failing after 3s
Build of FoxMesh / build-and-test (push) Successful in 2m25s
2026-10-22 22:35:02 +02:00
tom 3943d3de14 fix: retension-days 10 -- for artifacts in foxmesh-ci.yml
Build and Release Typst PDF / build-pdf (push) Failing after 4s
Build of FoxMesh / build-and-test (push) Successful in 2m27s
2026-10-22 21:53:19 +02:00
tom bdcb07c374 .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Failing after 2s
Build of FoxMesh / build-and-test (push) Successful in 2m31s
2026-10-22 02:35:10 +02:00
tom b9001572cd .gitea/workflows/build-pdf.yml aktualisiert
Build and Release Typst PDF / build-pdf (push) Failing after 2s
Build of FoxMesh / build-and-test (push) Failing after 11s
2026-10-22 02:28:08 +02:00
tom 21c3dc8771 remove generated pdf files
Build and Release Typst PDF / build-pdf (push) Failing after 1s
Build of FoxMesh / build-and-test (push) Failing after 2m29s
2026-10-22 02:13:06 +02:00
tom 652b794aae Merge pull request 'doc: foxmash protocol in typst, display chars' (#6) from docs/lora-protocol into main
Build and Release Typst PDF / build-pdf (push) Failing after 1s
Build of FoxMesh / build-and-test (push) Successful in 2m35s
Reviewed-on: #6
2026-10-22 01:51:11 +02:00
tom acd7d316bc doc: foxmash protocol in typst, display chars
Build of FoxMesh / build-and-test (pull_request) Successful in 2m41s
2026-10-22 00:27:17 +02:00
10 geänderte Dateien mit 840 neuen und 93 gelöschten Zeilen

Keine Dateien entsprechen Ihrer Suche

+62
Datei anzeigen
@@ -0,0 +1,62 @@
name: Build and Release Typst PDF
on:
push:
branches:
- main
tags:
- 'v*' # Triggers when a release tag like v1.0.0 is pushed
workflow_dispatch: # Enables manual start in the Web interface
jobs:
build-pdf:
runs-on: host
steps:
# 1. Checkout the source code from Git
- name: Checkout Repository
uses: actions/checkout@v4
# 2. Download and extract the latest Typst Binary (No installation required)
- name: Download Latest Typst
run: |
cd docs/lora/
XZ_FILE_URL=$(curl -s https://api.github.com/repos/typst/typst/releases/latest | jq -r '.assets[] | select(.name | endswith("x86_64-unknown-linux-musl.tar.xz")) | .browser_download_url')
echo "$XZ_FILE_URL"
curl -L "$XZ_FILE_URL" | tar -xJ --strip-components=1
chmod +x ./typst
# 3. Compile the PDF using the local binary
- name: Compile Typst Documents
run: |
cd docs/lora/
ls -la
./typst compile foxmesh-protocol.typ FoxMesh_Protocol.pdf
./typst compile foxmesh-protocol_de.typ FoxMesh_Protocol_de.pdf
# 3. Upload PDF as a Gitea artifact (runs on every push to main)
- name: Upload PDF Artifact
uses: actions/upload-artifact@v7
with:
name: foxmesh-protocol-pdf
path: docs/lora/FoxMesh_Protocol.pdf
retention-days: 14
archive: false
- name: Upload PDF Artifact de
uses: actions/upload-artifact@v7
with:
name: foxmesh-protocol-de-pdf
path: docs/lora/FoxMesh_Protocol_de.pdf
retention-days: 14
archive: false
# 4. Optional: PDF direkt an ein Gitea-Release anhängen (wenn ein Tag gepusht wird)
# - name: Upload to Gitea Release
# if: startsWith(github.ref, 'refs/tags/')
# uses: svenstaro/upload-release-action@v2
# with:
# repo_token: ${{ secrets.GITHUB_TOKEN }}
# file: FoxMesh_Engineering_Manual.pdf
# asset_name: FoxMesh_Engineering_Manual.pdf
# tag: ${{ github.ref }}
# overwrite: true
+3 -1
Datei anzeigen
@@ -5,6 +5,8 @@ on:
branches: [ main ] branches: [ main ]
pull_request: pull_request:
branches: [ main ] # Trigger the workflow on pull request events targeting the main branch branches: [ main ] # Trigger the workflow on pull request events targeting the main branch
tags:
- 'v*' # Triggers when a release tag like v1.0.0 is pushed
workflow_dispatch: workflow_dispatch:
inputs: inputs:
target_branch: target_branch:
@@ -276,4 +278,4 @@ jobs:
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
+3
Datei anzeigen
@@ -22,3 +22,6 @@ sdkconfig.*
# --- Log files --- # --- Log files ---
build_log.txt build_log.txt
logs/ logs/
# PDF files generated
*.pdf
!docs/lora/aprs*.pdf
+3 -2
Datei anzeigen
@@ -28,8 +28,9 @@ The firmware is designed to target popular LoRa development ecosystems, with pri
## 🌐 Project Mirrors / Hosting ## 🌐 Project Mirrors / Hosting
To ensure high availability and decentralization, this project is maintained across multiple platforms: To ensure high availability and decentralization, this project is maintained across multiple platforms:
* **Primary Repo (GitHub):** [github.com/tkuschel/FoxMesh](https://github.com/tkuschel/FoxMesh)
* **Self-Hosted Mirror (Gitea):** [git.kuschel.at/tom/FoxMesh](https://git.kuschel.at/tom/FoxMesh) * **Primary Self-Hosted Repo (Gitea):** [git.kuschel.at/tom/FoxMesh](https://git.kuschel.at/tom/FoxMesh)
* **Mirror Repo (GitHub):** [github.com/tkuschel/FoxMesh](https://github.com/tkuschel/FoxMesh)
*Note: Both repositories are automatically kept synchronized. You can clone, open issues, or contribute on either platform.* *Note: Both repositories are automatically kept synchronized. You can clone, open issues, or contribute on either platform.*
+28
Datei anzeigen
@@ -106,3 +106,31 @@ All public APIs, public structures, and method signatures within header files mu
*/ */
void updateProfile(const BlinkProfile& profile); void updateProfile(const BlinkProfile& profile);
``` ```
## 8. Git Branching and Commit Standards
To ensure traceability and maintain a clean repository history, FoxMesh strictly follows a standardized branching and commit convention based on *Conventional Commits*.
### 8.1 Branch Naming Convention
Developers must prefix every newly created Git branch with a specific scope identifier followed by a slash (`/`), the Gitea issue number, and a short description.
* **Format:** `<type>/<issue-number>-<short-description>`
* **Example:** `docs/123-readme-update` or `feat/45-lora-mesh-ack`
### 8.2 Scope Identifiers
The following prefixes are permitted for branch creation and commit messages:
* `feat/` (or `feature/`): Introduction of new hardware or software functionalities.
* `fix/` (or `bugfix/`): Resolution of bugs, runtime stability issues, or crashes.
* `docs/`: Exclusively reserved for pure documentation updates (Readmes, Doxygen, Wiki).
* `refactor/`: Code reorganization or optimization without changing external behavior.
* `chore/`: Updates to the build system, `platformio.ini`, CI/CD pipelines, or external dependencies.
### 8.3 Commit and Issue Automation
Every commit message should start with the lowercase prefix (e.g., `docs: description`) and must include the targeted Gitea issue number in parentheses at the end to allow automated tracking and timeline linkage:
```bash
git commit -m "docs: added docker installation guide (#123)"
```
When submitting a Pull Request (PR) to the `main` branch, developers must use standard closure keywords (`closes #123`, `resolves #123`) within the PR description to ensure Gitea automatically resolves the ticket upon integration.
Datei ohne Änderungen umbenannt.
+318
Datei anzeigen
@@ -0,0 +1,318 @@
#set page(
paper: "a4",
margin: (x: 2cm, top: 3.5cm, bottom: 3cm),
// Kopfzeile mit dynamischem Platzhalter
header: align(right)[
#text(size: 8pt, fill: luma(120))[FoxMash Protocol v0.1]
]
)
#set text(font: "Libertinus Serif", lang: "en", size: 10.5pt)
#set par(justify: true)
// Styling-Funktion für Tabellen-Header
#let table-header(..cells) = {
cells.pos().map(cell => text(weight: "bold", fill: rgb("1a1a1a"))[#cell])
}
// ==========================================
// DYNAMISCHES HEADER-LOGO (Fuchs + ARDF-Schirm)
// ==========================================
#place(top + left, dy: -2.5cm)[
#grid(
columns: (60pt, 1fr),
gutter: 15pt,
align: horizon,
// Der ARDF-Postenschirm (Orange/Weißes Dreieck) gepaart mit einer Fuchs-Farbe
stack(
spacing: 2pt,
rect(width: 50pt, height: 50pt, stroke: 1pt + rgb("1a1a1a"), fill: white)[
#place(top + left)[
#polygon(
fill: rgb("ff5500"), // ARDF Orange
(0pt, 50pt),
(50pt, 0pt),
(50pt, 50pt)
)
]
// Stilisierte Fuchs-Silhouette (Ohren und Schnauze) ohne Kurven-Fehler
#place(center + horizon, dy: 2pt)[
#polygon(
fill: rgb("1a1a1a"),
(0pt, 0pt), // Linkes Ohr Spitze
(6pt, 8pt), // Kopf Mitte oben
(12pt, 0pt), // Rechtes Ohr Spitze
(12pt, 10pt), // Rechte Wange
(6pt, 18pt), // Schnauze unten
(0pt, 10pt) // Linke Wange
)
]
]
),
[
#text(size: 24pt, weight: "bold", tracking: 1pt, fill: rgb("1a1a1a"))[FoxMash] \
#v(-8pt)
#text(size: 10pt, weight: "medium", fill: rgb("ff5500"))[Amateur Radio Direction Finding Mesh Protocol]
]
)
]
#v(1cm)
= Protocol
English version. In case of doubt, the English text shall be binding. \
#link("foxmash-protocol_de.pdf")[_Deutsche Version anzeigen_]
== Automatic Stations at the ARDF Transmitting Facilities (Foxes)
A fox regularly broadcasts its identifier and telemetry data.
This is done, whenever possible, at times when the fox is not transmitting on the 80m or 2m bands.
The purpose of this is, on the one hand, the time synchronization of adjacent nodes, and on the other hand, the regular official announcement of the call sign.
Usually, the call sign of the organizer or host of the direction-finding competition is used.
== Beacon
Composed based on the modified AX.25, APRS, and MeshCom protocols as follows:
Pseudo-Byte-Stream:
```text
{OE3TKT-03CpnnnnxRSqxRSqxRSq..
```
=== Structure
#table(
columns: (1.5fr, 2fr, 5fr),
align: (center, left, left),
stroke: 0.5pt + luma(150),
fill: (x, y) => if y == 0 { rgb("f0f0f0") } else { none },
..table-header("Byte / Offset", "Field / Format", "Description"),
[Byte 0], [`"{"` `0x7B`], [
*Data Type Identifier (DTI):* Derived from the APRS & MeshCom protocol specification (see #link("https://ui-view.net")[APRS Protocol Reference]). \
*Reasoning:* Currently marked as `[Unused]` in the specification; character visually resembles a station icon.
],
[Bytes 1–6], [`"OE3TKT"` \ (6 Bytes ASCII)], [
*International Call Sign:* Formatted as a 1–2 character prefix, followed by one or more digits, and a 1–3 letter suffix. Strictly 6 characters wide, right-padded with trailing spaces if shorter.
],
[Byte 7], [`"-"` `0x2D` or `"~"` `0x7E`], [
*Node Type Separator:* Hyphen for standard nodes, tilde for backup/hot-standby nodes. Serves as a visual delimiter and structural anchor for the parser.
],
[Bytes 8–9], [`"03"`, or `"S6"` \ (2 Bytes ASCII)], [
*Station SSID Identifier:* Sub-string format representing the node ID. Supports numeric IDs (0–99) or extended notation (e.g., `"S6"`) i.e. any letters as described in #link("https://aprs11/SSIDs.txt")[aprs11/SSIDs.txt] (e.g., `OE3TKT-S6` for any special 6th station).
],
[Byte 10], [Own Battery Level \ (`unsigned char`)], [
*Value:* Capacity in percent (0 to 100%). Values above 100 indicate charging or mains power.
],
[Byte 11], [Protocol / Net Flags \ (`unsigned char`)], [
*Network Flags / Padding:* Reserved for mesh network flags and hop control. Ensures precise 4-byte memory alignment for the subsequent 32-bit timestamp.
],
[Bytes 12–15], [`nnnn` / \ Timestamp \ (`int32_t`, Little-Endian)], [
*Timestamp:* Signed 32-bit integer representing the number of milliseconds since the start of the competition day. Formatted as Little-Endian (LSB first) and aligned to a 4-byte boundary for optimized, native ESP32 pointer access. All official ARDF contest times and logs must be recorded in Coordinated Universal Time (UTC). To convert _[Local Time Name]_ to UTC, subtract/add _[X]_ hours (UTC = _[Local Time]_ +/- _[X]_h). Please pay close attention to date changes for events crossing midnight UTC.
],
[Bytes 15+], [Heard Neighbor Nodes \ (`xRSqxRSqxRSq...`)], [
Multi-byte blocks containing mesh neighbor details (SSID, link quality, battery status). See detailed breakdown below.
]
)
==== Heard Neighbor Nodes Block Breakdown (Bytes 15+)
#table(
columns: (1.5fr, 2fr, 5fr),
align: (center, left, left),
stroke: 0.5pt + luma(150),
fill: (x, y) => if y == 0 { rgb("f0f0f0") } else { none },
..table-header("Byte Offset", "Field / Format", "Description"),
[Bytes 15–16], [Neighbor SSID \ (2 Bytes ASCII)], [Two ASCII characters representing the neighbor's station ID/SSID (e.g., `"03"` or `"S6"`). This matches the exact sub-string format from Bytes 8–9 of the source node for easy string matching.],
[Byte 17], [RSSI Value (`R`) \ (`unsigned char`)], [Stored as RSSI + 164 (e.g., `0x3A` = 58 -> 58 - 164 = -106 dBm).],
[Byte 18], [SNR Value (`S`) \ (`signed char`)], [Stored as SNR multiplied by 4 (e.g., `0x42` = 66 -> 66 / 4 = +16.5 dB).],
[Byte 19], [Status Byte (`q`) \ (`unsigned char`)], [Combined Quality and Battery Status Byte. (See Bit-mapping below).]
)
#v(0.5em)
*Byte 19 (`q`) Bit-mapping:*
#table(
columns: (1.5fr, 1.5fr, 6fr),
align: (center, center, left),
stroke: 0.5pt + luma(150),
fill: (x, y) => if y == 0 { rgb("f5f5f5") } else { none },
..table-header("Bits", "Value", "Definition"),
[Bits 7–6], [`00`], [Normal (Battery OK / Powered)],
[Bits 7–6], [`01`], [Low (Battery low, intervention recommended)],
[Bits 7–6], [`10`], [Critical (Battery critical, node might shut down soon)],
[Bits 7–6], [`11`], [Reserved],
[Bits 5–0], [0 to 63], [Moving average counter of how many times this station was heard within the last 30 minutes. If >= 3, the link is considered stable. Dropped if 0 within 30 minutes.]
)
== Checkpoint Messages (Posten)
As soon as a participant checks in at a checkpoint using their RFID tag or identifier, they are registered at that node. The runner is assigned a timestamp and their ID (runner number). The mapping between the runner number and the RFID code is stored within the system. During the next available time slot, a checkpoint message is transmitted to the destination with QoS 2 (guaranteed packet delivery, duplicate detection, and filtering).
Messages are sent to the destination and optionally to the start node simultaneously. Both nodes must have an SSID < 15, as the remaining bits in the address byte are reserved for protocol flags.
This message packet is designed to be extremely short and efficient. It contains the node number, runner number, capture timestamp, and the current hop count.
Pseudo-Byte-Stream:
```text
(ppMLzzzzRR
```
=== Structure (Checkpoint Message)
#table(
columns: (1.5fr, 2fr, 5fr),
align: (center, left, left),
stroke: 0.5pt + luma(150),
fill: (x, y) => if y == 0 { rgb("f0f0f0") } else { none },
..table-header("Byte / Offset", "Field / Format", "Description"),
[Byte 0], [`(` (0x40)], [
*Data Type Identifier (DTI):* Special identifier designated for FoxMash checkpoint messages.
],
[Bytes 1–2], [Own SSID (`pp`) \ (2 Bytes ASCII)], [
Two ASCII characters representing the transmitting checkpoint node.
],
[Byte 3], [Routing & Flags (`M`) \ (`unsigned char`)], [
Combined Target SSID, Hop Count, and ACK Flags. (See Bit-mapping below).
],
[Bytes 4–7], [`zzzz` (Timestamp) \ (`int32_t`, Little-Endian)], [
*Timestamp:* Signed 32-bit integer representing the number of milliseconds since the start of the competitoin day. Formatted as Little-Endian (LSB first) and aligned to a 4-byte boundery for direct, high-performance ESP32 pointer access.
],
[Bytes 8–9], [Runner Number (`L`) \ (`uint16_t`)], [
Unique participant identification / runner number (Supports IDs from 0 to 65,535).
],
[Bytes 10], [Hop Count / Flags \ (`unsigned char`)], [
Current network hop count and remaing mesh routing flags.
]
)
#v(0.5em)
*Byte 3 (`M`) Bit-mapping:*
#table(
columns: (1.5fr, 1.5fr, 6fr),
align: (center, center, left),
stroke: 0.5pt + luma(150),
fill: (x, y) => if y == 0 { rgb("f5f5f5") } else { none },
..table-header("Bits", "Flag / Field", "Definition"),
[Bits 7–5], [Hop Count], [Dynamic routing counter. `0` if destination is seen directly with good quality. Incremented by 1 if no ACK within 5 minutes. Discarded if it exceeds max hop value.],
[Bit 4], [ACK Flag], [`0` for a standard data message, `1` for an acknowledgment packet (ACK).],
[Bits 3–0], [Target SSID], [The SSID of the destination node (Supports IDs from 0 to 15).]
)
=== Acknowledgments and Retries
The maximum number of retries is configurable (default: 4). Every checkpoint packet must be acknowledged. The acknowledgment uses the exact same format (`vppMLzzzzRR`), but with *Bit 4 in Byte 3 (`M`) set to `1`*. The return routing follows the identical path in the reverse direction based on the route recorded in the packet (source routing). Remit nodes detect and discard duplicate packets by matching the runner's unique timestamp.
== Protocol Optimization Analysis
=== Hardware-Optimized Protocol Design for ESP32 and LoRa
When designing low-power, high-performance mesh networks using *ESP32* microcontrollers over *LoRa* modulation, data structure layout critically impacts both transmission time (Airtime) and CPU efficiency. The updated FoxMash protocol layout restructures fields to achieve an optimal balance between RF efficiency and hardware execution.
==== Why this Layout is Perfect
1. *Native Word Alignment (No Performance Penalties):* The Tensilica Xtensa and RISC-V architectures used in ESP32 chips are natively *32-bit Little-Endian* processors. Accessing a 4-byte integer from a memory address that is not a multiple of 4 (_unaligned access_) causes the hardware to execute multi-cycle fixes, shifting bytes internally. In worst-case scenarios or specific compiler configurations, this causes a `LoadStoreAlignmentCause` hardware crash (Core Dump). By placing the 32-bit timestamp at precisely *Byte 4* (Checkpoint) and *Byte 12* (Beacon), the integer aligns perfectly with the processor's natural word boundaries.
2. *Zero-Copy Pointer Casting:* Because fields are naturally aligned, the firmware does not need to waste clock cycles using `memcpy()` or bit-shifting operators (`<<`, `|`) to reconstruct integers from individual bytes. The application layer can execute direct pointer casting directly out of the LoRa hardware receive buffer.
3. *Zero Airtime Overhead:* Traditional alignment techniques insert dummy padding bytes (`0x00`) to force alignment, which inflates the packet size. In LoRa networks, every additional byte directly increases the Time-on-Air (ToA), resulting in higher energy consumption and a higher probability of packet collisions. This optimized layout achieves perfect alignment solely by *reordering existing essential fields*, keeping the frame size strictly minimal.
4. *Human-Readable Delimiters Maintained:* Keeping the standard hyphen (`"-"`) or tilde (`"~"`) delimiter between the international call sign and the SSID preserves compatibility with legacy APRS/MeshCom parsers and keeps raw hexadecimal logs intuitive during serial debugging.
=== High-Performance C++ Implementation Example
The following code snippet demonstrates how to leverage this memory-aligned layout within an ESP32 application utilizing standard Arduino or ESP-IDF LoRa libraries.
#rect(width: 100%, stroke: 0.5pt + luma(150), fill: luma(245), radius: 4pt)[
```cpp
#include <Arduino.h>
// Ensure the compiler doesn't add structural padding within our structs
#pragma pack(push, 1)
struct BeaconPacket {
char dti; // Offset 0 (0x7B '{')
char callsign[6]; // Offset 1 (e.g., "OE3TKT")
char separator; // Offset 7 (e.g., '-')
char ssid[2]; // Offset 8 (e.g., "03")
uint8_t battery; // Offset 10 (0-100%)
uint8_t net_flags; // Offset 11 (Routing control / Padding)
int32_t timestamp_ms; // Offset 12 -> PERFECTLY 4-BYTE ALIGNED!
uint8_t neighbor_block[]; // Offset 16+ (Dynamic mesh payload)
};
struct CheckpointPacket {
char dti; // Offset 0 (0x28 '(')
uint16_t own_ssid; // Offset 1
uint8_t routing_flags; // Offset 3 (Target / Control Flags)
int32_t timestamp_ms; // Offset 4 -> PERFECTLY 4-BYTE ALIGNED!
uint16_t runner_id; // Offset 8
uint8_t hop_count; // Offset 10
};
#pragma pack(pop)
// Example parser function processing incoming raw LoRa bytes
void processIncomingLoRa(uint8_t* rxBuffer, size_t packetLength) {
if (packetLength < 1) return;
// Check Data Type Identifier (DTI)
char dti = rxBuffer[0];
if (dti == '(') {
// Zero-copy direct cast pointer access
CheckpointPacket* cp = (CheckpointPacket*)rxBuffer;
// Fast native access directly from memory
int32_t time = cp->timestamp_ms;
uint16_t runner = cp->runner_id;
Serial.printf("Runner %d checked in. Local Timestamp: %d ms\n", runner, time);
}
else if (dti == '{') {
BeaconPacket* beacon = (BeaconPacket*)rxBuffer;
// Fast native access directly from memory
int32_t time = beacon->timestamp_ms;
Serial.printf("Beacon received. Network Timestamp: %d ms\n", time);
}
}
```
]
// ==========================================
// GESTALTETER FOOTER (Name & Typst-Hinweis)
// ==========================================
#v(3em)
#line(length: 100%, stroke: 0.5pt + luma(200))
#v(0.5em)
#align(center)[
#text(size: 9pt, fill: luma(100))[
Document maintained and compiled by *[OE3TKT, Tom]* \
#v(2pt)
#text(size: 8pt, style: "italic")[
Typeset beautifully with #link("https://typst.app")[Typst] #box(width: 8pt, image(bytes("<svg viewBox='0 0 24 24' width='8' height='8'><path fill='#ff5500' d='M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z'/></svg>"), format: "svg"))
]
]
]
Datei ohne Änderungen umbenannt.
+317
Datei anzeigen
@@ -0,0 +1,317 @@
#set page(
paper: "a4",
margin: (x: 2cm, top: 3.5cm, bottom: 3cm),
// Kopfzeile mit dynamischem Platzhalter
header: align(right)[
#text(size: 8pt, fill: luma(120))[FoxMash-Protokoll v0.1]
]
)
#set text(font: "Libertinus Serif", lang: "de", size: 10.5pt)
#set par(justify: true)
// Styling-Funktion für Tabellen-Header
#let table-header(..cells) = {
cells.pos().map(cell => text(weight: "bold", fill: rgb("1a1a1a"))[#cell])
}
// ==========================================
// DYNAMISCHES HEADER-LOGO (Fuchs + ARDF-Schirm)
// ==========================================
#place(top + left, dy: -2.5cm)[
#grid(
columns: (60pt, 1fr),
gutter: 15pt,
align: horizon,
// Der ARDF-Postenschirm (Orange/Weißes Dreieck) gepaart mit einer Fuchs-Farbe
stack(
spacing: 2pt,
rect(width: 50pt, height: 50pt, stroke: 1pt + rgb("1a1a1a"), fill: white)[
#place(top + left)[
#polygon(
fill: rgb("ff5500"), // ARDF Orange
(0pt, 50pt),
(50pt, 0pt),
(50pt, 50pt)
)
]
// Stilisierte Fuchs-Silhouette (Ohren und Schnauze) ohne Kurven-Fehler
#place(center + horizon, dy: 2pt)[
#polygon(
fill: rgb("1a1a1a"),
(0pt, 0pt), // Linkes Ohr Spitze
(6pt, 8pt), // Kopf Mitte oben
(12pt, 0pt), // Rechtes Ohr Spitze
(12pt, 10pt), // Rechte Wange
(6pt, 18pt), // Schnauze unten
(0pt, 10pt) // Linke Wange
)
]
]
),
[
#text(size: 24pt, weight: "bold", tracking: 1pt, fill: rgb("1a1a1a"))[FoxMash] #v(-8pt)
#text(size: 10pt, weight: "medium", fill: rgb("ff5500"))[Amateur Radio Direction Finding Mesh Protocol]
]
)
]
#v(1cm)
= Protokoll
Deutsche Version. Im Zweifelsfall ist der englische Text verbindlich. \
#link("foxmash-protocol.pdf")[_Show English version_]
== Automatische Stationen an den ARDF-Senderanlagen (Füchse)
Ein Fuchs sendet regelmäßig seine Kennung und Telemetriedaten aus.
Dies geschieht, wenn möglich, zu Zeiten, in denen der Fuchs nicht auf den Bändern 80m oder 2m sendet.
Dies dient einerseits der Zeitsynchronisation benachbarter Knoten und andererseits der regelmäßigen offiziellen Nennung des Rufzeichens.
Meist wird das Rufzeichen des Organisators oder Ausrichters des Peilwettbewerbs verwendet.
== Beacon (Bake)
Aufgebaut auf Basis der modifizierten AX.25-, APRS- und MeshCom-Protokolle wie folgt:
Pseudo-Byte-Stream:
```text
{OE3TKT-03CpnnnnxRSqxRSqxRSq..
```
=== Struktur
#table(
columns: (1.5fr, 2fr, 5fr),
align: (center, left, left),
stroke: 0.5pt + luma(150),
fill: (x, y) => if y == 0 { rgb("f0f0f0") } else { none },
..table-header("Byte / Offset", "Feld / Format", "Beschreibung"),
[Byte 0], [`"{"` `0x7B`], [
*Data Type Identifier (DTI):* Abgeleitet aus der APRS- und MeshCom-Protokollspezifikation (siehe #link("https://ui-view.net")[APRS-Protokollreferenz]). \
*Begründung:* Aktuell in der Spezifikation als `[Unused]` markiert; Zeichen ähnelt visuell einem Stationssymbol.
],
[Bytes 1–6], [`"OE3TKT"` \ (6 Bytes ASCII)], [
*Internationales Rufzeichen:* Formatiert als 1–2 Zeichen Präfix, gefolgt von einer oder mehreren Ziffern und einem 1–3 Zeichen Suffix. Strikt 6 Zeichen breit; bei kürzeren Rufen rechts mit Leerzeichen aufgefüllt.
],
[Byte 7], [`"-"` `0x2D` oder `"~"` `0x7E`], [
*Knotentyp-Trennzeichen:* Bindestrich für Standardknoten, Tilde für Backup- / Hot-Standby-Knoten. Dient als visueller Begrenzer und struktureller Anker für den Parser.
],
[Bytes 8–9], [`"03"`, oder `"S6"` \ (2 Bytes ASCII)], [
*Stations-SSID-Kennung:* Teilstring-Format, das die Knoten-ID darstellt. Unterstützt numerische IDs (0–99) oder erweiterte Schreibweisen (z. B. `"S6"`), d. h. alle Zeichen wie in #link("https://aprs11/SSIDs.txt")[aprs11/SSIDs.txt] beschrieben (z. B. `OE3TKT-S6` für eine spezielle 6. Station).
],
[Byte 10], [Eigener Batteriestand \ (`unsigned char`)], [
*Wert:* Kapazität in Prozent (0 bis 100 %). Werte über 100 weisen auf Ladevorgang oder Netzbetrieb hin.
],
[Byte 11], [Protokoll- / Netz-Flags \ (`unsigned char`)], [
*Netzwerk-Flags / Padding:* Reserviert für Mesh-Netzwerk-Flags und Hop-Steuerung. Gewährleistet die präzise 4-Byte-Speicherausrichtung (Alignment) für den nachfolgenden 32-Bit-Zeitstempel.
],
[Bytes 12–15], [`nnnn` / \ Zeitstempel \ (`int32_t`, Little-Endian)], [
*Zeitstempel:* Vorzeichenbehafteter 32-Bit-Integer, der die Anzahl der Millisekunden seit Beginn des Wettbewerbstages darstellt. Formatiert als Little-Endian (LSB zuerst) und auf eine 4-Byte-Grenze ausgerichtet für optimierten, nativen ESP32-Zeigerzugriff. Alle offiziellen ARDF-Wettbewerbszeiten und -Logs müssen in koordinierter Weltzeit (UTC) geführt werden. Um _[Lokaler Zeitzonenname]_ in UTC umzurechnen, subtrahiere/addiere _[X]_ Stunden (UTC = _[Lokale Zeit]_ +/- _[X]_h). Bitte achte genau auf Datumswechsel bei Ereignissen, die UTC-Mitternacht überschreiten.
],
[Bytes 16+], [Gehörte Nachbarknoten \ (`xRSqxRSqxRSq...`)], [
Multi-Byte-Blöcke, die Details zu Mesh-Nachbarknoten enthalten (SSID, Verbindungsqualität, Batteriestatus). Siehe detaillierte Aufschlüsselung unten.
]
)
==== Aufschlüsselung des Blocks der gehörten Nachbarknoten (Bytes 16+)
#table(
columns: (1.5fr, 2fr, 5fr),
align: (center, left, left),
stroke: 0.5pt + luma(150),
fill: (x, y) => if y == 0 { rgb("f0f0f0") } else { none },
..table-header("Byte-Offset", "Feld / Format", "Beschreibung"),
[Bytes 16–17], [Nachbar-SSID \ (2 Bytes ASCII)], [
Zwei ASCII-Zeichen, welche die SSID des Nachbarknotens darstellen (z. B. `"03"` oder `"S6"`). Dies entspricht dem exakten Teilstring-Format aus den Bytes 8–9 des Quellknotens für einfachen String-Abgleich. \
*Padding:* Einstellige Stationen sollten links mit einem Null-Byte (`0x00`) aufgefüllt werden (z. B. `0x00 0x33` für Station 3). \
*Parsing-Regeln:* Die Firmware-Implementierung muss flexibel sein. Der Parser muss führende Null-Bytes (`0x00`), führende Leerzeichen (`0x20`) und führende ASCII-Nullen (`0x30`) unterstützen und fehlerfrei dekodieren.
],
[Byte 18], [RSSI-Wert (`R`) \ (`unsigned char`)], [Gespeichert als RSSI + 164 (z. B. `0x3A` = 58 -> 58 - 164 = -106 dBm).],
[Byte 19], [SNR-Wert (`S`) \ (`signed char`)], [Gespeichert als SNR multipliziert mit 4 (z. B. `0x42` = 66 -> 66 / 4 = +16,5 dB).],
[Byte 20], [Status-Byte (`q`) \ (`unsigned char`)], [Kombiniertes Byte für Qualität und Batteriestatus. (Siehe Bit-Mapping unten).]
)
#v(0.5em)
*Bit-Mapping für Byte 20 (`q`):*
#table(
columns: (1.5fr, 1.5fr, 6fr),
align: (center, center, left),
stroke: 0.5pt + luma(150),
fill: (x, y) => if y == 0 { rgb("f5f5f5") } else { none },
..table-header("Bits", "Wert", "Definition"),
[Bits 7–6], [`00`], [Normal (Battery OK / Powered)],
[Bits 7–6], [`01`], [Low (Battery low, intervention recommended)],
[Bits 7–6], [`10`], [Critical (Battery critical, node might shut down soon)],
[Bits 7–6], [`11`], [Reserved],
[Bits 5–0], [0 to 63], [Moving average counter of how many times this station was heard within the last 30 minutes. If >= 3, the link is considered stable. Dropped if 0 within 30 minutes.]
)
== Kontrollpunkt-Nachrichten (Posten / Checkpoints)
Sobald sich ein Teilnehmer an einem Kontrollpunkt mittels seines RFID-Tags oder seiner Kennung registriert, wird er an diesem Knoten erfasst. Dem Läufer wird ein Zeitstempel und seine ID (Startnummer) zugewiesen. Die Zuordnung zwischen Startnummer und RFID-Code ist systemintern hinterlegt. Während des nächsten verfügbaren Zeitschlitzes wird eine Kontrollpunkt-Nachricht mit QoS 2 (garantierte Paketzustellung, Duplikaterkennung und -filterung) an das Ziel übertragen.
Nachrichten werden gleichzeitig an den Zielknoten und optional an den Startknoten gesendet. Beide Knoten müssen eine SSID < 15 besitzen, da die verbleibenden Bits im Adress-Byte für Protokoll-Flags reserviert sind.
Dieses Nachrichtenpaket ist extrem kurz und effizient konzipiert. Es enthält die Knotennummer, die Startnummer, den Erfassungs-Zeitstempel und den aktuellen Hop-Count.
Pseudo-Byte-Stream:
```text
(ppMzzzzLLR
```
=== Structure (Checkpoint Message)
#table(
columns: (1.5fr, 2fr, 5fr),
align: (center, left, left),
stroke: 0.5pt + luma(150),
fill: (x, y) => if y == 0 { rgb("f0f0f0") } else { none },
..table-header("Byte / Offset", "Feld / Format", "Beschreibung"),
[Byte 0], [`(` (0x28)], [
*Data Type Identifier (DTI):* Spezielle Kennung, die für FoxMash-Kontrollpunkt-Nachrichten vorgesehen ist.
],
[Bytes 1–2], [Eigene SSID (`pp`) \ (`uint16_t`)], [
16-Bit unsigned Integer des sendenden Kontrollpunkt-Knotens.
],
[Byte 3], [Routing & Flags (`M`) \ (`unsigned char`)], [
Kombinierte Ziel-SSID und Protokoll-Steuerungs-Flags. Dient gleichzeitig als 1-Byte-Grenzausrichter (Alignment).
],
[Bytes 4–7], [`zzzz` (Zeitstempel) \ (`int32_t`, Little-Endian)], [
*Zeitstempel:* Vorzeichenbehafteter 32-Bit-Integer, der die Anzahl der Millisekunden seit Beginn des Wettbewerbstages darstellt. Formatiert als Little-Endian (LSB zuerst) und auf eine 4-Byte-Grenze ausgerichtet für direkten, hochperformanten ESP32-Zeigerzugriff.
],
[Bytes 8–9], [Startnummer (`L`) \ (`uint16_t`)], [
Eindeutige Teilnehmeridentifikation / Startnummer (Unterstützt IDs von 0 bis 65.535).
],
[Bytes 10], [Hop-Count / Flags \ (`unsigned char`)], [
Aktueller Netzwerk-Hop-Count und verbleibende Mesh-Routing-Flags.
]
)
#v(0.5em)
*Bit-Mapping für Byte 3 (`M`):*
#table(
columns: (1.5fr, 1.5fr, 6fr),
align: (center, center, left),
stroke: 0.5pt + luma(150),
fill: (x, y) => if y == 0 { rgb("f5f5f5") } else { none },
..table-header("Bits", "Flag / Feld", "Definition"),
[Bits 7–5], [Hop-Count], [Dynamischer Routing-Zähler. `0`, wenn das Ziel direkt mit guter Qualität sichtbar ist. Wird um 1 erhöht, wenn innerhalb von 5 Minuten kein ACK erfolgt. Wird verworfen, wenn der maximale Hop-Wert überschritten wird.],
[Bit 4], [ACK-Flag], [`0` für eine Standard-Datennachricht, `1` für ein Bestätigungspaket (ACK).],
[Bits 3–0], [Ziel-SSID], [Die SSID des Zielknotens (Unterstützt IDs von 0 bis 15).]
)
=== Bestätigungen (Acknowledgments) und Wiederholungen
Die maximale Anzahl der Sendeversuche ist konfigurierbar (Standard: 4). Jedes Kontrollpunkt-Paket muss quittiert werden. Die Bestätigung verwendet exakt dasselbe Format (`vppMLzzzzRR`), jedoch mit *auf `1` gesetztem Bit 4 in Byte 3 (`M`)*. Das Rückrouting erfolgt auf dem identischen Pfad in umgekehrter Richtung basierend auf der im Paket aufgezeichneten Route (Source-Routing). Weiterleitende Knoten erkennen und verwerfen Duplikate durch Abgleich des eindeutigen Zeitstempels des Läufers.
== Protokoll-Optimierungsanalyse
=== Hardware-optimiertes Protokolldesign für ESP32 und LoRa
Beim Entwurf von stromsparenden, hochperformanten Mesh-Netzwerken mit *ESP32*-Mikrocontrollern über *LoRa*-Modulation beeinflusst das Layout der Datenstrukturen sowohl die Übertragungszeit (Airtime) als auch die CPU-Effizienz maßgeblich. Das aktualisierte FoxMash-Protokolllayout strukturiert die Felder so um, dass ein optimales Gleichgewicht zwischen HF-Effizienz und Hardware-Ausführung erreicht wird.
==== Warum dieses Layout perfekt ist
1. *Native Wortausrichtung (Keine Performance-Verluste):* Die in ESP32-Chips verwendeten Tensilica Xtensa- und RISC-V-Architekturen sind von Natur aus *32-Bit Little-Endian*-Prozessoren. Der Zugriff auf einen 4-Byte-Integer von einer Speicheradresse, die kein Vielfaches von 4 is (_unaligned access_), zwingt die Hardware zu speicherintensiven Korrekturen durch internes Byte-Shifting. In ungünstigen Fällen oder spezifischen Compiler-Konfigurationen führt dies zu einem Hardware-Absturz (`LoadStoreAlignmentCause` Core Dump). Durch Platzierung des 32-Bit-Zeitstempels an exakt *Byte 4* (Kontrollpunkt) und *Byte 12* (Beacon) fügt sich der Integer perfekt in die natürlichen Wortgrenzen des Prozessors ein.
2. *Zero-Copy Zeiger-Casting:* Da die Felder nativ ausgerichtet sind, muss die Firmware keine Taktzyklen für `memcpy()` oder Bit-Shifting-Operatoren (`<<`, `|`) verschwenden, um Integer-Werte aus einzelnen Bytes zu rekonstruieren. Die Anwendungsschicht kann direkt aus dem LoRa-Hardware-Empfangspuffer ein Zero-Copy-Pointer-Casting durchführen.
3. *Null Airtime-Overhead:* Traditionelle Alignment-Techniken fügen künstliche Füllbytes (_Padding_, `0x00`) ein, um die Ausrichtung zu erheben, was das Paket vergrößert. In LoRa-Netzwerken erhöht jedes zusätzliche Byte direkt die Time-on-Air (ToA), was zu höherem Energieverbrauch und einer größeren Wahrscheinlichkeit von Paketkollisionen führt. Dieses optimierte Layout erreicht die perfekte Ausrichtung rein durch die *Umordnung bestehender, essenzieller Felder*, wodurch die Frame-Größe minimal bleibt.
4. *Menschlich lesbare Trennzeichen beibehalten:* Das Beibehalten des Standard-Bindestrichs (`"-"`) oder der Tilde (`"~"`) als Trennzeichen zwischen dem internationalen Rufzeichen und der SSID wahrt die Kompatibilität mit bestehenden APRS-/MeshCom-Parsern und hält rohe Hex-Logs beim seriellen Debuggen intuitiv lesbar.
=== Hochperformantes C++ Implementierungsbeispiel
Der folgende Code-Ausschnitt zeigt, wie dieses speicheroptimierte Layout in einer ESP32-Anwendung unter Verwendung von Standard-Arduino- oder ESP-IDF-LoRa-Bibliotheken genutzt werden kann.
#rect(width: 100%, stroke: 0.5pt + luma(150), fill: luma(245), radius: 4pt)[
```cpp
#include <Arduino.h>
// Sicherstellen, dass der Compiler kein strukturelles Padding in den Structs hinzufügt
#pragma pack(push, 1)
struct BeaconPacket {
char dti; // Offset 0 (0x7B '{')
char callsign[6]; // Offset 1 (z. B. "OE3TKT")
char separator; // Offset 7 (z. B. '-')
char ssid[2]; // Offset 8 (z. B. "03")
uint8_t battery; // Offset 10 (0-100%)
uint8_t net_flags; // Offset 11 (Routing-Steuerung / Padding)
int32_t timestamp_ms; // Offset 12 -> PERFEKT AUF 4-BYTE AUSGERICHTET!
uint8_t neighbor_block[]; // Offset 16+ (Dynamische Mesh-Nutzlast)
};
struct CheckpointPacket {
char dti; // Offset 0 (0x28 '(')
uint16_t own_ssid; // Offset 1
uint8_t routing_flags; // Offset 3 (Ziel- / Steuerungs-Flags)
int32_t timestamp_ms; // Offset 4 -> PERFEKT AUF 4-BYTE AUSGERICHTET!
uint16_t runner_id; // Offset 8
uint8_t hop_count; // Offset 10
};
#pragma pack(pop)
// Beispiel-Parserfunktion zur Verarbeitung eingehender roher LoRa-Bytes
void processIncomingLoRa(uint8_t* rxBuffer, size_t packetLength) {
if (packetLength < 1) return;
// Data Type Identifier (DTI) prüfen
char dti = rxBuffer[0];
if (dti == '(') {
// Zero-copy Direktzugriff über Zeiger-Cast
CheckpointPacket* cp = (CheckpointPacket*)rxBuffer;
// Schneller nativer Zugriff direkt aus dem Speicher
int32_t time = cp->timestamp_ms;
uint16_t runner = cp->runner_id;
Serial.printf("Läufer %d erfasst. Lokaler Zeitstempel: %d ms\n", runner, time);
}
else if (dti == '{') {
BeaconPacket* beacon = (BeaconPacket*)rxBuffer;
// Schneller nativer Zugriff direkt aus dem Speicher
int32_t time = beacon->timestamp_ms;
Serial.printf("Beacon empfangen. Netzwerk-Zeitstempel: %d ms\n", time);
}
}
```
]
// ==========================================
// GESTALTETER FOOTER (Name & Typst-Hinweis)
// ==========================================
#v(3em)
#line(length: 100%, stroke: 0.5pt + luma(200))
#v(0.5em)
#align(center)[
#text(size: 9pt, fill: luma(100))[
Dokument gepflegt und kompiliert von *[OE3TKT, Tom]* \
#v(2pt)
#text(size: 8pt, style: "italic")[
Wunderschön gesetzt mit #link("https://typst.app")[Typst] #box(width: 8pt, image(bytes("<svg viewBox='0 0 24 24' width='8' height='8'><path fill='#ff5500' d='M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z'/></svg>"), format: "svg"))
]
]
]
+106 -90
Datei anzeigen
@@ -117,7 +117,6 @@ static const uint8_t font_5x7[][5] = {
{0x00, 0x41, 0x36, 0x08, 0x00}, // 125 } {0x00, 0x41, 0x36, 0x08, 0x00}, // 125 }
{0x08, 0x08, 0x2a, 0x1c, 0x08}, // 126 ~ {0x08, 0x08, 0x2a, 0x1c, 0x08}, // 126 ~
{0x00, 0x00, 0x00, 0x00, 0x00}, // 127 DEL (Control character) {0x00, 0x00, 0x00, 0x00, 0x00}, // 127 DEL (Control character)
// --- UTF-8 0xC2 RANGE (ISO-8859-1 Indizes 128 to 191) --- // --- UTF-8 0xC2 RANGE (ISO-8859-1 Indizes 128 to 191) ---
{0x00, 0x00, 0x00, 0x00, 0x00}, // 128 Padding (Control character) {0x00, 0x00, 0x00, 0x00, 0x00}, // 128 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 129 Padding (Control character) {0x00, 0x00, 0x00, 0x00, 0x00}, // 129 Padding (Control character)
@@ -152,103 +151,102 @@ static const uint8_t font_5x7[][5] = {
{0x00, 0x00, 0x00, 0x00, 0x00}, // 158 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}, // 159 Padding (Control character)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 160 Non-breaking space {0x00, 0x00, 0x00, 0x00, 0x00}, // 160 Non-breaking space
{0x00, 0x00, 0x2f, 0x00, 0x00}, // 161 Inverted exclamation mark (¡) {0x00, 0x00, 0x5f, 0x00, 0x00}, // 161 Inverted exclamation mark (¡)
{0x43, 0x4c, 0x78, 0x4c, 0x43}, // 162 Cent sign (¢) {0x24, 0x2a, 0x7f, 0x2a, 0x12}, // 162 Cent sign (¢)
{0x18, 0x7c, 0x14, 0x3c, 0x54}, // 163 Pound sign (£) {0x10, 0x7e, 0x24, 0x44, 0x28}, // 163 Pound sign (£)
{0x6c, 0x6c, 0x6c, 0x6c, 0x6c}, // 164 Currency sign (¤) {0x6c, 0x28, 0x28, 0x28, 0x6c}, // 164 Currency sign (¤)
{0x30, 0x78, 0x4d, 0x78, 0x30}, // 165 Yen sign (¥) {0x08, 0x3e, 0x49, 0x3e, 0x08}, // 165 Yen sign (¥)
{0x00, 0x00, 0x77, 0x00, 0x00}, // 166 Broken bar (¦) {0x00, 0x00, 0x5f, 0x00, 0x00}, // 166 Broken bar (¦) (Alternativ durchgezogen)
{0x00, 0x4d, 0x4d, 0x6c, 0x00}, // 167 Section sign / Paragraph (§) {0x22, 0x49, 0x49, 0x49, 0x22}, // 167 Section sign / Paragraph (§)
{0x00, 0x00, 0x01, 0x00, 0x00}, // 168 Diaeresis (¨) {0x00, 0x03, 0x00, 0x03, 0x00}, // 168 Diaeresis (¨)
{0x00, 0x15, 0x16, 0x7c, 0x14}, // 169 Copyright sign (©) {0x3e, 0x41, 0x5d, 0x49, 0x3e}, // 169 Copyright sign (©)
{0x00, 0x42, 0x7b, 0x42, 0x42}, // 170 Feminine ordinal indicator (ª) {0x44, 0x28, 0x3e, 0x44, 0x3e}, // 170 Feminine ordinal indicator (ª)
{0x08, 0x14, 0x2a, 0x14, 0x22}, // 171 Left-pointing double-angle quotation mark («) {0x08, 0x14, 0x22, 0x00, 0x00}, // 171 Left-pointing double-angle quotation mark («)
{0x04, 0x04, 0x04, 0x04, 0x1c}, // 172 Not sign (¬) {0x04, 0x04, 0x04, 0x04, 0x07}, // 172 Not sign (¬)
{0x00, 0x08, 0x08, 0x08, 0x00}, // 173 Soft hyphen {0x04, 0x04, 0x04, 0x04, 0x04}, // 173 Soft hyphen
{0x00, 0x14, 0x14, 0x14, 0x14}, // 174 Registered sign (®) {0x3e, 0x41, 0x5d, 0x55, 0x3e}, // 174 Registered sign (®)
{0x00, 0x02, 0x02, 0x02, 0x02}, // 175 Macron (¯) {0x01, 0x01, 0x01, 0x01, 0x01}, // 175 Macron (¯)
{0x00, 0x03, 0x03, 0x00, 0x00}, // 176 Degree sign (°) {0x00, 0x06, 0x09, 0x06, 0x00}, // 176 Degree sign (°)
{0x44, 0x44, 0x5f, 0x44, 0x44}, // 177 Plus-minus sign (±) {0x08, 0x08, 0x3e, 0x08, 0x08}, // 177 Plus-minus sign (±) (Unterstrich wird dynamisch generiert/Zweit-Byte)
{0x00, 0x00, 0x07, 0x04, 0x04}, // 178 Superscript two (²) {0x00, 0x00, 0x00, 0x00, 0x00}, // 178 Superscript two (²) -> Oft custom generiert
{0x00, 0x00, 0x05, 0x05, 0x07}, // 179 Superscript three (³) {0x00, 0x00, 0x00, 0x00, 0x00}, // 179 Superscript three (³) -> Oft custom generiert
{0x02, 0x01, 0x02, 0x04, 0x00}, // 180 Acute accent (´) {0x00, 0x01, 0x02, 0x04, 0x00}, // 180 Acute accent (´)
{0x30, 0x48, 0x30, 0x08, 0x06}, // 181 Micro sign (µ) {0x3c, 0x0a, 0x0a, 0x0a, 0x02}, // 181 Micro sign (µ)
{0x00, 0x6e, 0x6e, 0x6e, 0x00}, // 182 Pilcrow sign / Paragraph sign (¶) {0x06, 0x7f, 0x4b, 0x4b, 0x3a}, // 182 Pilcrow sign / Paragraph sign (¶)
{0x00, 0x00, 0x04, 0x00, 0x00}, // 183 Middle dot (·) {0x00, 0x00, 0x0c, 0x0c, 0x00}, // 183 Middle dot (·)
{0x00, 0x40, 0x44, 0x40, 0x40}, // 184 Cedilla (¸) {0x10, 0x20, 0x00, 0x00, 0x00}, // 184 Cedilla (¸)
{0x00, 0x00, 0x01, 0x00, 0x00}, // 185 Superscript one (¹) {0x00, 0x00, 0x00, 0x00, 0x00}, // 185 Superscript one (¹)
{0x00, 0x44, 0x7d, 0x40, 0x00}, // 186 Masculine ordinal indicator (º) {0x38, 0x44, 0x44, 0x44, 0x38}, // 186 Masculine ordinal indicator (º)
{0x22, 0x14, 0x2a, 0x14, 0x08}, // 187 Right-pointing double-angle quotation mark (») {0x00, 0x00, 0x22, 0x14, 0x08}, // 187 Right-pointing double-angle quotation mark (»)
{0x1c, 0x22, 0x49, 0x49, 0x30}, // 188 Vulgar fraction one quarter (¼) {0x00, 0x00, 0x00, 0x00, 0x00}, // 188 Vulgar fraction one quarter (¼)
{0x38, 0x44, 0x54, 0x24, 0x18}, // 189 Vulgar fraction one half (½) {0x00, 0x00, 0x00, 0x00, 0x00}, // 189 Vulgar fraction one half (½)
{0x00, 0x00, 0x00, 0x00, 0x00}, // 190 Vulgar fraction three quarters (¾) {0x00, 0x00, 0x00, 0x00, 0x00}, // 190 Vulgar fraction three quarters (¾)
{0x40, 0x00, 0x45, 0x06, 0x00}, // 191 Inverted question mark (¿) {0x30, 0x48, 0x4a, 0x40, 0x20}, // 191 Inverted question mark (¿)
// --- UTF-8 0xC3 RANGE (ISO-8859-1 Indizes 192 to 255) --- // --- UTF-8 0xC3 RANGE (ISO-8859-1 Indizes 192 to 255) ---
{0x7c, 0x12, 0x11, 0x12, 0x7c}, // 192 Capital A with grave (À) {0x7c, 0x13, 0x11, 0x10, 0x7c}, // 192 Capital A with grave (À)
{0x7c, 0x11, 0x12, 0x14, 0x78}, // 193 Capital A with acute (Á) {0x7c, 0x10, 0x11, 0x13, 0x7c}, // 193 Capital A with acute (Á)
{0x7c, 0x15, 0x16, 0x14, 0x7c}, // 194 Capital A with circumflex (Â) {0x7c, 0x12, 0x15, 0x12, 0x7c}, // 194 Capital A with circumflex (Â)
{0x7c, 0x16, 0x19, 0x16, 0x7c}, // 195 Capital A with tilde (Ã) {0x7c, 0x12, 0x15, 0x12, 0x7c}, // 195 Capital A with tilde (Ã)
{0x7c, 0x11, 0x13, 0x11, 0x7c}, // 196 Capital A with umlaut (Ä) {0x7c, 0x11, 0x11, 0x11, 0x7c}, // 196 Capital A with umlaut (Ä)
{0x7c, 0x11, 0x15, 0x11, 0x7c}, // 197 Capital A with ring above (Å) {0x7c, 0x11, 0x11, 0x11, 0x7c}, // 197 Capital A with ring above (Å)
{0x7e, 0x51, 0x7f, 0x41, 0x73}, // 198 Capital AE ligature (Æ) {0x7f, 0x49, 0x7f, 0x41, 0x63}, // 198 Capital AE ligature (Æ)
{0x3e, 0x41, 0x49, 0x22, 0x10}, // 199 Capital C with cedilla (Ç) {0x3e, 0x41, 0x41, 0x22, 0x18}, // 199 Capital C with cedilla (Ç)
{0x7f, 0x4a, 0x49, 0x4a, 0x41}, // 200 Capital E with grave (È) {0x7f, 0x4a, 0x4a, 0x42, 0x42}, // 200 Capital E with grave (È)
{0x7f, 0x49, 0x4a, 0x4c, 0x40}, // 201 Capital E with acute (É) {0x7f, 0x42, 0x4a, 0x4a, 0x42}, // 201 Capital E with acute (É)
{0x7f, 0x4d, 0x4e, 0x4d, 0x41}, // 202 Capital E with circumflex (Ê) {0x7f, 0x43, 0x4d, 0x43, 0x41}, // 202 Capital E with circumflex (Ê)
{0x7f, 0x49, 0x4b, 0x49, 0x41}, // 203 Capital E with umlaut (Ë) {0x7f, 0x41, 0x41, 0x41, 0x41}, // 203 Capital E with umlaut (Ë)
{0x00, 0x42, 0x7d, 0x40, 0x00}, // 204 Capital I with grave (Ì) {0x00, 0x42, 0x7d, 0x40, 0x00}, // 204 Capital I with grave (Ì)
{0x00, 0x40, 0x7d, 0x42, 0x00}, // 205 Capital I with acute (Í) {0x00, 0x40, 0x7d, 0x42, 0x00}, // 205 Capital I with acute (Í)
{0x00, 0x46, 0x7d, 0x46, 0x00}, // 206 Capital I with circumflex (Î) {0x00, 0x42, 0x7d, 0x42, 0x00}, // 206 Capital I with circumflex (Î)
{0x00, 0x45, 0x7d, 0x45, 0x00}, // 207 Capital I with umlaut (Ï) {0x00, 0x41, 0x7d, 0x41, 0x00}, // 207 Capital I with umlaut (Ï)
{0x7f, 0x41, 0x41, 0x22, 0x1c}, // 208 Capital Eth (Ð) {0x7f, 0x41, 0x41, 0x22, 0x1c}, // 208 Capital Eth (Ð)
{0x7f, 0x05, 0x06, 0x0c, 0x7f}, // 209 Capital N with tilde (Ñ) {0x7f, 0x09, 0x05, 0x03, 0x7f}, // 209 Capital N with tilde (Ñ)
{0x3e, 0x42, 0x41, 0x42, 0x3e}, // 210 Capital O with grave (Ò) {0x3e, 0x43, 0x41, 0x41, 0x3e}, // 210 Capital O with grave (Ò)
{0x3e, 0x40, 0x41, 0x42, 0x3c}, // 211 Capital O with acute (Ó) {0x3e, 0x41, 0x41, 0x43, 0x3e}, // 211 Capital O with acute (Ó)
{0x3e, 0x44, 0x45, 0x44, 0x3e}, // 212 Capital O with circumflex (Ô) {0x3e, 0x42, 0x45, 0x42, 0x3e}, // 212 Capital O with circumflex (Ô)
{0x3e, 0x45, 0x46, 0x45, 0x3e}, // 213 Capital O with tilde (Õ) {0x3e, 0x42, 0x45, 0x42, 0x3e}, // 213 Capital O with tilde (Õ)
{0x3e, 0x41, 0x43, 0x41, 0x3e}, // 214 Capital O with umlaut (Ö) {0x3e, 0x41, 0x41, 0x41, 0x3e}, // 214 Capital O with umlaut (Ö)
{0x22, 0x14, 0x08, 0x14, 0x22}, // 215 Multiplication sign (×) {0x08, 0x14, 0x22, 0x14, 0x08}, // 215 Multiplication sign (×)
{0x2e, 0x41, 0x41, 0x41, 0x3a}, // 216 Capital O with stroke (Ø) {0x3e, 0x61, 0x51, 0x4b, 0x3e}, // 216 Capital O with stroke (Ø)
{0x3f, 0x42, 0x41, 0x40, 0x3f}, // 217 Capital U with grave (Ù) {0x3f, 0x42, 0x40, 0x40, 0x3f}, // 217 Capital U with grave (Ù)
{0x3f, 0x40, 0x41, 0x42, 0x3e}, // 218 Capital U with acute (Ú) {0x3f, 0x40, 0x40, 0x42, 0x3f}, // 218 Capital U with acute (Ú)
{0x3f, 0x44, 0x45, 0x44, 0x3f}, // 219 Capital U with circumflex (Û) {0x3f, 0x42, 0x44, 0x42, 0x3f}, // 219 Capital U with circumflex (Û)
{0x3f, 0x41, 0x43, 0x41, 0x3f}, // 220 Capital U with umlaut (Ü) {0x3f, 0x41, 0x40, 0x41, 0x3f}, // 220 Capital U with umlaut (Ü)
{0x03, 0x08, 0x70, 0x0c, 0x03}, // 221 Capital Y with acute (Ý) {0x03, 0x04, 0x78, 0x04, 0x03}, // 221 Capital Y with acute (Ý)
{0x7f, 0x49, 0x09, 0x09, 0x06}, // 222 Capital Thorn (Þ) {0x7f, 0x49, 0x49, 0x36, 0x00}, // 222 Capital Thorn (Þ)
{0x7f, 0x49, 0x49, 0x36, 0x00}, // 223 Small sharp s / Eszett (ß) {0x3e, 0x49, 0x49, 0x49, 0x32}, // 223 Small sharp s / Eszett (ß)
{0x20, 0x56, 0x55, 0x56, 0x78}, // 224 Small a with grave (à) {0x20, 0x55, 0x55, 0x54, 0x78}, // 224 Small a with grave (à)
{0x20, 0x55, 0x56, 0x58, 0x70}, // 225 Small a with acute (á) {0x20, 0x54, 0x55, 0x55, 0x78}, // 225 Small a with acute (á)
{0x20, 0x59, 0x5a, 0x58, 0x78}, // 226 Small a with circumflex (â) {0x20, 0x56, 0x55, 0x56, 0x78}, // 226 Small a with circumflex (â)
{0x20, 0x5a, 0x5d, 0x5a, 0x78}, // 227 Small a with tilde (ã) {0x20, 0x56, 0x55, 0x56, 0x78}, // 227 Small a with tilde (ã)
{0x20, 0x55, 0x57, 0x55, 0x78}, // 228 Small a with umlaut (ä) {0x20, 0x55, 0x54, 0x55, 0x78}, // 228 Small a with umlaut (ä)
{0x20, 0x55, 0x59, 0x55, 0x78}, // 229 Small a with ring above (å) {0x20, 0x55, 0x54, 0x55, 0x78}, // 229 Small a with ring above (å)
{0x24, 0x54, 0x78, 0x54, 0x5c}, // 230 Small ae ligature (æ) {0x24, 0x54, 0x78, 0x54, 0x5c}, // 230 Small ae ligature (æ)
{0x38, 0x44, 0x4c, 0x54, 0x24}, // 231 Small c with cedilla (ç) {0x38, 0x44, 0x44, 0x28, 0x10}, // 231 Small c with cedilla (ç)
{0x38, 0x56, 0x55, 0x56, 0x18}, // 232 Small e with grave (è) {0x38, 0x55, 0x55, 0x54, 0x18}, // 232 Small e with grave (è)
{0x38, 0x55, 0x56, 0x58, 0x10}, // 233 Small e with acute (é) {0x38, 0x54, 0x55, 0x55, 0x18}, // 233 Small e with acute (é)
{0x38, 0x59, 0x5a, 0x58, 0x18}, // 234 Small e with circumflex (ê) {0x38, 0x56, 0x55, 0x56, 0x18}, // 234 Small e with circumflex (ê)
{0x38, 0x55, 0x57, 0x55, 0x18}, // 235 Small e with umlaut (ë) {0x38, 0x55, 0x54, 0x55, 0x18}, // 235 Small e with umlaut (ë)
{0x00, 0x45, 0x7e, 0x40, 0x00}, // 236 Small i with grave (ì) {0x00, 0x45, 0x7c, 0x40, 0x00}, // 236 Small i with grave (ì)
{0x00, 0x40, 0x7e, 0x45, 0x00}, // 237 Small i with acute (í) {0x00, 0x40, 0x7c, 0x45, 0x00}, // 237 Small i with acute (í)
{0x00, 0x49, 0x7e, 0x49, 0x00}, // 238 Small i with circumflex (î) {0x00, 0x46, 0x7c, 0x46, 0x00}, // 238 Small i with circumflex (î)
{0x00, 0x46, 0x7e, 0x46, 0x00}, // 239 Small i with umlaut (ï) {0x00, 0x45, 0x7c, 0x45, 0x00}, // 239 Small i with umlaut (ï)
{0x38, 0x44, 0x44, 0x48, 0x7f}, // 240 Small eth (ð) {0x38, 0x44, 0x44, 0x48, 0x7f}, // 240 Small eth (ð)
{0x7c, 0x09, 0x0a, 0x04, 0x78}, // 241 Small n with tilde (ñ) {0x7c, 0x05, 0x05, 0x04, 0x78}, // 241 Small n with tilde (ñ)
{0x38, 0x46, 0x45, 0x46, 0x38}, // 242 Small o with grave (ò) {0x38, 0x45, 0x45, 0x44, 0x38}, // 242 Small o with grave (ò)
{0x38, 0x45, 0x46, 0x48, 0x30}, // 243 Small o with acute (ó) {0x38, 0x44, 0x45, 0x45, 0x38}, // 243 Small o with acute (ó)
{0x38, 0x4a, 0x4b, 0x4a, 0x38}, // 244 Small o with circumflex (ô) {0x38, 0x46, 0x45, 0x46, 0x38}, // 244 Small o with circumflex (ô)
{0x38, 0x4b, 0x4c, 0x4b, 0x38}, // 245 Small o with tilde (õ) {0x38, 0x46, 0x45, 0x46, 0x38}, // 245 Small o with tilde (õ)
{0x38, 0x45, 0x47, 0x45, 0x38}, // 246 Small o with umlaut (ö) {0x38, 0x45, 0x44, 0x45, 0x38}, // 246 Small o with umlaut (ö)
{0x08, 0x08, 0x2a, 0x08, 0x08}, // 247 Division sign (÷) {0x08, 0x08, 0x2a, 0x08, 0x08}, // 247 Division sign (÷)
{0x38, 0x44, 0x44, 0x44, 0x38}, // 248 Small o with stroke (ø) {0x38, 0x64, 0x54, 0x4c, 0x38}, // 248 Small o with stroke (ø)
{0x3c, 0x42, 0x41, 0x20, 0x7c}, // 249 Small u with grave (ù) {0x3c, 0x41, 0x40, 0x20, 0x7c}, // 249 Small u with grave (ù)
{0x3c, 0x40, 0x41, 0x22, 0x78}, // 250 Small u with acute (ú) {0x3c, 0x40, 0x40, 0x21, 0x7c}, // 250 Small u with acute (ú)
{0x3c, 0x44, 0x45, 0x24, 0x7c}, // 251 Small u with circumflex (û) {0x3c, 0x42, 0x40, 0x22, 0x7c}, // 251 Small u with circumflex (û)
{0x3c, 0x41, 0x43, 0x21, 0x7c}, // 252 Small u with umlaut (ü) {0x3c, 0x41, 0x40, 0x21, 0x7c}, // 252 Small u with umlaut (ü)
{0x0c, 0x50, 0x52, 0x50, 0x3c}, // 253 Small y with acute (ý) {0x4c, 0x30, 0x30, 0x30, 0x7c}, // 253 Small y with acute (ý)
{0x7f, 0x14, 0x14, 0x08, 0x04}, // 254 Small thorn (þ) {0x7f, 0x24, 0x24, 0x18, 0x00}, // 254 Small thorn (þ)
{0x0c, 0x51, 0x53, 0x51, 0x3c} // 255 Small y with umlaut (ÿ) {0x4c, 0x31, 0x30, 0x31, 0x7c} // 255 Small y with umlaut (ÿ)
}; };
/** /**
@@ -525,6 +523,7 @@ void run_display_font_test(DisplayController* display) {
if (display == nullptr) return; if (display == nullptr) return;
// Test 1: Standard printable ASCII characters (Lines 0 and 1) // Test 1: Standard printable ASCII characters (Lines 0 and 1)
display->printMessage(0, " ",0); // clear line
display->printMessage(0, "=== FONT TEST ===", 12); display->printMessage(0, "=== FONT TEST ===", 12);
display->printMessage(1, "ASCII: abcXYZ 123!?", 0); display->printMessage(1, "ASCII: abcXYZ 123!?", 0);
@@ -542,6 +541,23 @@ void run_display_font_test(DisplayController* display) {
// Let the user look at the test pattern for 60 seconds // Let the user look at the test pattern for 60 seconds
vTaskDelay(pdMS_TO_TICKS(60000)); vTaskDelay(pdMS_TO_TICKS(60000));
// Display löschen oder Zeilen vorbereiten, falls nötig
for (uint8_t line = 0; line < 8; line++) {
display->printMessage(line, " "); // Zeile leeren
}
int c = 32;
for ( uint8_t line = 0; line < 8 ; line++) {
for ( uint8_t i = 0; i < 22 ; i++ ) {
if (c > 255) break;
char buf[2] = { (char)c, '\0' };
display->appendMessage(line, buf , i);
c++; //next char
}
}
vTaskDelay(pdMS_TO_TICKS(60000));
// Print a quick shutdown notice to the CLI console before restarting // Print a quick shutdown notice to the CLI console before restarting
printf("\r\n[SYSTEM] Visual test completed. Triggering hardware reset now...\n"); printf("\r\n[SYSTEM] Visual test completed. Triggering hardware reset now...\n");
vTaskDelay(pdMS_TO_TICKS(1000)); vTaskDelay(pdMS_TO_TICKS(1000));