Added IR Lib, samples and specification
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s

This commit is contained in:
2026-01-04 20:53:39 +01:00
parent 2cb0a33b8e
commit 667600c14e
19 changed files with 546 additions and 6 deletions

View File

@@ -107,13 +107,23 @@ sequenceDiagram
* **Status:** `GAME_STATE_RUNNING`.
* **Aktion:** Waffen sind entsperrt. Sensoren sind scharf.
* **Treffer-Logik (Dezentral):**
1. Waffe A schießt (sendet IR-Code mit `ShooterID` + `Damage`).
2. Weste B empfängt IR-Signal.
1. Waffe A schießt (sendet IR-Frame mit `Type=Hit`, `ShooterID`, `Damage`, `CRC8`).
2. Weste B empfängt IR-Signal über TSOP4838, validiert CRC.
3. Weste B berechnet Schaden (unter Berücksichtigung von Trefferzone-Multiplikator).
4. Weste B zieht Lebenspunkte ab.
5. **Feedback:** Weste B leuchtet/vibriert/spielt Sound ("Ugh!").
6. **Speicherung:** Weste B speichert den Treffer im internen Flash-Log (`Timestamp, ShooterID, Zone, Damage`).
7. *(Optional)* Weste B sendet UDP-Paket an Leader für Live-Scoreboard (Best Effort).
!!! info "Warum kein MilesTag2?"
MilesTag2 wurde als Basis erwogen, ist aber mit ~40 ms Frame-Zeit und starren 8-Bit-IDs zu langsam und unflexibel. Unser Custom-Protokoll bietet:
- **Kürzere Frames:** ~36 ms vs. ~40 ms (weniger anfällig für Zittern/Bewegung)
- **Flexible Type-Codes:** Hit/Heal/PowerUp/Admin in einem Format
- **CRC8-Prüfung:** >99.5% Fehlerrate-Erkennung bei Sonnenlicht
- **Variable Daten:** 13-Bit-Payload anpassbar pro Type
Details siehe [IR-Protokoll-Spezifikation](../specifications/ir_protocol.md).
* **Heilquellen:** Medic/Medipack-IR (breit gestreut, kurze Reichweite, negativer Damage) werden als Heilung interpretiert.
* **Zonen-Effekte:** Bases/Joiner senden `game/zone` (Link-Local, Hop=1); Weste prüft RSSI-Schwelle und addiert HP-Deltas (friend/foe) nach optionalem Warn-Countdown.

View File

@@ -39,7 +39,7 @@ Diese Roadmap führt vom nRF52840DK bis zum fertigen Produkt.
- [ ] Zephyr Setup: Installation des nRF Connect SDK (NCS) und VS Code.
- [ ] Custom Board Definition: Board-File anlegen, das die Pins des nRF52840DK auf die geplanten Funktionen mappt (PWM für IR, GPIO für Buttons).
- [ ] Thread Mesh: Minimalen OpenThread-Stack aufsetzen. Ein DK als Leader (FTD), einer als Child. UDP/CoAP-Ping bei Knopfdruck.
- [ ] IR-Engine: MilesTag-Encoder mit nrfx_pwm + PPI implementieren. Signal mit Oszilloskop/Logic Analyzer verifizieren. Sicherstellen, dass Funk die IR-Engine nicht stört.
- [ ] IR-Engine: Custom IR-Protokoll (pulse-distance, 38 kHz) mit nrfx_pwm + PPI implementieren. Signal mit Oszilloskop/Logic Analyzer verifizieren. Sicherstellen, dass Funk die IR-Engine nicht stört. **Hinweis:** MilesTag2 wurde verworfen zugunsten eines eigenen, kürzeren Protokolls mit CRC8 (siehe [Spezifikationen](specifications/ir_protocol.md)).
#### Phase 2: Der "Prototyp" (Integration)
**Ziel:** Einbindung von Audio und Solenoid.

View File

@@ -0,0 +1,201 @@
# IR-Kommunikationsprotokoll
## Übersicht
Das Infrarot-Kommunikationsprotokoll basiert auf Pulse-Distance-Codierung mit 38 kHz Träger und ist ähnlich Sony SIRC, aber optimiert für die Anforderungen des Lasertag-Systems. Das Protokoll bietet robuste Übertragung mit CRC-Fehlerprüfung und kurzen Frame-Zeiten (~36 ms).
## Physikalische Schicht
| Parameter | Wert | Anmerkung |
|-----------|------|----------|
| Trägerfrequenz | 38 kHz | Standard für TSOP48xx Empfänger |
| Tastgrad (Duty Cycle) | 50 % | Konfigurierbar (2575 %) |
| Modulation | PWM mit Pulse-Distance-Codierung | Hardware-basiert via nRF52 PWM-Peripheral |
| Empfänger | TSOP4838 (kompatibel) | Active-Low Ausgang, 38 kHz Bandpass |
## Timing-Spezifikation
| Symbol | Dauer | Toleranz | Beschreibung |
|--------|-------|----------|-------------|
| **Start Burst** | 2400 µs | ±200 µs | Frame-Synchronisations-Impuls |
| **Mark** | 600 µs | ±100 µs | Träger AN (konstant für alle Bits) |
| **Space 0** | 600 µs | ±100 µs | Träger AUS für logisch 0 |
| **Space 1** | 1200 µs | ±150 µs | Träger AUS für logisch 1 |
### Bit-Codierung
```
Bit 0: [Mark 600µs] + [Space 600µs] = 1.2 ms
Bit 1: [Mark 600µs] + [Space 1.2ms] = 1.8 ms
```
### Beispiel-Wellenform (3 Bits: `101`)
**Träger-Timing für Bits 1-0-1:**
| Segment | Dauer | State | Bit-Wert |
|---------|-------|-------|---------|
| Mark | 600 µs | AN | |
| Space 1 | 1200 µs | AUS | **1** (1.8 ms total) |
| Mark | 600 µs | AN | |
| Space 0| 600 µs | AUS | **0** (1.2 ms total) |
| Mark | 600 µs | AN | |
| Space 1| 1200 µs | AUS | **1** (1.8 ms total) |
```mermaid
block-beta
columns 8
mark1["Mark<br>600 µs"] space1["Space 1<br>1200 µs"]:2 mark2["Mark<br>600 µs"] space2["Space 0<br>600 µs"] mark3["Mark<br>600 µs"] space3["Space 1<br>1200 µs"]:2
bit1["<b>1</b>"]:3 bit2["<b>0</b>"]:2 bit3["<b>1</b>"]:3
style space1 fill:none
style space2 fill:none
style space3 fill:none
```
## Frame-Format
Alle Frames bestehen aus 24 Bits, übertragen MSB-first:
| Feld | Start Burst | Type | Data | CRC8 |
|------|-------------|------|------|------|
| **Dauer** | 2400 µs | 3 Bits | 13 Bits | 8 Bits |
| **Funktion** | Synchronisation | Frame-Typ | Payload | Fehlerprüfung |
| **Summe** | | | | **24 Bits** |
```mermaid
packet-beta
title Frame
+3: "Typ"
+13: "Payload"
+8: "Fehlerprüfung"
```
**Gesamte Frame-Zeit:** ~36 ms (Start + 24 × 1.5 ms durchschnittliche Bit-Zeit)
### Type-Feld (3 Bits)
| Wert | Typ | Beschreibung |
|------|-----|-------------|
| `000` | Hit | Standard-Schuss |
| `001` | Heal | Medic-Heilung oder Health Pack |
| `010` | PowerUp | Station Power-Up Grant |
| `011` | Admin | System-Steuerbefehle |
| `100``111` | Reserviert | Zukünftige Nutzung |
### Data-Feld (13 Bits) Type-abhängig
#### Hit-Frame (`000`)
| Bit-Range | Feld | Wertbereich | Beschreibung |
|-----------|------|-------------|-------------|
| 07 | Shooter ID | 0255 | ID des Schützen (256 mögliche Spieler) |
| 812 | Damage | 031 | Schadenpunkte (0 = kein Schaden, 31 = Maximum) |
#### Heal-Frame (`001`)
| Bit-Range | Feld | Wertbereich | Beschreibung |
|-----------|------|-------------|-------------|
| 07 | Healer ID | 0255 | ID des Heilers (Medic oder Station) |
| 812 | Amount | 031 | Heilpunkte wiederhergestellt |
#### PowerUp-Frame (`010`)
| Bit-Range | Feld | Wertbereich | Beschreibung |
|-----------|------|-------------|-------------|
| 07 | Station ID | 0255 | Stations-ID, die das Power-Up gewährt |
| 812 | PowerUp | 031 | Power-Up-Typ-Identifier |
#### Admin-Frame (`011`)
| Bit-Range | Feld | Wertbereich | Beschreibung |
|-----------|------|-------------|-------------|
| 012 | Command Data | 08191 | Implementierungsdefinierte Steuerbefehle |
### CRC-Feld (8 Bits)
- **Algorithmus:** CRC-8-CCITT
- **Polynom:** 0x07 (x⁸ + x² + x + 1)
- **Initialwert:** 0x00
- **Eingabe:** Type (3 Bits) + Data (13 Bits) = 16 Bits
- **Zweck:** Fehlererkennung bei Bitfehlern durch Umgebungslicht oder Interferenzen
**Erwartete Fehlererkennungsrate:** >99.5 % für Einfach- oder Doppelbitfehler
## Beispiel-Frame
**Hit von Spieler 42 mit 10 Schaden:**
```
Type: 000 (Hit)
Data: 00101010 01010 (ShooterID=42, Damage=10)
CRC8: [berechnet aus obigen Daten]
Kompletter Frame (24 Bits):
000 00101010 01010 CCCCCCCC
│ │ │ └─ CRC8
│ │ └─ Damage (10)
│ └─ Shooter ID (42)
└─ Type (Hit)
```
**Übertragungsabfolge:**
1. Start Burst: 2400 µs Träger AN
2. Bit 0 (Type): 600 µs Mark + 600 µs Space
3. Bit 1 (Type): 600 µs Mark + 600 µs Space
4. Bit 2 (Type): 600 µs Mark + 600 µs Space
5. ... (21 weitere Bits)
6. Ende: Träger AUS
## Empfänger-Implementierung
### Hardware-Anforderungen
- TSOP4838 verbunden mit GPIO mit Interrupt-Fähigkeit
- Steigende/fallende Flanken-Erkennung
- Timer zur Messung der Space-Dauern
### Software-State-Machine
1. **IDLE:** Auf Start Burst warten (20002800 µs)
2. **SYNC:** Start Burst erkannt, Vorbereitung zur Bit-Empfang
3. **DATA:** Space nach jedem Mark messen, 24 Bits dekodieren
4. **VALIDATE:** CRC prüfen, Frame bei Gültigkeit verarbeiten
### Timing-Toleranzen
- Breite Toleranzbereiche (±1733 %) kompensieren Interrupt-Jitter und Träger-Drift
- Fehlgeschlagener CRC zeigt beschädigten Frame an → stille Verwerfung
- Empfänger resynchronisiert automatisch beim nächsten Start Burst
## Konfigurierbare Parameter
Die Protokoll-Timing kann via Kconfig für verschiedene Umgebungen angepasst werden:
- `CONFIG_IR_SEND_CARRIER_HZ`: Trägerfrequenz (3045 kHz)
- `CONFIG_IR_SEND_DUTY_CYCLE_PERCENT`: PWM Tastgrad (2575 %)
- `CONFIG_IR_SEND_MARK_US`: Mark-Dauer (3001000 µs)
- `CONFIG_IR_SEND_SPACE0_US`: Space für Bit 0 (3001000 µs)
- `CONFIG_IR_SEND_SPACE1_US`: Space für Bit 1 (8002000 µs)
- `CONFIG_IR_SEND_START_BURST_US`: Start Burst (15004000 µs)
Die Standardwerte folgen Sony SIRC Timing-Konventionen für bewährte Zuverlässigkeit.
## Leistungscharakteristiken
| Metrik | Wert |
|--------|------|
| Frame-Zeit | ~36 ms |
| Datenrate | ~670 bit/s |
| Max. Spieler-IDs | 256 |
| Reichweite (Außen) | ~510 m (abhängig von Sender-Leistung und Umgebungslicht) |
| Fehler-Erkennung | >99.5 % via CRC-8 |
| Störfestigkeit | Hoch (Hardware-Bandpass 38 kHz) |
---
## Bluetooth LE Protokoll
*Zu dokumentieren: BLE-Charakteristiken für Spielstatus-Synchronisierung, Team-Zuordnung, etc.*

View File

@@ -8,6 +8,8 @@ nav:
- Hardware: konzept/hardware.md
- Software: konzept/software.md
- Gameplay & Modi: konzept/gameplay.md
- Spezifikationen:
- IR-Protokoll: specifications/ir_protocol.md
- Planung: planung.md
- Lizenz: license.md

View File

@@ -0,0 +1,10 @@
cmake_minimum_required(VERSION 3.20)
# Tell Zephyr to look into our libs folder for extra modules
list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(ir_send)
# Define application source files
target_sources(app PRIVATE src/main.c)

View File

@@ -0,0 +1,13 @@
config IR_SEND_SAMPLE_BURST_US
int "IR test burst length (microseconds)"
default 1000
range 50 100000
help
Duration of the carrier burst for each test pulse in the sample app.
config IR_SEND_SAMPLE_PERIOD_MS
int "IR test burst period (milliseconds)"
default 1000
range 10 60000
help
Interval between consecutive test bursts in the sample app.

View File

@@ -0,0 +1 @@
../../../../boards/nrf52840dk/nrf52840dk_nrf52840.overlay

View File

@@ -0,0 +1,13 @@
# Logging
CONFIG_LOG=y
# IR Send Library
CONFIG_IR_SEND=y
# PWM driver for IR carrier
CONFIG_PWM=y
CONFIG_PWM_NRFX=y
# Sample test configuration
CONFIG_IR_SEND_SAMPLE_BURST_US=1000
CONFIG_IR_SEND_SAMPLE_PERIOD_MS=1000

View File

@@ -0,0 +1,37 @@
/*
* ir_send sample app - IR transmission test
*/
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include "ir_send.h"
LOG_MODULE_REGISTER(ir_send);
int main(void)
{
LOG_INF("=== IR Send Sample ===");
LOG_INF("Board: %s", CONFIG_BOARD);
int ret = ir_send_init();
if (ret != 0) {
LOG_ERR("Failed to initialize IR send: %d", ret);
return ret;
}
ir_send_set_frequency(CONFIG_IR_SEND_CARRIER_HZ);
LOG_INF("Ready to test IR transmission (burst %u us every %u ms @ %u Hz)",
CONFIG_IR_SEND_SAMPLE_BURST_US,
CONFIG_IR_SEND_SAMPLE_PERIOD_MS,
CONFIG_IR_SEND_CARRIER_HZ);
while (true) {
ret = ir_send_pulse(CONFIG_IR_SEND_SAMPLE_BURST_US);
if (ret != 0) {
LOG_ERR("ir_send_pulse failed: %d", ret);
}
k_msleep(CONFIG_IR_SEND_SAMPLE_PERIOD_MS);
}
}

View File

@@ -3,6 +3,9 @@ cmake_minimum_required(VERSION 3.20)
# Tell Zephyr to look into our libs folder for extra modules
list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/../../libs)
# Set board root to find custom board overlays in firmware/boards
set(BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../..)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(lasertag_leader)

View File

@@ -3,6 +3,9 @@ cmake_minimum_required(VERSION 3.20)
# Zephyr mitteilen, dass unsere Libs Teil des Projekts sind
list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/../../libs)
# Set board root to find custom board overlays in firmware/boards
set(BOARD_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../..)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(lasertag_weapon)

View File

@@ -0,0 +1,56 @@
/*
* Device Tree Overlay für nRF52840 DK
* Definiert GPIO-Pins für Trigger, LEDs und IR-Transmission (PWM3 @ P0.16)
*/
/ {
aliases {
trigger-btn = &button0;
ir-output = &ir_tx0;
led-status = &led0;
led-power = &led1;
};
buttons {
compatible = "gpio-keys";
button0: button_0 {
gpios = <&gpio0 11 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
label = "Trigger Button";
};
};
leds {
compatible = "gpio-leds";
led0: led_0 {
gpios = <&gpio0 13 GPIO_ACTIVE_HIGH>;
label = "Status LED";
};
led1: led_1 {
gpios = <&gpio0 14 GPIO_ACTIVE_HIGH>;
label = "Power LED";
};
};
ir_pwm: ir_pwm {
compatible = "pwm-leds";
ir_tx0: ir_tx_0 {
pwms = <&pwm3 0 PWM_NSEC(26316) PWM_POLARITY_NORMAL>;
label = "IR TX PWM";
};
};
};
&pwm3 {
status = "okay";
pinctrl-0 = <&pwm3_default>;
pinctrl-names = "default";
};
&pinctrl {
pwm3_default: pwm3_default {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 0, 16)>;
};
};
};

View File

@@ -2,4 +2,5 @@
# Build ble_mgmt and thread_mgmt first since lasertag_utils depends on them
add_subdirectory(ble_mgmt)
add_subdirectory(thread_mgmt)
add_subdirectory(lasertag_utils)
add_subdirectory(lasertag_utils)
add_subdirectory(ir_send)

View File

@@ -1,4 +1,5 @@
# Main entry point for custom project Kconfigs
rsource "lasertag_utils/Kconfig"
rsource "thread_mgmt/Kconfig"
rsource "ble_mgmt/Kconfig"
rsource "ble_mgmt/Kconfig"
rsource "ir_send/Kconfig"

View File

@@ -0,0 +1,5 @@
if(CONFIG_IR_SEND)
zephyr_library()
zephyr_sources(src/ir_send.c)
zephyr_include_directories(include)
endif()

View File

@@ -0,0 +1,61 @@
config IR_SEND
bool "IR Send Library"
help
Enable IR transmission library for laser tag system.
Provides PWM-based IR carrier generation with pulse-distance coding.
if IR_SEND
menu "IR Protocol Configuration"
config IR_SEND_CARRIER_HZ
int "IR carrier frequency (Hz)"
default 38000
range 30000 45000
help
Carrier frequency for PWM generation. Standard value is 38 kHz
for TSOP48xx receivers. Adjust only if using different receivers.
config IR_SEND_DUTY_CYCLE_PERCENT
int "Carrier duty cycle (%)"
default 50
range 25 75
help
PWM duty cycle percentage. Some receivers prefer 33%, others 50%.
Default 50% works with most TSOP-series receivers.
config IR_SEND_MARK_US
int "Mark duration (microseconds)"
default 600
range 300 1000
help
Duration of carrier burst (mark) for each bit. Standard is 600 µs
following Sony SIRC timing.
config IR_SEND_SPACE0_US
int "Space duration for bit 0 (microseconds)"
default 600
range 300 1000
help
Carrier-off duration (space) after mark for logical 0.
Default 600 µs creates 1.2 ms total bit time.
config IR_SEND_SPACE1_US
int "Space duration for bit 1 (microseconds)"
default 1200
range 800 2000
help
Carrier-off duration (space) after mark for logical 1.
Default 1200 µs creates 1.8 ms total bit time.
config IR_SEND_START_BURST_US
int "Start burst duration (microseconds)"
default 2400
range 1500 4000
help
Initial synchronization burst at frame start. Receivers detect
this to sync on incoming frames. Default 2400 µs = 4× mark time.
endmenu
endif

View File

@@ -0,0 +1,45 @@
#ifndef IR_SEND_H
#define IR_SEND_H
/**
* @file ir_send.h
* @brief Infrared transmission library for laser tag system.
*/
#include <stdint.h>
/**
* @brief Initialize IR output hardware (PWM backend).
* @return 0 on success, negative error code otherwise.
*/
int ir_send_init(void);
/**
* @brief Send a single IR pulse (carrier burst).
* @param duration_us Pulse duration in microseconds.
* @return 0 on success.
*/
int ir_send_pulse(uint32_t duration_us);
/**
* @brief Send a carrier burst of given duration (us) at configured frequency.
* @param burst_us Duration in microseconds.
* @return 0 on success.
*/
int ir_send_burst_us(uint32_t burst_us);
/**
* @brief Send IR message (sequence of pulses and gaps).
* @param data IR payload (e.g., shooter ID, power-up type).
* @param len Length of data in bytes.
* @return 0 on success.
*/
int ir_send_message(const uint8_t *data, uint8_t len);
/**
* @brief Set IR carrier frequency.
* @param freq_hz Carrier frequency in Hz (typically 38000 Hz).
*/
void ir_send_set_frequency(uint32_t freq_hz);
#endif /* IR_SEND_H */

View File

@@ -0,0 +1,78 @@
/*
* IR Send Library - PWM-based carrier generation
*/
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/drivers/pwm.h>
#include "ir_send.h"
LOG_MODULE_REGISTER(ir_send_lib);
static const struct pwm_dt_spec ir_pwm = PWM_DT_SPEC_GET(DT_ALIAS(ir_output));
static uint32_t carrier_freq = 38000; /* Standard IR carrier frequency in Hz */
int ir_send_init(void)
{
if (!device_is_ready(ir_pwm.dev)) {
LOG_ERR("IR PWM device not ready!");
return -ENODEV;
}
LOG_INF("IR output PWM ready: dev=%p, channel=%u", ir_pwm.dev, ir_pwm.channel);
return 0;
}
int ir_send_pulse(uint32_t duration_us)
{
return ir_send_burst_us(duration_us);
}
int ir_send_message(const uint8_t *data, uint8_t len)
{
if (!device_is_ready(ir_pwm.dev)) {
return -ENODEV;
}
LOG_DBG("Sending IR message (%u bytes)", len);
/* TODO: Implement IR protocol encoding (e.g., NEC, custom protocol)
* For now, just a placeholder that sends a test pulse.
*/
ir_send_pulse(1000); /* 1ms test pulse */
return 0;
}
void ir_send_set_frequency(uint32_t freq_hz)
{
carrier_freq = freq_hz;
LOG_DBG("IR carrier frequency set to %u Hz", carrier_freq);
}
int ir_send_burst_us(uint32_t burst_us)
{
if (!device_is_ready(ir_pwm.dev)) {
return -ENODEV;
}
uint64_t period_ns = PWM_HZ(carrier_freq);
uint64_t duty_ns = period_ns / 2U; /* 50% duty cycle */
int ret = pwm_set_dt(&ir_pwm, period_ns, duty_ns);
if (ret != 0) {
LOG_ERR("Failed to enable PWM burst: %d", ret);
return ret;
}
k_usleep(burst_us);
/* Stop carrier after burst */
ret = pwm_set_dt(&ir_pwm, period_ns, 0);
if (ret != 0) {
LOG_ERR("Failed to stop PWM after burst: %d", ret);
}
return ret;
}

View File

@@ -6,7 +6,7 @@
],
"settings": {
"nrf-connect.applications": [
"${workspaceFolder}/firmware/apps/leader"
"${workspaceFolder}/firmware/apps/_samples/ir_send"
]
}
}