diff --git a/doc/docs/konzept/software.md b/doc/docs/konzept/software.md
index 603adcf..95ae0fd 100644
--- a/doc/docs/konzept/software.md
+++ b/doc/docs/konzept/software.md
@@ -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.
diff --git a/doc/docs/planung.md b/doc/docs/planung.md
index 184e3f8..674544f 100644
--- a/doc/docs/planung.md
+++ b/doc/docs/planung.md
@@ -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.
diff --git a/doc/docs/specifications/ir_protocol.md b/doc/docs/specifications/ir_protocol.md
new file mode 100644
index 0000000..e37d19f
--- /dev/null
+++ b/doc/docs/specifications/ir_protocol.md
@@ -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 (25–75 %) |
+| 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
600 µs"] space1["Space 1
1200 µs"]:2 mark2["Mark
600 µs"] space2["Space 0
600 µs"] mark3["Mark
600 µs"] space3["Space 1
1200 µs"]:2
+ bit1["1"]:3 bit2["0"]:2 bit3["1"]: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 |
+|-----------|------|-------------|-------------|
+| 0–7 | Shooter ID | 0–255 | ID des Schützen (256 mögliche Spieler) |
+| 8–12 | Damage | 0–31 | Schadenpunkte (0 = kein Schaden, 31 = Maximum) |
+
+#### Heal-Frame (`001`)
+
+| Bit-Range | Feld | Wertbereich | Beschreibung |
+|-----------|------|-------------|-------------|
+| 0–7 | Healer ID | 0–255 | ID des Heilers (Medic oder Station) |
+| 8–12 | Amount | 0–31 | Heilpunkte wiederhergestellt |
+
+#### PowerUp-Frame (`010`)
+
+| Bit-Range | Feld | Wertbereich | Beschreibung |
+|-----------|------|-------------|-------------|
+| 0–7 | Station ID | 0–255 | Stations-ID, die das Power-Up gewährt |
+| 8–12 | PowerUp | 0–31 | Power-Up-Typ-Identifier |
+
+#### Admin-Frame (`011`)
+
+| Bit-Range | Feld | Wertbereich | Beschreibung |
+|-----------|------|-------------|-------------|
+| 0–12 | Command Data | 0–8191 | 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 (2000–2800 µ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 (±17–33 %) 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 (30–45 kHz)
+- `CONFIG_IR_SEND_DUTY_CYCLE_PERCENT`: PWM Tastgrad (25–75 %)
+- `CONFIG_IR_SEND_MARK_US`: Mark-Dauer (300–1000 µs)
+- `CONFIG_IR_SEND_SPACE0_US`: Space für Bit 0 (300–1000 µs)
+- `CONFIG_IR_SEND_SPACE1_US`: Space für Bit 1 (800–2000 µs)
+- `CONFIG_IR_SEND_START_BURST_US`: Start Burst (1500–4000 µ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) | ~5–10 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.*
+
diff --git a/doc/mkdocs.yml b/doc/mkdocs.yml
index 73c5978..41d703f 100644
--- a/doc/mkdocs.yml
+++ b/doc/mkdocs.yml
@@ -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
diff --git a/firmware/apps/_samples/ir_send/CMakeLists.txt b/firmware/apps/_samples/ir_send/CMakeLists.txt
new file mode 100644
index 0000000..f81f13a
--- /dev/null
+++ b/firmware/apps/_samples/ir_send/CMakeLists.txt
@@ -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)
diff --git a/firmware/apps/_samples/ir_send/Kconfig b/firmware/apps/_samples/ir_send/Kconfig
new file mode 100644
index 0000000..69023b7
--- /dev/null
+++ b/firmware/apps/_samples/ir_send/Kconfig
@@ -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.
diff --git a/firmware/apps/_samples/ir_send/boards/nrf52840dk_nrf52840.overlay b/firmware/apps/_samples/ir_send/boards/nrf52840dk_nrf52840.overlay
new file mode 120000
index 0000000..39e2587
--- /dev/null
+++ b/firmware/apps/_samples/ir_send/boards/nrf52840dk_nrf52840.overlay
@@ -0,0 +1 @@
+../../../../boards/nrf52840dk/nrf52840dk_nrf52840.overlay
\ No newline at end of file
diff --git a/firmware/apps/_samples/ir_send/prj.conf b/firmware/apps/_samples/ir_send/prj.conf
new file mode 100644
index 0000000..eab55ba
--- /dev/null
+++ b/firmware/apps/_samples/ir_send/prj.conf
@@ -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
diff --git a/firmware/apps/_samples/ir_send/src/main.c b/firmware/apps/_samples/ir_send/src/main.c
new file mode 100644
index 0000000..aff4491
--- /dev/null
+++ b/firmware/apps/_samples/ir_send/src/main.c
@@ -0,0 +1,37 @@
+/*
+ * ir_send sample app - IR transmission test
+ */
+
+#include
+#include
+
+#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);
+ }
+}
+
diff --git a/firmware/apps/leader/CMakeLists.txt b/firmware/apps/leader/CMakeLists.txt
index a2daf85..87f1faf 100644
--- a/firmware/apps/leader/CMakeLists.txt
+++ b/firmware/apps/leader/CMakeLists.txt
@@ -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)
diff --git a/firmware/apps/weapon/CMakeLists.txt b/firmware/apps/weapon/CMakeLists.txt
index f25e480..9009ce9 100644
--- a/firmware/apps/weapon/CMakeLists.txt
+++ b/firmware/apps/weapon/CMakeLists.txt
@@ -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)
diff --git a/firmware/boards/nrf52840dk/nrf52840dk_nrf52840.overlay b/firmware/boards/nrf52840dk/nrf52840dk_nrf52840.overlay
new file mode 100644
index 0000000..b144684
--- /dev/null
+++ b/firmware/boards/nrf52840dk/nrf52840dk_nrf52840.overlay
@@ -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 = ;
+ };
+ };
+};
+
diff --git a/firmware/libs/CMakeLists.txt b/firmware/libs/CMakeLists.txt
index 5174dc6..6fe1643 100644
--- a/firmware/libs/CMakeLists.txt
+++ b/firmware/libs/CMakeLists.txt
@@ -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)
\ No newline at end of file
+add_subdirectory(lasertag_utils)
+add_subdirectory(ir_send)
\ No newline at end of file
diff --git a/firmware/libs/Kconfig b/firmware/libs/Kconfig
index 82a4aee..8748f87 100644
--- a/firmware/libs/Kconfig
+++ b/firmware/libs/Kconfig
@@ -1,4 +1,5 @@
# Main entry point for custom project Kconfigs
rsource "lasertag_utils/Kconfig"
rsource "thread_mgmt/Kconfig"
-rsource "ble_mgmt/Kconfig"
\ No newline at end of file
+rsource "ble_mgmt/Kconfig"
+rsource "ir_send/Kconfig"
\ No newline at end of file
diff --git a/firmware/libs/ir_send/CMakeLists.txt b/firmware/libs/ir_send/CMakeLists.txt
new file mode 100644
index 0000000..956ee18
--- /dev/null
+++ b/firmware/libs/ir_send/CMakeLists.txt
@@ -0,0 +1,5 @@
+if(CONFIG_IR_SEND)
+ zephyr_library()
+ zephyr_sources(src/ir_send.c)
+ zephyr_include_directories(include)
+endif()
diff --git a/firmware/libs/ir_send/Kconfig b/firmware/libs/ir_send/Kconfig
new file mode 100644
index 0000000..4165bd4
--- /dev/null
+++ b/firmware/libs/ir_send/Kconfig
@@ -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
diff --git a/firmware/libs/ir_send/include/ir_send.h b/firmware/libs/ir_send/include/ir_send.h
new file mode 100644
index 0000000..f1f1695
--- /dev/null
+++ b/firmware/libs/ir_send/include/ir_send.h
@@ -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
+
+/**
+ * @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 */
diff --git a/firmware/libs/ir_send/src/ir_send.c b/firmware/libs/ir_send/src/ir_send.c
new file mode 100644
index 0000000..d53652b
--- /dev/null
+++ b/firmware/libs/ir_send/src/ir_send.c
@@ -0,0 +1,78 @@
+/*
+ * IR Send Library - PWM-based carrier generation
+ */
+
+#include
+#include
+#include
+
+#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;
+}
diff --git a/lasertag.code-workspace b/lasertag.code-workspace
index c7dc82f..0fb9e7c 100644
--- a/lasertag.code-workspace
+++ b/lasertag.code-workspace
@@ -6,7 +6,7 @@
],
"settings": {
"nrf-connect.applications": [
- "${workspaceFolder}/firmware/apps/leader"
+ "${workspaceFolder}/firmware/apps/_samples/ir_send"
]
}
}
\ No newline at end of file