Compare commits

...

52 Commits

Author SHA1 Message Date
1a589e104c Sync while working on OT
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-18 14:37:32 +01:00
07127fb074 Sync while working on OT
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-17 15:02:46 +01:00
17cc33332d Sync while working on OT
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-17 14:23:28 +01:00
5406c9917f Sync while working on OT
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-17 13:44:03 +01:00
37d1bd6db1 Sync while working on OT
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-17 13:39:18 +01:00
8c15260166 Sync while working on OT
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 13s
2026-02-17 12:52:32 +01:00
6b36435759 Fixed priority ranges
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-17 11:06:01 +01:00
675a99930a Implemented stable IR receiving over SAADC with PPI timing
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 13s
2026-02-16 17:20:22 +01:00
f012ce9fe0 sync during ir_recv dev
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-16 17:09:15 +01:00
2a4d007805 sync during ir_recv dev
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-16 16:25:07 +01:00
93b7c5fe9e sync during ir_recv dev
Some checks failed
Deploy Docs / build-and-deploy (push) Has been cancelled
2026-02-16 16:25:00 +01:00
1ce021c76f Implemented stable IR receiving
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-16 15:49:05 +01:00
480baa97fa sync during ir_recv dev
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-16 15:06:27 +01:00
8d97a5d33f sync during ir_recv dev
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-16 15:02:53 +01:00
f5b1849ada sync during ir_recv dev
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-16 14:59:42 +01:00
0113edb816 sync during ir_recv dev
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-16 14:34:02 +01:00
a1b047f4bb sync during ir_recv dev
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-16 14:20:14 +01:00
d51ff27e26 sync during ir_recv dev
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-16 14:18:21 +01:00
ac477e290e sync during ir_recv dev
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-16 14:15:18 +01:00
4906dc31eb sync during ir_recv dev
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-16 14:14:34 +01:00
be48e8ada7 sync during ir_recv dev
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-16 14:12:27 +01:00
918092cd9f sync during ir_recv dev
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-16 14:09:18 +01:00
b1f5578be9 sync during ir_recv dev
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-16 14:02:03 +01:00
9c1d19af67 sync during ir_recv dev
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-16 13:39:45 +01:00
6376185622 sync during ir_recv dev
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-16 13:31:49 +01:00
3febb6411e Added CRC8, implemented testing sample
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-16 12:45:50 +01:00
0c3a8bfa39 Improved audio lib and test app
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-16 11:38:19 +01:00
8305adf917 Improved audio lib error handling and aborting playback
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-15 20:40:06 +01:00
fae79ad8b0 Added audio libs
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 13s
2026-02-15 18:47:37 +01:00
794d8e36c9 Added mcumgr/smp v2 commands for listing and deleting files
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-12 11:26:43 +01:00
0785d9a755 Fixed lfs
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-11 21:05:33 +01:00
11ad746f04 Added audio generation
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-11 20:58:35 +01:00
149a142c22 Added upload scipt
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-11 15:27:32 +01:00
e84efc2e8c Added lfs generation tool
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-11 14:16:52 +01:00
76ac36a59b Added mcumgr snippet
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-11 12:06:22 +01:00
5e7a817e03 feat: move configs into libs, add filesystem support
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-02-10 18:14:43 +01:00
1b8d3e17b8 Moved config options from prj to libs
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 13s
2026-02-10 08:24:41 +01:00
93a7da7855 Fix: Network key persistence in Thread dataset
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 19s
- Network key was being reset to 0x00... after dataset updates
- Root cause: otDatasetSetActive() was overwriting the network key
- Solution: Call otThreadSetNetworkKey() before otDatasetSetActive()
  and read the key back to ensure it's included in the persistent dataset
- Verified: Network key now survives both software and hardware reboots
- Vest joins network as router in <1.1s after reboot
2026-01-14 17:16:17 +01:00
fb4578ac51 Zwischenstand vor refactor 2026-01-14 15:58:45 +01:00
63f8f2aaac Sync
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 27s
2026-01-13 16:46:12 +01:00
55b5421671 Added led board
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 23s
2026-01-13 16:24:10 +01:00
bd0a8cce8d Lobby Screen
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 21s
2026-01-13 15:05:52 +01:00
832a60d044 Added game mgmt, thread restart
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 24s
2026-01-13 13:23:40 +01:00
a041d5a49c Umbau Thread-Config an/vom leader in einem paket
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-01-13 10:17:53 +01:00
38396738a6 Arbeiten an der BT-Kommunikation, stand vor umbau
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 22s
2026-01-13 08:22:38 +01:00
65688b7b99 Fixed BLE Scan activation after Disconect
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 23s
2026-01-12 16:18:42 +01:00
395d577b78 BLE Handling 2026-01-12 15:10:54 +01:00
c13b6d73c9 Advencing bluetooth mist :)
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 17s
2026-01-12 14:29:09 +01:00
6b1bbca992 sync
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 21s
2026-01-12 12:21:00 +01:00
9d5dad0e8d BLE Scan implemented
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
2026-01-12 11:57:47 +01:00
a6bedb6b79 Bluetooth-Try
Some checks failed
Deploy Docs / build-and-deploy (push) Failing after 35s
2026-01-12 11:45:58 +01:00
e460aac7a1 Refactoring um Anzeige und Logik zu trennen
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 15s
2026-01-12 11:27:36 +01:00
132 changed files with 15175 additions and 1076 deletions

View File

@@ -0,0 +1 @@
build*/

View File

@@ -0,0 +1,10 @@
cmake_minimum_required(VERSION 3.20.0)
# 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(_mcumgr)
target_sources(app PRIVATE src/main.c)

View File

@@ -0,0 +1,44 @@
// To get started, press Ctrl+Space (or Option+Esc) to bring up the completion menu and view the available nodes.
// You can also use the buttons in the sidebar to perform actions on nodes.
// Actions currently available include:
// * Enabling / disabling the node
// * Adding the bus to a bus
// * Removing the node
// * Connecting ADC channels
// For more help, browse the DeviceTree documentation at https://docs.zephyrproject.org/latest/guides/dts/index.html
// You can also visit the nRF DeviceTree extension documentation at https://docs.nordicsemi.com/bundle/nrf-connect-vscode/page/guides/ncs_configure_app.html#devicetree-support-in-the-extension
/ {
chosen {
nordic,pm-ext-flash = &mx25r64;
};
};
&pinctrl {
i2s0_default: i2s0_default {
group1 {
psels = <NRF_PSEL(I2S_SCK_M, 0, 31)>, /* SCK Pin */
<NRF_PSEL(I2S_LRCK_M, 0, 30)>, /* WS/LRCK Pin */
<NRF_PSEL(I2S_SDOUT, 0, 29)>; /* SD Pin (DIN am MAX) */
};
};
i2s0_sleep: i2s0_sleep {
group1 {
psels = <NRF_PSEL(I2S_SCK_M, 0, 31)>,
<NRF_PSEL(I2S_LRCK_M, 0, 30)>,
<NRF_PSEL(I2S_SDOUT, 0, 29)>;
low-power-enable;
};
};
};
&i2s0 {
status = "okay";
pinctrl-0 = <&i2s0_default>;
pinctrl-1 = <&i2s0_sleep>;
pinctrl-names = "default", "sleep";
};

View File

@@ -0,0 +1,4 @@
littlefs_storage:
address: 0x0
size: 0x800000
region: external_flash

View File

@@ -0,0 +1,30 @@
CONFIG_LOG=y
# UART basics
CONFIG_SERIAL=y
CONFIG_UART_INTERRUPT_DRIVEN=y
# Shell configuration
CONFIG_SHELL=y
CONFIG_SHELL_BACKEND_SERIAL=y
# # MCU Manager
# CONFIG_NET_BUF=y
# CONFIG_ZCBOR=y
# CONFIG_MCUMGR=y
# CONFIG_BASE64=y
# CONFIG_CRC=y
# CONFIG_MCUMGR_TRANSPORT_SHELL=y
# # MCUMGR groups
# CONFIG_MCUMGR_GRP_OS=y
# CONFIG_MCUMGR_GRP_OS_ECHO=y
# CONFIG_MCUMGR_GRP_FS=y
# CONFIG_MCUMGR_GRP_FS_CHECKSUM_HASH=y
# Lasertag-specific configuration
CONFIG_LASERTAG_UTILS=y
CONFIG_FS_MGMT=y
CONFIG_FS_MGMT_LOG_LEVEL_DBG=n
CONFIG_AUDIO=y
CONFIG_AUDIO_LOG_LEVEL_DBG=y

View File

@@ -0,0 +1,68 @@
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/logging/log_ctrl.h>
#include <fs_mgmt.h>
#include <audio.h>
#include <hal/nrf_i2s.h>
#include <lasertag_utils.h>
LOG_MODULE_REGISTER(MMS, LOG_LEVEL_INF);
int main(void)
{
LOG_INF("Starting Audio Sample Application...");
LOG_INF("Sleeping for one second to allow thread analyzer to initialize and log thread states before we start the test.");
k_sleep(K_MSEC(1000));
int err;
LOG_INF("Audio test snippet");
err = fs_mgmt_init();
if (err)
{
LOG_ERR("Failed to initialize fs_mgmt: %d", err);
return err;
}
err = audio_init();
if (err)
{
LOG_ERR("Failed to initialize audio: %d", err);
return err;
}
LOG_INF("Triggering NULL file playback to test error handling...");
audio_play_file(NULL);
while(log_process());
LOG_INF("Triggering NULL sound to test error handling...");
audio_play_sound(NULL);
while(log_process());
LOG_INF("Triggering nonexistent sound to test error handling...");
audio_play_sound("nonexistent_file");
k_sleep(K_MSEC(100));
while(log_process());
LOG_INF("Triggering very long file name to test error handling...");
audio_play_sound("very_long_file_name_that_exceeds_the_maximum_length_allowed_by_the_system_to_test_error_handling");
while(log_process());
LOG_INF("Triggering first sound...");
audio_play_sound("s1");
k_sleep(K_MSEC(100));
audio_stop();
LOG_INF("Triggering second sound after abort...");
audio_play_sound("s1");
k_sleep(K_MSEC(100));
// Directly stop the I2S peripheral to simulate an abrupt stop that
// might occur with a DMA failure or similar issue. This will cause the
// next playback attempt to hit the slab timeout and trigger the I2S
// reset logic in the audio thread.
LOG_INF("Simulating failure by stopping I2S directly...");
NRF_I2S0->TASKS_STOP = 1;
NRF_I2S0->ENABLE = 0;
LOG_INF("Triggering third sound after failure simulation...");
audio_play_sound("s1");
LOG_INF(FORMAT_GREEN_BOLD("If you made it to this point, the test completed successfully and everything should work fine!"));
LOG_INF(FORMAT_BRIGHT_BOLD("More output might follow due to the async nature of the audio playback."));
return 0;
}

View File

@@ -0,0 +1,2 @@
pyserial
cbor2

View File

@@ -0,0 +1,120 @@
import serial
import base64
import cbor2
import struct
import time
import argparse
import sys
# Icons (NerdFont / Emoji)
ICON_DIR = "📁"
ICON_FILE = "📄"
class nRF_FS_Client:
def __init__(self, port, baud):
try:
self.ser = serial.Serial(port, baud, timeout=0.2)
self.seq = 0
self.ser.reset_input_buffer()
except serial.SerialException as e:
print(f"Error: Could not open {port} ({e})")
sys.exit(1)
def crc16(self, data):
crc = 0x0000
for byte in data:
crc ^= (byte << 8)
for _ in range(8):
if crc & 0x8000:
crc = (crc << 1) ^ 0x1021
else:
crc = crc << 1
crc &= 0xFFFF
return crc
def build_packet(self, group, cmd, payload):
self.seq = (self.seq + 1) % 256
cbor_payload = cbor2.dumps(payload)
header = struct.pack(">BBHHBB", 0x00, 0x08, len(cbor_payload), group, self.seq, cmd)
full_body = header + cbor_payload
checksum = self.crc16(full_body)
full_msg = full_body + struct.pack(">H", checksum)
return struct.pack(">H", len(full_msg)) + full_msg
def request(self, group, cmd, payload):
packet = self.build_packet(group, cmd, payload)
b64_data = base64.b64encode(packet).decode()
self.ser.write(f"\x06\t{b64_data}\n".encode())
full_response_b64 = ""
expected_len = -1
start_time = time.time()
while (time.time() - start_time) < 3.0:
line = self.ser.readline().strip()
if not line:
continue
is_smp = line.startswith(b'\x06\t') or line.startswith(b'\x06\n')
is_cont_special = line.startswith(b'\x04\x14') and expected_len > 0
if is_smp or is_cont_special:
full_response_b64 += line[2:].decode()
try:
raw_data = base64.b64decode(full_response_b64)
if expected_len == -1 and len(raw_data) >= 2:
expected_len = struct.unpack(">H", raw_data[:2])[0]
if expected_len != -1 and len(raw_data) >= expected_len + 2:
if raw_data[8] == self.seq:
return cbor2.loads(raw_data[10:-2])
except:
continue
return None
def list_recursive(self, path="/", prefix=""):
res = self.request(64, 0, {"path": path})
if res is None or 'files' not in res:
return
# Sorting: directories first, then names
entries = sorted(res['files'], key=lambda x: (x.get('t', 'f') != 'd', x['n']))
count = len(entries)
for i, entry in enumerate(entries):
is_last = (i == count - 1)
name = entry['n']
is_dir = entry.get('t', 'f').startswith('d')
# Line style selection
# connector = "└── " if is_last else "├── "
connector = "└─ " if is_last else "├─ "
print(f"{prefix}{connector}{ICON_DIR if is_dir else ICON_FILE} {name}")
if is_dir:
# Extend prefix for the next level
extension = " " if is_last else ""
sub_path = f"{path}/{name}".replace("//", "/")
self.list_recursive(sub_path, prefix + extension)
def close(self):
if hasattr(self, 'ser') and self.ser.is_open:
self.ser.close()
def main():
parser = argparse.ArgumentParser(description="nRF52840 LittleFS Tree Tool")
parser.add_argument("port", help="Serial port (e.g. /dev/cu.usbmodem...)")
args = parser.parse_args()
client = nRF_FS_Client(args.port, 115200)
print(f"--- Directory tree on nRF ({args.port}) ---")
try:
# Initial call
client.list_recursive("/")
finally:
client.close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1 @@
build*/

View File

@@ -0,0 +1,10 @@
cmake_minimum_required(VERSION 3.20.0)
# 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(_mcumgr)
target_sources(app PRIVATE src/main.c)

View File

@@ -0,0 +1,51 @@
#include <zephyr/dt-bindings/adc/adc.h>
#include <zephyr/dt-bindings/adc/nrf-adc.h>
/ {
zephyr,user {
/* Mappt die logischen Kanäle 0-3 auf die physischen ADC-Knoten */
io-channels = <&adc 0>, <&adc 1>, <&adc 2>, <&adc 3>;
};
};
&adc {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
channel@0 {
reg = <0>;
zephyr,gain = "ADC_GAIN_1_4";
zephyr,reference = "ADC_REF_VDD_1_4";
zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 15)>;
zephyr,input-positive = <NRF_SAADC_AIN0>; /* Pin P0.02 */
zephyr,resolution = <12>;
};
channel@1 {
reg = <1>;
zephyr,gain = "ADC_GAIN_1_4";
zephyr,reference = "ADC_REF_VDD_1_4";
zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 15)>;
zephyr,input-positive = <NRF_SAADC_AIN1>; /* Pin P0.03 */
zephyr,resolution = <12>;
};
channel@2 {
reg = <2>;
zephyr,gain = "ADC_GAIN_1_4";
zephyr,reference = "ADC_REF_VDD_1_4";
zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 15)>;
zephyr,input-positive = <NRF_SAADC_AIN2>; /* Pin P0.04 */
zephyr,resolution = <12>;
};
channel@3 {
reg = <3>;
zephyr,gain = "ADC_GAIN_1_4";
zephyr,reference = "ADC_REF_VDD_1_4";
zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 15)>;
zephyr,input-positive = <NRF_SAADC_AIN3>; /* Pin P0.05 */
zephyr,resolution = <12>;
};
};

View File

@@ -0,0 +1,44 @@
// To get started, press Ctrl+Space (or Option+Esc) to bring up the completion menu and view the available nodes.
// You can also use the buttons in the sidebar to perform actions on nodes.
// Actions currently available include:
// * Enabling / disabling the node
// * Adding the bus to a bus
// * Removing the node
// * Connecting ADC channels
// For more help, browse the DeviceTree documentation at https://docs.zephyrproject.org/latest/guides/dts/index.html
// You can also visit the nRF DeviceTree extension documentation at https://docs.nordicsemi.com/bundle/nrf-connect-vscode/page/guides/ncs_configure_app.html#devicetree-support-in-the-extension
/ {
chosen {
nordic,pm-ext-flash = &mx25r64;
};
};
&pinctrl {
i2s0_default: i2s0_default {
group1 {
psels = <NRF_PSEL(I2S_SCK_M, 0, 31)>, /* SCK Pin */
<NRF_PSEL(I2S_LRCK_M, 0, 30)>, /* WS/LRCK Pin */
<NRF_PSEL(I2S_SDOUT, 0, 29)>; /* SD Pin (DIN am MAX) */
};
};
i2s0_sleep: i2s0_sleep {
group1 {
psels = <NRF_PSEL(I2S_SCK_M, 0, 31)>,
<NRF_PSEL(I2S_LRCK_M, 0, 30)>,
<NRF_PSEL(I2S_SDOUT, 0, 29)>;
low-power-enable;
};
};
};
&i2s0 {
status = "okay";
pinctrl-0 = <&i2s0_default>;
pinctrl-1 = <&i2s0_sleep>;
pinctrl-names = "default", "sleep";
};

View File

@@ -0,0 +1,4 @@
littlefs_storage:
address: 0x0
size: 0x800000
region: external_flash

View File

@@ -0,0 +1,25 @@
CONFIG_LOG=y
# UART-Grundlagen
CONFIG_SERIAL=y
CONFIG_UART_INTERRUPT_DRIVEN=y
# Shell-Konfiguration
CONFIG_SHELL=y
CONFIG_SHELL_BACKEND_SERIAL=y
# Lasertag-spezifische Konfiguration
CONFIG_LASERTAG_UTILS=y
CONFIG_IR_RECV=y
CONFIG_IR_RECV_LOG_LEVEL_DBG=y
# UART basics
# Thread Analyzer aktivieren
CONFIG_THREAD_ANALYZER=y
# Shell configuration
CONFIG_THREAD_ANALYZER_AUTO_INTERVAL=5
# CPU-Laufzeit-Statistiken aktivieren
CONFIG_THREAD_RUNTIME_STATS=y
# Lasertag-specific configuration

View File

@@ -0,0 +1,15 @@
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <lasertag_utils.h>
#include <ir_recv.h>
LOG_MODULE_REGISTER(ir_recv_adc, LOG_LEVEL_INF);
int main(void)
{
LOG_INF("Starting IR receive ADC application...");
lasertag_utils_init();
ir_recv_init();
return 0;
}

View File

@@ -0,0 +1,2 @@
pyserial
cbor2

View File

@@ -0,0 +1,120 @@
import serial
import base64
import cbor2
import struct
import time
import argparse
import sys
# Icons (NerdFont / Emoji)
ICON_DIR = "📁"
ICON_FILE = "📄"
class nRF_FS_Client:
def __init__(self, port, baud):
try:
self.ser = serial.Serial(port, baud, timeout=0.2)
self.seq = 0
self.ser.reset_input_buffer()
except serial.SerialException as e:
print(f"Error: Could not open {port} ({e})")
sys.exit(1)
def crc16(self, data):
crc = 0x0000
for byte in data:
crc ^= (byte << 8)
for _ in range(8):
if crc & 0x8000:
crc = (crc << 1) ^ 0x1021
else:
crc = crc << 1
crc &= 0xFFFF
return crc
def build_packet(self, group, cmd, payload):
self.seq = (self.seq + 1) % 256
cbor_payload = cbor2.dumps(payload)
header = struct.pack(">BBHHBB", 0x00, 0x08, len(cbor_payload), group, self.seq, cmd)
full_body = header + cbor_payload
checksum = self.crc16(full_body)
full_msg = full_body + struct.pack(">H", checksum)
return struct.pack(">H", len(full_msg)) + full_msg
def request(self, group, cmd, payload):
packet = self.build_packet(group, cmd, payload)
b64_data = base64.b64encode(packet).decode()
self.ser.write(f"\x06\t{b64_data}\n".encode())
full_response_b64 = ""
expected_len = -1
start_time = time.time()
while (time.time() - start_time) < 3.0:
line = self.ser.readline().strip()
if not line:
continue
is_smp = line.startswith(b'\x06\t') or line.startswith(b'\x06\n')
is_cont_special = line.startswith(b'\x04\x14') and expected_len > 0
if is_smp or is_cont_special:
full_response_b64 += line[2:].decode()
try:
raw_data = base64.b64decode(full_response_b64)
if expected_len == -1 and len(raw_data) >= 2:
expected_len = struct.unpack(">H", raw_data[:2])[0]
if expected_len != -1 and len(raw_data) >= expected_len + 2:
if raw_data[8] == self.seq:
return cbor2.loads(raw_data[10:-2])
except:
continue
return None
def list_recursive(self, path="/", prefix=""):
res = self.request(64, 0, {"path": path})
if res is None or 'files' not in res:
return
# Sorting: directories first, then names
entries = sorted(res['files'], key=lambda x: (x.get('t', 'f') != 'd', x['n']))
count = len(entries)
for i, entry in enumerate(entries):
is_last = (i == count - 1)
name = entry['n']
is_dir = entry.get('t', 'f').startswith('d')
# Line style selection
# connector = "└── " if is_last else "├── "
connector = "└─ " if is_last else "├─ "
print(f"{prefix}{connector}{ICON_DIR if is_dir else ICON_FILE} {name}")
if is_dir:
# Extend prefix for the next level
extension = " " if is_last else ""
sub_path = f"{path}/{name}".replace("//", "/")
self.list_recursive(sub_path, prefix + extension)
def close(self):
if hasattr(self, 'ser') and self.ser.is_open:
self.ser.close()
def main():
parser = argparse.ArgumentParser(description="nRF52840 LittleFS Tree Tool")
parser.add_argument("port", help="Serial port (e.g. /dev/cu.usbmodem...)")
args = parser.parse_args()
client = nRF_FS_Client(args.port, 115200)
print(f"--- Directory tree on nRF ({args.port}) ---")
try:
# Initial call
client.list_recursive("/")
finally:
client.close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1 @@
build*/

View File

@@ -0,0 +1,10 @@
cmake_minimum_required(VERSION 3.20.0)
# 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(_mcumgr)
target_sources(app PRIVATE src/main.c)

View File

@@ -0,0 +1,44 @@
// To get started, press Ctrl+Space (or Option+Esc) to bring up the completion menu and view the available nodes.
// You can also use the buttons in the sidebar to perform actions on nodes.
// Actions currently available include:
// * Enabling / disabling the node
// * Adding the bus to a bus
// * Removing the node
// * Connecting ADC channels
// For more help, browse the DeviceTree documentation at https://docs.zephyrproject.org/latest/guides/dts/index.html
// You can also visit the nRF DeviceTree extension documentation at https://docs.nordicsemi.com/bundle/nrf-connect-vscode/page/guides/ncs_configure_app.html#devicetree-support-in-the-extension
/ {
chosen {
nordic,pm-ext-flash = &mx25r64;
};
};
&pinctrl {
i2s0_default: i2s0_default {
group1 {
psels = <NRF_PSEL(I2S_SCK_M, 0, 31)>, /* SCK Pin */
<NRF_PSEL(I2S_LRCK_M, 0, 30)>, /* WS/LRCK Pin */
<NRF_PSEL(I2S_SDOUT, 0, 29)>; /* SD Pin (DIN am MAX) */
};
};
i2s0_sleep: i2s0_sleep {
group1 {
psels = <NRF_PSEL(I2S_SCK_M, 0, 31)>,
<NRF_PSEL(I2S_LRCK_M, 0, 30)>,
<NRF_PSEL(I2S_SDOUT, 0, 29)>;
low-power-enable;
};
};
};
&i2s0 {
status = "okay";
pinctrl-0 = <&i2s0_default>;
pinctrl-1 = <&i2s0_sleep>;
pinctrl-names = "default", "sleep";
};

View File

@@ -0,0 +1,4 @@
littlefs_storage:
address: 0x0
size: 0x800000
region: external_flash

View File

@@ -0,0 +1,26 @@
CONFIG_LOG=y
# UART basics
CONFIG_SERIAL=y
CONFIG_UART_INTERRUPT_DRIVEN=y
# Shell configuration
CONFIG_SHELL=y
CONFIG_SHELL_BACKEND_SERIAL=y
CONFIG_CPLUSPLUS=y
# Lasertag-specific configuration
CONFIG_LASERTAG_UTILS=y
CONFIG_IR_RECV=y
CONFIG_IR_RECV_LOG_LEVEL_INF=y
CONFIG_IR_RECV_SIMULATOR=y
# Enable Thread analyzer
CONFIG_THREAD_ANALYZER=y
CONFIG_THREAD_ANALYZER_AUTO=y
CONFIG_THREAD_ANALYZER_AUTO_INTERVAL=5
# Enable CPU runtime statistics
CONFIG_THREAD_RUNTIME_STATS=y
CONFIG_THREAD_RUNTIME_STATS_USE_TIMING_FUNCTIONS=y

View File

@@ -0,0 +1,31 @@
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <lasertag_utils.h>
#include <ir_recv.h>
LOG_MODULE_REGISTER(ir_recv_sim, LOG_LEVEL_INF);
int main(void)
{
LOG_INF("Starting IR receive simulator application...");
lasertag_utils_init();
ir_recv_init();
ir_packet_t test_packet = {0};
/* Test 1: Perfektes Signal */
LOG_INF("Sending perfect packet...");
test_packet.data.fields.type = 1;
test_packet.data.fields.value = 15;
uint8_t id = 0;
for(;;)
{
test_packet.data.fields.id = id++;
ir_recv_sim_send_packet(&test_packet, NULL);
k_sleep(K_MSEC(300));
}
return 0;
}

View File

@@ -0,0 +1,2 @@
pyserial
cbor2

View File

@@ -0,0 +1,120 @@
import serial
import base64
import cbor2
import struct
import time
import argparse
import sys
# Icons (NerdFont / Emoji)
ICON_DIR = "📁"
ICON_FILE = "📄"
class nRF_FS_Client:
def __init__(self, port, baud):
try:
self.ser = serial.Serial(port, baud, timeout=0.2)
self.seq = 0
self.ser.reset_input_buffer()
except serial.SerialException as e:
print(f"Error: Could not open {port} ({e})")
sys.exit(1)
def crc16(self, data):
crc = 0x0000
for byte in data:
crc ^= (byte << 8)
for _ in range(8):
if crc & 0x8000:
crc = (crc << 1) ^ 0x1021
else:
crc = crc << 1
crc &= 0xFFFF
return crc
def build_packet(self, group, cmd, payload):
self.seq = (self.seq + 1) % 256
cbor_payload = cbor2.dumps(payload)
header = struct.pack(">BBHHBB", 0x00, 0x08, len(cbor_payload), group, self.seq, cmd)
full_body = header + cbor_payload
checksum = self.crc16(full_body)
full_msg = full_body + struct.pack(">H", checksum)
return struct.pack(">H", len(full_msg)) + full_msg
def request(self, group, cmd, payload):
packet = self.build_packet(group, cmd, payload)
b64_data = base64.b64encode(packet).decode()
self.ser.write(f"\x06\t{b64_data}\n".encode())
full_response_b64 = ""
expected_len = -1
start_time = time.time()
while (time.time() - start_time) < 3.0:
line = self.ser.readline().strip()
if not line:
continue
is_smp = line.startswith(b'\x06\t') or line.startswith(b'\x06\n')
is_cont_special = line.startswith(b'\x04\x14') and expected_len > 0
if is_smp or is_cont_special:
full_response_b64 += line[2:].decode()
try:
raw_data = base64.b64decode(full_response_b64)
if expected_len == -1 and len(raw_data) >= 2:
expected_len = struct.unpack(">H", raw_data[:2])[0]
if expected_len != -1 and len(raw_data) >= expected_len + 2:
if raw_data[8] == self.seq:
return cbor2.loads(raw_data[10:-2])
except:
continue
return None
def list_recursive(self, path="/", prefix=""):
res = self.request(64, 0, {"path": path})
if res is None or 'files' not in res:
return
# Sorting: directories first, then names
entries = sorted(res['files'], key=lambda x: (x.get('t', 'f') != 'd', x['n']))
count = len(entries)
for i, entry in enumerate(entries):
is_last = (i == count - 1)
name = entry['n']
is_dir = entry.get('t', 'f').startswith('d')
# Line style selection
# connector = "└── " if is_last else "├── "
connector = "└─ " if is_last else "├─ "
print(f"{prefix}{connector}{ICON_DIR if is_dir else ICON_FILE} {name}")
if is_dir:
# Extend prefix for the next level
extension = " " if is_last else ""
sub_path = f"{path}/{name}".replace("//", "/")
self.list_recursive(sub_path, prefix + extension)
def close(self):
if hasattr(self, 'ser') and self.ser.is_open:
self.ser.close()
def main():
parser = argparse.ArgumentParser(description="nRF52840 LittleFS Tree Tool")
parser.add_argument("port", help="Serial port (e.g. /dev/cu.usbmodem...)")
args = parser.parse_args()
client = nRF_FS_Client(args.port, 115200)
print(f"--- Directory tree on nRF ({args.port}) ---")
try:
# Initial call
client.list_recursive("/")
finally:
client.close()
if __name__ == "__main__":
main()

Submodule firmware/apps/_samples/mcumgr added at b9c1c03f6e

View File

@@ -0,0 +1 @@
build*/

View File

@@ -0,0 +1,10 @@
cmake_minimum_required(VERSION 3.20.0)
# 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(_mcumgr)
target_sources(app PRIVATE src/main.c)

View File

@@ -0,0 +1,44 @@
// To get started, press Ctrl+Space (or Option+Esc) to bring up the completion menu and view the available nodes.
// You can also use the buttons in the sidebar to perform actions on nodes.
// Actions currently available include:
// * Enabling / disabling the node
// * Adding the bus to a bus
// * Removing the node
// * Connecting ADC channels
// For more help, browse the DeviceTree documentation at https://docs.zephyrproject.org/latest/guides/dts/index.html
// You can also visit the nRF DeviceTree extension documentation at https://docs.nordicsemi.com/bundle/nrf-connect-vscode/page/guides/ncs_configure_app.html#devicetree-support-in-the-extension
/ {
chosen {
nordic,pm-ext-flash = &mx25r64;
};
};
&pinctrl {
i2s0_default: i2s0_default {
group1 {
psels = <NRF_PSEL(I2S_SCK_M, 0, 31)>, /* SCK Pin */
<NRF_PSEL(I2S_LRCK_M, 0, 30)>, /* WS/LRCK Pin */
<NRF_PSEL(I2S_SDOUT, 0, 29)>; /* SD Pin (DIN am MAX) */
};
};
i2s0_sleep: i2s0_sleep {
group1 {
psels = <NRF_PSEL(I2S_SCK_M, 0, 31)>,
<NRF_PSEL(I2S_LRCK_M, 0, 30)>,
<NRF_PSEL(I2S_SDOUT, 0, 29)>;
low-power-enable;
};
};
};
&i2s0 {
status = "okay";
pinctrl-0 = <&i2s0_default>;
pinctrl-1 = <&i2s0_sleep>;
pinctrl-names = "default", "sleep";
};

View File

@@ -0,0 +1,4 @@
littlefs_storage:
address: 0x0
size: 0x800000
region: external_flash

View File

@@ -0,0 +1,22 @@
CONFIG_LOG=y
# UART basics
CONFIG_SERIAL=y
CONFIG_UART_INTERRUPT_DRIVEN=y
# Shell configuration
CONFIG_SHELL_BACKEND_SERIAL=y
CONFIG_FILE_SYSTEM_SHELL=y
# Lasertag-specific configuration
CONFIG_BLE_MGMT=y
CONFIG_GAME_MGMT=y
CONFIG_GAME_MGMT_SHELL=y
CONFIG_GAME_MGMT_LOG_LEVEL_DBG=y
CONFIG_LASERTAG_ROLE_LEADER=y
CONFIG_THREAD_MGMT=y
CONFIG_THREAD_MGMT_LOG_LEVEL_DBG=y
CONFIG_THREAD_MGMT_SHELL=y
CONFIG_FS_MGMT=y
CONFIG_FS_MGMT_LOG_LEVEL_DBG=y
CONFIG_AUDIO_LOG_LEVEL_DBG=y

View File

@@ -0,0 +1,44 @@
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <thread_mgmt.h>
#include <game_mgmt.h>
#include <lasertag_utils.h>
#include <fs_mgmt.h>
#include <audio.h>
LOG_MODULE_REGISTER(OT_SAMPLE, LOG_LEVEL_INF);
int main(void)
{
LOG_INF("Starting Thread Management test application...");
lasertag_utils_init();
int rc = thread_mgmt_init();
if (rc < 0) {
LOG_ERR("Thread management initialization failed: %d", rc);
return rc;
}
LOG_INF("Thread management initialized successfully.");
rc = fs_mgmt_init();
if (rc < 0) {
LOG_ERR("File system management initialization failed: %d", rc);
return rc;
}
LOG_INF("File system management initialized successfully.");
rc = audio_init();
if (rc < 0) {
LOG_ERR("Audio initialization failed: %d", rc);
return rc;
}
LOG_INF("Audio initialized successfully.");
rc = game_mgmt_init();
if (rc < 0) {
LOG_ERR("Game management initialization failed: %d", rc);
return rc;
}
LOG_INF("Game management initialized successfully.");
return 0;
}

View File

@@ -0,0 +1,2 @@
pyserial
cbor2

View File

@@ -0,0 +1,120 @@
import serial
import base64
import cbor2
import struct
import time
import argparse
import sys
# Icons (NerdFont / Emoji)
ICON_DIR = "📁"
ICON_FILE = "📄"
class nRF_FS_Client:
def __init__(self, port, baud):
try:
self.ser = serial.Serial(port, baud, timeout=0.2)
self.seq = 0
self.ser.reset_input_buffer()
except serial.SerialException as e:
print(f"Error: Could not open {port} ({e})")
sys.exit(1)
def crc16(self, data):
crc = 0x0000
for byte in data:
crc ^= (byte << 8)
for _ in range(8):
if crc & 0x8000:
crc = (crc << 1) ^ 0x1021
else:
crc = crc << 1
crc &= 0xFFFF
return crc
def build_packet(self, group, cmd, payload):
self.seq = (self.seq + 1) % 256
cbor_payload = cbor2.dumps(payload)
header = struct.pack(">BBHHBB", 0x00, 0x08, len(cbor_payload), group, self.seq, cmd)
full_body = header + cbor_payload
checksum = self.crc16(full_body)
full_msg = full_body + struct.pack(">H", checksum)
return struct.pack(">H", len(full_msg)) + full_msg
def request(self, group, cmd, payload):
packet = self.build_packet(group, cmd, payload)
b64_data = base64.b64encode(packet).decode()
self.ser.write(f"\x06\t{b64_data}\n".encode())
full_response_b64 = ""
expected_len = -1
start_time = time.time()
while (time.time() - start_time) < 3.0:
line = self.ser.readline().strip()
if not line:
continue
is_smp = line.startswith(b'\x06\t') or line.startswith(b'\x06\n')
is_cont_special = line.startswith(b'\x04\x14') and expected_len > 0
if is_smp or is_cont_special:
full_response_b64 += line[2:].decode()
try:
raw_data = base64.b64decode(full_response_b64)
if expected_len == -1 and len(raw_data) >= 2:
expected_len = struct.unpack(">H", raw_data[:2])[0]
if expected_len != -1 and len(raw_data) >= expected_len + 2:
if raw_data[8] == self.seq:
return cbor2.loads(raw_data[10:-2])
except:
continue
return None
def list_recursive(self, path="/", prefix=""):
res = self.request(64, 0, {"path": path})
if res is None or 'files' not in res:
return
# Sorting: directories first, then names
entries = sorted(res['files'], key=lambda x: (x.get('t', 'f') != 'd', x['n']))
count = len(entries)
for i, entry in enumerate(entries):
is_last = (i == count - 1)
name = entry['n']
is_dir = entry.get('t', 'f').startswith('d')
# Line style selection
# connector = "└── " if is_last else "├── "
connector = "└─ " if is_last else "├─ "
print(f"{prefix}{connector}{ICON_DIR if is_dir else ICON_FILE} {name}")
if is_dir:
# Extend prefix for the next level
extension = " " if is_last else ""
sub_path = f"{path}/{name}".replace("//", "/")
self.list_recursive(sub_path, prefix + extension)
def close(self):
if hasattr(self, 'ser') and self.ser.is_open:
self.ser.close()
def main():
parser = argparse.ArgumentParser(description="nRF52840 LittleFS Tree Tool")
parser.add_argument("port", help="Serial port (e.g. /dev/cu.usbmodem...)")
args = parser.parse_args()
client = nRF_FS_Client(args.port, 115200)
print(f"--- Directory tree on nRF ({args.port}) ---")
try:
# Initial call
client.list_recursive("/")
finally:
client.close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1 @@
build*/

View File

@@ -0,0 +1,10 @@
cmake_minimum_required(VERSION 3.20.0)
# 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(_mcumgr)
target_sources(app PRIVATE src/main.c)

View File

@@ -0,0 +1,44 @@
// To get started, press Ctrl+Space (or Option+Esc) to bring up the completion menu and view the available nodes.
// You can also use the buttons in the sidebar to perform actions on nodes.
// Actions currently available include:
// * Enabling / disabling the node
// * Adding the bus to a bus
// * Removing the node
// * Connecting ADC channels
// For more help, browse the DeviceTree documentation at https://docs.zephyrproject.org/latest/guides/dts/index.html
// You can also visit the nRF DeviceTree extension documentation at https://docs.nordicsemi.com/bundle/nrf-connect-vscode/page/guides/ncs_configure_app.html#devicetree-support-in-the-extension
/ {
chosen {
nordic,pm-ext-flash = &mx25r64;
};
};
&pinctrl {
i2s0_default: i2s0_default {
group1 {
psels = <NRF_PSEL(I2S_SCK_M, 0, 31)>, /* SCK Pin */
<NRF_PSEL(I2S_LRCK_M, 0, 30)>, /* WS/LRCK Pin */
<NRF_PSEL(I2S_SDOUT, 0, 29)>; /* SD Pin (DIN am MAX) */
};
};
i2s0_sleep: i2s0_sleep {
group1 {
psels = <NRF_PSEL(I2S_SCK_M, 0, 31)>,
<NRF_PSEL(I2S_LRCK_M, 0, 30)>,
<NRF_PSEL(I2S_SDOUT, 0, 29)>;
low-power-enable;
};
};
};
&i2s0 {
status = "okay";
pinctrl-0 = <&i2s0_default>;
pinctrl-1 = <&i2s0_sleep>;
pinctrl-names = "default", "sleep";
};

View File

@@ -0,0 +1,4 @@
littlefs_storage:
address: 0x0
size: 0x800000
region: external_flash

View File

@@ -0,0 +1,14 @@
CONFIG_LOG=y
# UART basics
CONFIG_SERIAL=y
CONFIG_UART_INTERRUPT_DRIVEN=y
# Shell configuration
CONFIG_SHELL=y
CONFIG_SHELL_BACKEND_SERIAL=y
# Lasertag-specific configuration
CONFIG_LASERTAG_UTILS=y
CONFIG_LASERTAG_UTILS_LOG_LEVEL_DBG=y

View File

@@ -0,0 +1,24 @@
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <lasertag_utils.h>
LOG_MODULE_REGISTER(MMS, LOG_LEVEL_INF);
int main(void)
{
LOG_INF("Starting Utils test application...");
lasertag_utils_init();
uint8_t data[] = {0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe};
uint8_t crc = lastertag_crc8(data, sizeof(data));
LOG_INF("CRC8: 0x%02X", crc);
if (crc != 0xbe) {
LOG_ERR("CRC8 check failed!!");
} else {
LOG_INF("CRC8 check passed.");
}
LOG_INF(FORMAT_BLUE_BOLD("This should be in blue and bold if ANSI color codes are supported in the terminal."));
LOG_INF("Here, only a part should be " FORMAT_RED("red") " and the rest normal.");
LOG_INF(FORMAT_RED_BOLD("this ") FORMAT_GREEN_BOLD("is ") FORMAT_BLUE_BOLD("colorful") FORMAT_YELLOW_BOLD(" as") FORMAT_BRIGHT_BOLD(" fuck") "!");
return 0;
}

View File

@@ -0,0 +1,2 @@
pyserial
cbor2

View File

@@ -0,0 +1,120 @@
import serial
import base64
import cbor2
import struct
import time
import argparse
import sys
# Icons (NerdFont / Emoji)
ICON_DIR = "📁"
ICON_FILE = "📄"
class nRF_FS_Client:
def __init__(self, port, baud):
try:
self.ser = serial.Serial(port, baud, timeout=0.2)
self.seq = 0
self.ser.reset_input_buffer()
except serial.SerialException as e:
print(f"Error: Could not open {port} ({e})")
sys.exit(1)
def crc16(self, data):
crc = 0x0000
for byte in data:
crc ^= (byte << 8)
for _ in range(8):
if crc & 0x8000:
crc = (crc << 1) ^ 0x1021
else:
crc = crc << 1
crc &= 0xFFFF
return crc
def build_packet(self, group, cmd, payload):
self.seq = (self.seq + 1) % 256
cbor_payload = cbor2.dumps(payload)
header = struct.pack(">BBHHBB", 0x00, 0x08, len(cbor_payload), group, self.seq, cmd)
full_body = header + cbor_payload
checksum = self.crc16(full_body)
full_msg = full_body + struct.pack(">H", checksum)
return struct.pack(">H", len(full_msg)) + full_msg
def request(self, group, cmd, payload):
packet = self.build_packet(group, cmd, payload)
b64_data = base64.b64encode(packet).decode()
self.ser.write(f"\x06\t{b64_data}\n".encode())
full_response_b64 = ""
expected_len = -1
start_time = time.time()
while (time.time() - start_time) < 3.0:
line = self.ser.readline().strip()
if not line:
continue
is_smp = line.startswith(b'\x06\t') or line.startswith(b'\x06\n')
is_cont_special = line.startswith(b'\x04\x14') and expected_len > 0
if is_smp or is_cont_special:
full_response_b64 += line[2:].decode()
try:
raw_data = base64.b64decode(full_response_b64)
if expected_len == -1 and len(raw_data) >= 2:
expected_len = struct.unpack(">H", raw_data[:2])[0]
if expected_len != -1 and len(raw_data) >= expected_len + 2:
if raw_data[8] == self.seq:
return cbor2.loads(raw_data[10:-2])
except:
continue
return None
def list_recursive(self, path="/", prefix=""):
res = self.request(64, 0, {"path": path})
if res is None or 'files' not in res:
return
# Sorting: directories first, then names
entries = sorted(res['files'], key=lambda x: (x.get('t', 'f') != 'd', x['n']))
count = len(entries)
for i, entry in enumerate(entries):
is_last = (i == count - 1)
name = entry['n']
is_dir = entry.get('t', 'f').startswith('d')
# Line style selection
# connector = "└── " if is_last else "├── "
connector = "└─ " if is_last else "├─ "
print(f"{prefix}{connector}{ICON_DIR if is_dir else ICON_FILE} {name}")
if is_dir:
# Extend prefix for the next level
extension = " " if is_last else ""
sub_path = f"{path}/{name}".replace("//", "/")
self.list_recursive(sub_path, prefix + extension)
def close(self):
if hasattr(self, 'ser') and self.ser.is_open:
self.ser.close()
def main():
parser = argparse.ArgumentParser(description="nRF52840 LittleFS Tree Tool")
parser.add_argument("port", help="Serial port (e.g. /dev/cu.usbmodem...)")
args = parser.parse_args()
client = nRF_FS_Client(args.port, 115200)
print(f"--- Directory tree on nRF ({args.port}) ---")
try:
# Initial call
client.list_recursive("/")
finally:
client.close()
if __name__ == "__main__":
main()

Submodule firmware/apps/_samples/watchdog added at 10ca5017c5

View File

@@ -1,41 +1,29 @@
# Console and Logging
CONFIG_LOG=y
# Shell and Built-in Commands
CONFIG_SHELL=y
CONFIG_KERNEL_SHELL=y
CONFIG_DEVICE_SHELL=y
CONFIG_REBOOT=y
# UART basics
CONFIG_SERIAL=y
CONFIG_UART_INTERRUPT_DRIVEN=y
# --- STACK SIZE UPDATES (Fixes the Hard Fault) ---
CONFIG_MAIN_STACK_SIZE=4096
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048
CONFIG_BT_RX_STACK_SIZE=2048
# Shell configuration
CONFIG_SHELL_BACKEND_SERIAL=y
CONFIG_FILE_SYSTEM_SHELL=y
# Storage and Settings (NVS)
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y
CONFIG_SETTINGS=y
# Stack protection
CONFIG_HW_STACK_PROTECTION=y
CONFIG_STACK_SENTINEL=y
# Network and OpenThread
CONFIG_NETWORKING=y
CONFIG_NET_L2_OPENTHREAD=y
CONFIG_OPENTHREAD=y
CONFIG_OPENTHREAD_FTD=y
CONFIG_OPENTHREAD_SHELL=y
# --- CoAP & UDP Features ---
CONFIG_OPENTHREAD_COAP=y
CONFIG_OPENTHREAD_MANUAL_START=y
# Bluetooth
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="Lasertag-Device"
CONFIG_BT_DEVICE_NAME_DYNAMIC=y
# Enable Lasertag Shared Modules
CONFIG_LASERTAG_UTILS=y
# Lasertag-specific configuration
CONFIG_BLE_MGMT=y
CONFIG_GAME_MGMT=y
CONFIG_GAME_MGMT_SHELL=y
CONFIG_GAME_MGMT_LOG_LEVEL_DBG=y
CONFIG_THREAD_MGMT=y
CONFIG_BLE_MGMT=y
CONFIG_THREAD_MGMT_LOG_LEVEL_DBG=y
CONFIG_THREAD_MGMT_SHELL=y
CONFIG_FS_MGMT=y
CONFIG_FS_MGMT_LOG_LEVEL_DBG=y
CONFIG_AUDIO_LOG_LEVEL_DBG=y
CONFIG_LASERTAG_ROLE_LEADER=y
CONFIG_ENTROPY_GENERATOR=y

View File

@@ -1,46 +1,56 @@
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <lasertag_utils.h>
#include <thread_mgmt.h>
#include <ble_mgmt.h>
#include <game_mgmt.h>
#include <lasertag_utils.h>
#include <fs_mgmt.h>
#include <audio.h>
#include <zephyr/random/random.h>
LOG_MODULE_REGISTER(leader_app, CONFIG_LOG_DEFAULT_LEVEL);
LOG_MODULE_REGISTER(OT_SAMPLE, LOG_LEVEL_INF);
uint64_t generate_64bit_random(void) {
uint64_t rnd_val;
/* Füllt den Speicherbereich der Variable mit Zufallsbytes */
sys_csrand_get(&rnd_val, sizeof(rnd_val));
return rnd_val;
}
int main(void)
{
/* Initialize shared project logic and NVS */
LOG_INF("Starting Thread Management test application...");
lasertag_utils_init();
/* Initialize and start BLE management for provisioning */
int rc = ble_mgmt_init(LT_TYPE_LEADER);
if (rc) {
LOG_ERR("BLE initialization failed (err %d)", rc);
return rc;
} else {
LOG_INF("BLE Management initialized successfully.");
}
/* Start BLE advertising */
rc = ble_mgmt_adv_start();
if (rc) {
LOG_ERR("BLE advertising start failed (err %d)", rc);
} else {
LOG_INF("BLE advertising started.");
}
/* Initialize and start OpenThread stack */
rc = thread_mgmt_init();
if (rc) {
LOG_ERR("Thread initialization failed (err %d)", rc);
} else {
LOG_INF("Leader Application successfully started with Thread Mesh.");
int rc = thread_mgmt_init();
if (rc < 0) {
LOG_ERR("Thread management initialization failed: %d", rc);
return rc;
}
LOG_INF("Thread management initialized successfully.");
while (1) {
/* Main loop - handle high-level game logic here */
k_sleep(K_MSEC(1000));
rc = fs_mgmt_init();
if (rc < 0) {
LOG_ERR("File system management initialization failed: %d", rc);
return rc;
}
LOG_INF("File system management initialized successfully.");
rc = audio_init();
if (rc < 0) {
LOG_ERR("Audio initialization failed: %d", rc);
return rc;
}
LOG_INF("Audio initialized successfully.");
rc = game_mgmt_init();
if (rc < 0) {
LOG_ERR("Game management initialization failed: %d", rc);
return rc;
}
LOG_INF(FORMAT_BRIGHT("Game management initialized successfully. Switching to LOBBY state..."));
game_mgmt_set_game_id(generate_64bit_random()); /* Set a dummy game ID for testing */
game_mgmt_set_state(SYS_STATE_LOBBY);
return 0;
}
}

View File

@@ -0,0 +1,5 @@
VERSION_MAJOR = 0
VERSION_MINOR = 0
PATCHLEVEL = 0
VERSION_TWEAK = 0
EXTRAVERSION =

View File

@@ -0,0 +1 @@
../leader/pm_static.yml

View File

@@ -1,41 +1,28 @@
# Console and Logging
CONFIG_LOG=y
# Shell and Built-in Commands
CONFIG_SHELL=y
CONFIG_KERNEL_SHELL=y
CONFIG_DEVICE_SHELL=y
CONFIG_REBOOT=y
# UART basics
CONFIG_SERIAL=y
CONFIG_UART_INTERRUPT_DRIVEN=y
# --- STACK SIZE UPDATES (Fixes the Hard Fault) ---
CONFIG_MAIN_STACK_SIZE=4096
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048
CONFIG_BT_RX_STACK_SIZE=2048
# Shell configuration
CONFIG_SHELL_BACKEND_SERIAL=y
CONFIG_FILE_SYSTEM_SHELL=y
# Storage and Settings (NVS)
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y
CONFIG_SETTINGS=y
# Lasertag-specific configuration
CONFIG_AUDIO=y
CONFIG_AUDIO_LOG_LEVEL_DBG=y
# Network and OpenThread
CONFIG_NETWORKING=y
CONFIG_NET_L2_OPENTHREAD=y
CONFIG_OPENTHREAD=y
CONFIG_OPENTHREAD_FTD=y
CONFIG_OPENTHREAD_SHELL=y
CONFIG_BLE_MGMT=y
# --- CoAP & UDP Features ---
CONFIG_OPENTHREAD_COAP=y
CONFIG_OPENTHREAD_MANUAL_START=y
CONFIG_GAME_MGMT=y
CONFIG_GAME_MGMT_SHELL=y
CONFIG_GAME_MGMT_LOG_LEVEL_DBG=y
# Bluetooth
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="Lasertag-Device"
CONFIG_BT_DEVICE_NAME_DYNAMIC=y
# Enable Lasertag Shared Modules
CONFIG_LASERTAG_UTILS=y
CONFIG_THREAD_MGMT=y
CONFIG_BLE_MGMT=y
CONFIG_THREAD_MGMT_LOG_LEVEL_DBG=y
CONFIG_THREAD_MGMT_SHELL=y
CONFIG_FS_MGMT=y
CONFIG_FS_MGMT_LOG_LEVEL_DBG=y
CONFIG_LASERTAG_ROLE_VEST=y

View File

@@ -1,46 +1,44 @@
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <lasertag_utils.h>
#include <thread_mgmt.h>
#include <ble_mgmt.h>
#include <game_mgmt.h>
#include <lasertag_utils.h>
#include <fs_mgmt.h>
#include <audio.h>
LOG_MODULE_REGISTER(vest_app, CONFIG_LOG_DEFAULT_LEVEL);
LOG_MODULE_REGISTER(OT_SAMPLE, LOG_LEVEL_INF);
int main(void)
{
/* Initialize shared project logic and NVS */
LOG_INF("Starting Thread Management test application...");
lasertag_utils_init();
/* Initialize and start BLE management for provisioning */
int rc = ble_mgmt_init(LT_TYPE_VEST);
if (rc) {
LOG_ERR("BLE initialization failed (err %d)", rc);
return rc;
} else {
LOG_INF("BLE Management initialized successfully.");
}
/* Start BLE advertising */
rc = ble_mgmt_adv_start();
if (rc) {
LOG_ERR("BLE advertising start failed (err %d)", rc);
} else {
LOG_INF("BLE advertising started.");
}
/* Initialize and start OpenThread stack */
rc = thread_mgmt_init();
if (rc) {
LOG_ERR("Thread initialization failed (err %d)", rc);
} else {
LOG_INF("Leader Application successfully started with Thread Mesh.");
int rc = thread_mgmt_init();
if (rc < 0) {
LOG_ERR("Thread management initialization failed: %d", rc);
return rc;
}
LOG_INF("Thread management initialized successfully.");
while (1) {
/* Main loop - handle high-level game logic here */
k_sleep(K_MSEC(1000));
rc = fs_mgmt_init();
if (rc < 0) {
LOG_ERR("File system management initialization failed: %d", rc);
return rc;
}
LOG_INF("File system management initialized successfully.");
rc = audio_init();
if (rc < 0) {
LOG_ERR("Audio initialization failed: %d", rc);
return rc;
}
LOG_INF("Audio initialized successfully.");
rc = game_mgmt_init();
if (rc < 0) {
LOG_ERR("Game management initialization failed: %d", rc);
return rc;
}
LOG_INF(FORMAT_BRIGHT("Game management initialized successfully. Switching to LOBBY state..."));
game_mgmt_set_state(SYS_STATE_LOBBY);
return 0;
}
}

View File

@@ -1,20 +1,23 @@
# Logging
CONFIG_LOG=y
CONFIG_LASERTAG_WEAPON_LOG_LEVEL_INF=y
# Network / OpenThread
CONFIG_NETWORKING=y
CONFIG_NET_L2_OPENTHREAD=y
CONFIG_OPENTHREAD_COAP=y
# UART basics
CONFIG_SERIAL=y
CONFIG_UART_INTERRUPT_DRIVEN=y
# Hardware (Buttons & LEDs)
CONFIG_DK_LIBRARY=y
# Shell configuration
CONFIG_SHELL_BACKEND_SERIAL=y
CONFIG_FILE_SYSTEM_SHELL=y
# Lasertag Game Logic
CONFIG_LASERTAG_GAME_LOGIC=y
CONFIG_LASERTAG_ROLE_PLAYER=y
CONFIG_LASERTAG_PLAYER_ID_DEFAULT=2
# Lasertag-specific configuration
CONFIG_BLE_MGMT=y
CONFIG_GAME_MGMT=y
CONFIG_GAME_MGMT_SHELL=y
CONFIG_GAME_MGMT_LOG_LEVEL_DBG=y
CONFIG_THREAD_MGMT=y
CONFIG_THREAD_MGMT_LOG_LEVEL_DBG=y
CONFIG_THREAD_MGMT_SHELL=y
CONFIG_FS_MGMT=y
CONFIG_FS_MGMT_LOG_LEVEL_DBG=y
CONFIG_AUDIO_LOG_LEVEL_DBG=y
# Optional: Shell for debugging
CONFIG_SHELL=y
CONFIG_OPENTHREAD_SHELL=y
CONFIG_LASERTAG_ROLE_VEST=y

View File

@@ -1,6 +1,8 @@
# Add library subdirectories
# 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(ir)
add_subdirectory(ir)
add_subdirectory(game_mgmt)
add_subdirectory(fs_mgmt)
add_subdirectory(audio)

View File

@@ -1,5 +1,26 @@
# Main entry point for custom project Kconfigs
choice LASERTAG_DEVICE_ROLE
prompt "Lasertag Device Role"
default LASERTAG_ROLE_VEST
help
Select the role of the lasertag device. This can be used to conditionally compile code specific to vests, guns, or other device types.
config LASERTAG_ROLE_VEST
bool "Vest"
help
A standard role for the vest device, which may have responsibilities such as receiving hit notifications, managing player health, etc.
config LASERTAG_ROLE_WEAPON
bool "Weapon"
help
A special role for the weapon device, which may have additional responsibilities such as sending hit notifications, managing ammo count, etc.
config LASERTAG_ROLE_LEADER
bool "Game Leader"
help
A special role for the game leader device, which may have additional responsibilities such as starting/stopping games, managing player lists, etc.
endchoice
rsource "lasertag_utils/Kconfig"
rsource "thread_mgmt/Kconfig"
rsource "ble_mgmt/Kconfig"
rsource "ir/Kconfig"
rsource "ir/Kconfig"
rsource "game_mgmt/Kconfig"
rsource "fs_mgmt/Kconfig"
rsource "audio/Kconfig"

View File

@@ -0,0 +1,5 @@
if(CONFIG_AUDIO)
zephyr_library()
zephyr_library_sources(src/audio.c)
zephyr_include_directories(include)
endif()

View File

@@ -0,0 +1,76 @@
menuconfig AUDIO
bool "Audio Support"
select FS_MGMT
select I2S
select I2S_NRFX if DT_HAS_NORDIC_NRF_I2S_ENABLED
help
Library for initializing and managing the audio subsystem.
if AUDIO
config AUDIO_DEFAULT_VOLUME
int "Default Audio Volume (0..255)"
default 128
range 0 255
help
Set the default audio volume level. 0 is silent, 255 is maximum volume. Default is 128 (50% volume).
config AUDIO_SAMPLE_RATE
int "Audio Sample Rate (Hz)"
default 16000
range 8000 48000
help
Set the audio sample rate in Hz. Common values are 8000, 16000, 44100, and 48000 Hz. Default is 16000 Hz.
config AUDIO_WORD_WIDTH
int "Audio Bit Depth"
default 16
range 8 32
help
Set the audio bit depth. Common values are 8, 16, 24, and 32 bits. Default is 16 bits.
config AUDIO_BLOCK_COUNT
int "Audio Block Count"
default 4
range 1 16
help
Set the number of audio blocks for buffering. More blocks can help with smoother audio but use more memory. Default is 4 blocks.
config AUDIO_BLOCK_SIZE
int "Audio Block Size (bytes)"
default 1024
range 256 8192
help
Set the size of each audio block in bytes. Larger blocks can reduce CPU overhead but increase latency. Default is 1024 bytes.
config AUDIO_THREAD_PRIORITY
int "Audio Thread Priority"
default 5
range 0 255
help
Set the thread priority for audio processing. Lower numbers indicate higher priority. Default is 5
config AUDIO_STACK_SIZE
int "Audio Thread Stack Size (bytes)"
default 1200
range 256 8192
help
Set the stack size for the audio processing thread in bytes. Default is 2048 bytes.
config AUDIO_SAMPLE_FOLDER
string "Audio Sample Folder"
default "a"
help
Set the folder path where audio sample files are stored. No leading or trailing slashes. Default is "a".
config AUDIO_MAX_PATH_LEN
int "Maximum Audio File Path Length"
default 32
range 8 128
help
Set the maximum length for audio file paths. Default is 16 characters.
# Logging configuration for the Audio module
module = AUDIO
module-str = audio
source "subsys/logging/Kconfig.template.log_config"
endif # AUDIO

View File

@@ -0,0 +1,28 @@
/**
* @file audio.h
* @brief Public API for the audio subsystem.
* This header defines the public interface for the audio subsystem, which
* provides functionality to play audio files and manage audio playback. It
* abstracts away the details of the underlying I2S peripheral and file
* system, allowing other parts of the application to easily trigger audio
* playback by specifying file paths or sound names.
*
* FLASH MEMORY USAGE:
* LOG LEVEL DEBUG: ~1.8 KB
* LOG LEVEL INFO: ~1.7 KB
* LOG LEVEL WARNING: ~1.2 KB
* LOG LEVEL ERROR: ~1.1 KB
*
* RAM USAGE (without stack and audio buffers):
* Any LOG LEVEL: ~0.47 KB
*/
#ifndef AUDIO_H
#define AUDIO_H
int audio_init(void);
int audio_play_sound(const char* file);
int audio_play_file(const char* file);
void audio_stop(void);
#endif // AUDIO_H

View File

@@ -0,0 +1,271 @@
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/drivers/i2s.h>
#include <zephyr/fs/fs.h>
#include <audio.h>
#include <string.h>
LOG_MODULE_REGISTER(audio, CONFIG_AUDIO_LOG_LEVEL);
#define SAMPLES_PER_BLOCK (CONFIG_AUDIO_BLOCK_SIZE / (CONFIG_AUDIO_WORD_WIDTH / 8) / 2) // Divide by 2 for stereo
#define BLOCK_DURATION_MS ((SAMPLES_PER_BLOCK * 1000U) / CONFIG_AUDIO_SAMPLE_RATE) // Duration of audio in each block in milliseconds
#define MAX_WAIT_TIME_MS (3 * BLOCK_DURATION_MS) // Maximum time to wait for the I2S peripheral to request the next block before we consider it stalled and reset it
/* Get the I2S device from the devicetree */
#define I2S_NODE DT_NODELABEL(i2s0)
static const struct device *i2s_dev = DEVICE_DT_GET(I2S_NODE);
/* Memory Slab for I2S DMA */
K_MEM_SLAB_DEFINE(audio_slab, CONFIG_AUDIO_BLOCK_SIZE, CONFIG_AUDIO_BLOCK_COUNT, 4);
/* Globals */
static volatile bool abort_playback = false;
static volatile uint8_t audio_volume = CONFIG_AUDIO_DEFAULT_VOLUME;
static void wait_for_i2s_drain(void)
{
const uint32_t frames_per_block = CONFIG_AUDIO_BLOCK_SIZE / 4;
const uint32_t block_ms = (frames_per_block * 1000U) / CONFIG_AUDIO_SAMPLE_RATE;
const uint32_t max_wait_ms = (block_ms + 1U) * CONFIG_AUDIO_BLOCK_COUNT + 5U;
int64_t deadline = k_uptime_get() + max_wait_ms;
while (k_mem_slab_num_free_get(&audio_slab) < CONFIG_AUDIO_BLOCK_COUNT)
{
if (k_uptime_get() >= deadline)
{
LOG_WRN("Timeout waiting for I2S drain");
break;
}
k_sleep(K_MSEC(1));
}
}
/* Message Queue: transfers the file path to the thread */
K_MSGQ_DEFINE(audio_msgq, CONFIG_AUDIO_MAX_PATH_LEN, 10, 4);
/* Audio thread function */
void audio_thread_fn(void *p1, void *p2, void *p3)
{
ARG_UNUSED(p1);
ARG_UNUSED(p2);
ARG_UNUSED(p3);
char file_path[CONFIG_AUDIO_MAX_PATH_LEN];
struct fs_file_t file;
fs_file_t_init(&file);
LOG_DBG("Audio thread started, priority %d", k_thread_priority_get(k_current_get()));
while (1)
{
bool trigger_started = false;
if (k_msgq_get(&audio_msgq, &file_path, K_FOREVER) == 0)
{
abort_playback = false;
if (fs_open(&file, file_path, FS_O_READ) < 0)
{
LOG_ERR("thread: could not open %s", file_path);
continue;
}
LOG_DBG("thread: preparing %s...", file_path);
uint32_t queued_blocks = 0;
while (!abort_playback)
{
void *mem_block;
if (k_mem_slab_alloc(&audio_slab, &mem_block, K_MSEC(MAX_WAIT_TIME_MS)) < 0)
{
LOG_ERR("audio: slab timeout (I2S stall? DMA failure?) - skipping sound and resetting I2S...");
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP);
audio_init();
break;
}
if (abort_playback)
{
LOG_DBG("thread: playback aborted while waiting for memory block.");
k_mem_slab_free(&audio_slab, mem_block);
break;
}
int16_t *data_ptr = (int16_t *)mem_block;
const uint32_t max_mono_samples = CONFIG_AUDIO_BLOCK_SIZE / 4;
ssize_t bytes_read = fs_read(&file, data_ptr, max_mono_samples * sizeof(int16_t));
if (bytes_read <= 0)
{
k_mem_slab_free(&audio_slab, mem_block);
break;
}
uint32_t samples_read = bytes_read / sizeof(int16_t);
// Padding with zeros if we read less than a full block of mono samples
if (samples_read < max_mono_samples)
{
memset(&data_ptr[samples_read], 0, (max_mono_samples - samples_read) * sizeof(int16_t));
}
uint32_t *stereo_dst = (uint32_t *)mem_block;
for (int32_t i = max_mono_samples - 1; i >= 0; i--)
{
int32_t scaled = (int32_t)data_ptr[i] * audio_volume;
int16_t sample = (int16_t)(scaled >> 8);
stereo_dst[i] = ((uint16_t)sample << 16) | (uint16_t)sample;
}
if (i2s_write(i2s_dev, mem_block, CONFIG_AUDIO_BLOCK_SIZE) < 0)
{
k_mem_slab_free(&audio_slab, mem_block);
break;
}
queued_blocks++;
// We start playback only when 2 blocks are in the DMA queue to avoid underruns
if (!trigger_started && queued_blocks >= 2)
{
if (i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_START) == 0)
{
trigger_started = true;
LOG_DBG("thread: playback started.");
}
}
if (samples_read < max_mono_samples)
{
// Short sample: start with a single queued block so DRAIN can play it.
if (!trigger_started)
{
if (i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_START) == 0)
{
trigger_started = true;
LOG_DBG("thread: playback started (short sample).");
}
}
break;
}
}
if (abort_playback)
{
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP);
trigger_started = false;
LOG_DBG("thread: playback aborted.");
}
else
{
if (k_msgq_num_used_get(&audio_msgq) > 0)
{
LOG_DBG("thread: play request pending, not draining I2S to minimize latency...");
}
else
{
LOG_DBG("thread: sample finished, waiting for I2S to drain...");
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DRAIN);
trigger_started = false;
wait_for_i2s_drain();
LOG_DBG("thread: playback finished.");
}
}
fs_close(&file);
}
}
}
K_THREAD_DEFINE(audio_thread, CONFIG_AUDIO_STACK_SIZE, audio_thread_fn, NULL, NULL, NULL, CONFIG_AUDIO_THREAD_PRIORITY, 0, 0);
int audio_init(void)
{
LOG_DBG("Initializing audio subsystem...");
if (!device_is_ready(i2s_dev))
{
LOG_ERR("I2S device not ready");
return -ENODEV;
}
/* Initial configuration of the I2S peripheral */
struct i2s_config config = {
.word_size = CONFIG_AUDIO_WORD_WIDTH,
.channels = 2,
.format = I2S_FMT_DATA_FORMAT_I2S,
.options = I2S_OPT_BIT_CLK_MASTER | I2S_OPT_FRAME_CLK_MASTER,
.frame_clk_freq = CONFIG_AUDIO_SAMPLE_RATE,
.mem_slab = &audio_slab,
.block_size = CONFIG_AUDIO_BLOCK_SIZE,
.timeout = SYS_FOREVER_MS,
};
int ret = i2s_configure(i2s_dev, I2S_DIR_TX, &config);
if (ret < 0)
{
LOG_ERR("Failed to configure I2S: %d", ret);
return ret;
}
LOG_DBG("Audio subsystem initialized successfully");
return 0;
}
int audio_play_file(const char *file)
{
if (file == NULL)
{
LOG_ERR("audio_play_file: file path is NULL");
return -EINVAL;
}
size_t len = strnlen(file, CONFIG_AUDIO_MAX_PATH_LEN);
if (len >= CONFIG_AUDIO_MAX_PATH_LEN)
{
LOG_ERR("audio_play_file: file path too long: %s", file);
return -ENAMETOOLONG;
}
char path_item[CONFIG_AUDIO_MAX_PATH_LEN];
memcpy(path_item, file, len + 1);
if (k_msgq_put(&audio_msgq, path_item, K_NO_WAIT) < 0)
{
LOG_ERR("audio_play_file: message queue full");
return -EAGAIN;
}
LOG_DBG("Queued file for playback: %s", file);
return 0;
}
int audio_play_sound(const char *file)
{
if (file == NULL)
{
LOG_ERR("audio_play_sound: file name is NULL");
return -EINVAL;
}
size_t len = strnlen(file, CONFIG_AUDIO_MAX_PATH_LEN) + strlen(CONFIG_FS_MGMT_MOUNT_POINT) + strlen(CONFIG_AUDIO_SAMPLE_FOLDER) + 2;
if (len >= CONFIG_AUDIO_MAX_PATH_LEN)
{
LOG_ERR("audio_play_sound: file path too long: %s/%s/%s", CONFIG_FS_MGMT_MOUNT_POINT, CONFIG_AUDIO_SAMPLE_FOLDER, file);
return -ENAMETOOLONG;
}
char path[CONFIG_AUDIO_MAX_PATH_LEN];
snprintf(path, sizeof(path), "%s/%s/%s",
CONFIG_FS_MGMT_MOUNT_POINT,
CONFIG_AUDIO_SAMPLE_FOLDER,
file);
return audio_play_file(path);
}
void audio_stop(void)
{
abort_playback = true;
k_msgq_purge(&audio_msgq);
i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP);
LOG_DBG("Playback stop requested, message queue purged");
}

View File

@@ -1,5 +1,5 @@
if(CONFIG_BLE_MGMT)
zephyr_library()
zephyr_sources(src/ble_mgmt.c)
zephyr_library_sources(src/ble_mgmt.c)
zephyr_include_directories(include)
endif()

View File

@@ -1,11 +1,30 @@
menuconfig BLE_MGMT
bool "BLE Management"
depends on BT
select BT
select BT_PERIPHERAL
select BT_DEVICE_NAME_DYNAMIC
help
Library for BLE provisioning of the lasertag device.
if BLE_MGMT
config BLE_MGMT_LOG_LEVEL
int "BLE Management Log Level"
default 3
module = BLE_MGMT
module-str = ble_mgmt
source "subsys/logging/Kconfig.template.log_config"
config BLE_MGMT_CAN_BE_GAME_LEADER
bool "Can be game leader"
default n
help
Allow this device to take the game leader role in the lasertag game.
config BT_DEVICE_NAME
default "Lasertag Device"
config BT_L2CAP_TX_MTU
default 252
config BT_BUF_ACL_RX_SIZE
default 251
config BT_BUF_ACL_TX_SIZE
default 251
config BT_ATT_PREPARE_COUNT
default 5
endif

View File

@@ -15,6 +15,20 @@
#define LT_TYPE_VEST 0x03
#define LT_TYPE_BEACON 0x04
/**
* @brief Device configuration payload structure for BLE management.
*/
typedef struct __packed {
uint8_t system_state; /* Offset 0 */
uint64_t game_id; /* Offset 1 */
uint16_t pan_id; /* Offset 9 */
uint8_t channel; /* Offset 11 */
uint8_t ext_pan_id[8]; /* Offset 12 */
uint8_t network_key[16]; /* Offset 20 */
char network_name[17]; /* Offset 36 */
char node_name[33]; /* Offset 53 */
} device_config_payload_t;
/**
* @brief Initialize Bluetooth and prepare services.
*

View File

@@ -1,3 +1,9 @@
/**
* BLE Management Module (ble_mgmt.c)
* * Structural Fix: Offloading heavy NVS and Thread operations to a workqueue
* to prevent stack overflows in the BT RX thread.
*/
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/bluetooth/bluetooth.h>
@@ -9,203 +15,166 @@
#include <lasertag_utils.h>
#include <thread_mgmt.h>
#include <ble_mgmt.h>
#include <game_mgmt.h>
#include <string.h>
LOG_MODULE_REGISTER(ble_mgmt, CONFIG_BLE_MGMT_LOG_LEVEL);
/**
* Base UUID: 03afe2cf-6c64-4a22-9289-c3ae820cXXXX
* Alias positions: Byte 12 & 13
*/
/* UUID Definitions */
#define BT_UUID_LT_PROV_SERVICE BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c1000))
#define BT_UUID_LT_PROV_NAME_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c1001))
#define BT_UUID_LT_PROV_TYPE_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c1008))
#define BT_UUID_LT_PROV_CONFIG_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c100c))
#define LT_UUID_BASE_VAL \
BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c0000)
/* Global state and Workqueue structures */
static uint8_t device_role = 0;
static uint8_t adv_enabled = 0;
static struct k_work_delayable adv_restart_work;
/* ==========================================================================
SERVICE 1: PROVISIONING (0x10XX)
========================================================================== */
#define BT_UUID_LT_PROV_SERVICE BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c1000))
/* Buffers for asynchronous config application */
static device_config_payload_t pending_config;
static struct k_work config_apply_work;
#define BT_UUID_LT_PROV_NAME_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c1001))
#define BT_UUID_LT_PROV_PANID_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c1002))
#define BT_UUID_LT_PROV_CHAN_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c1003))
#define BT_UUID_LT_PROV_EXTPAN_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c1004))
#define BT_UUID_LT_PROV_NETKEY_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c1005))
#define BT_UUID_LT_PROV_NETNAME_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c1006))
#define BT_UUID_LT_PROV_NODES_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c1007))
#define BT_UUID_LT_PROV_TYPE_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c1008))
/* ============================================================================
Workqueue Handlers
============================================================================ */
static void config_apply_work_handler(struct k_work *work)
{
ARG_UNUSED(work);
/* ==========================================================================
SERVICE 2: GAME (0x20XX)
========================================================================== */
#define BT_UUID_LT_GAME_SERVICE BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c2000))
LOG_DBG("conf rcv, name: " FORMAT_BOLD("%s") ", state: " FORMAT_BOLD("%d") ", game-id: " FORMAT_BOLD("0x%llx") ", net name: " FORMAT_BOLD("%s") ", channel: " FORMAT_BOLD("%u") ", pan: " FORMAT_BOLD("0x%04X"),
pending_config.node_name,
pending_config.system_state,
pending_config.game_id,
pending_config.network_name,
pending_config.channel,
pending_config.pan_id);
LOG_HEXDUMP_DBG(pending_config.ext_pan_id, 8, "ext pan id");
LOG_HEXDUMP_DBG(pending_config.network_key, 16, "network key");
#define BT_UUID_LT_GAME_CONFIG_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c2001))
#define BT_UUID_LT_GAME_COMMAND_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c2002))
#define BT_UUID_LT_GAME_LOG_DATA_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c2003))
if (pending_config.system_state != SYS_STATE_NO_CHANGE) {
game_mgmt_set_state((sys_state_t)pending_config.system_state);
}
if (pending_config.game_id != 0) {
game_mgmt_set_game_id(pending_config.game_id);
}
/* --- Global Variables --- */
static uint8_t device_role = 0; // Store device type for provisioning
if (pending_config.node_name[0] != '\0') {
lasertag_set_device_name(pending_config.node_name, strlen(pending_config.node_name));
bt_set_name(lasertag_get_device_name());
}
/* --- GATT Callbacks --- */
if (pending_config.channel != 0) {
thread_mgmt_restart_thread_stack(&pending_config, false);
}
}
static ssize_t read_lasertag_val(struct bt_conn *conn, const struct bt_gatt_attr *attr,
void *buf, uint16_t len, uint16_t offset)
static void adv_restart_work_handler(struct k_work *work)
{
ARG_UNUSED(work);
LOG_DBG("Restarting BLE advertising via System Workqueue...");
if (adv_enabled == 0) {
int err = ble_mgmt_adv_start();
if (err) {
LOG_ERR("Fehler beim Neustart des Advertisings (err %d)", err);
}
}
}
/* ============================================================================
GATT Handlers
============================================================================ */
static ssize_t read_leader_config(struct bt_conn *conn, const struct bt_gatt_attr *attr,
void *buf, uint16_t len, uint16_t offset)
{
device_config_payload_t payload;
memset(&payload, 0, sizeof(payload));
payload.system_state = (uint8_t)game_mgmt_get_state();
payload.game_id = game_mgmt_get_game_id();
payload.pan_id = thread_mgmt_get_pan_id();
payload.channel = thread_mgmt_get_channel();
thread_mgmt_get_ext_pan_id(payload.ext_pan_id);
thread_mgmt_get_network_key(payload.network_key);
thread_mgmt_get_network_name(payload.network_name, sizeof(payload.network_name));
strncpy(payload.node_name, lasertag_get_device_name(), 32);
LOG_DBG("conf snd, name: " FORMAT_BOLD("%s") ", state: " FORMAT_BOLD("%d") ", game-id: "
FORMAT_BOLD("0x%llx") ", net name: " FORMAT_BOLD("%s") ", channel: " FORMAT_BOLD("%u") ", pan: " FORMAT_BOLD("0x%04X"),
payload.node_name,
payload.system_state,
payload.game_id,
payload.network_name,
payload.channel,
payload.pan_id);
LOG_HEXDUMP_DBG(payload.ext_pan_id, 8, "ext pan id");
LOG_HEXDUMP_DBG(payload.network_key, 16, "network key");
return bt_gatt_attr_read(conn, attr, buf, len, offset, &payload, sizeof(payload));
}
static ssize_t write_leader_config(struct bt_conn *conn, const struct bt_gatt_attr *attr,
const void *buf, uint16_t len, uint16_t offset, uint8_t flags)
{
if (len != sizeof(device_config_payload_t)) {
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
}
/* Copy data to buffer and delegate to system workqueue */
memcpy(&pending_config, buf, sizeof(pending_config));
k_work_submit(&config_apply_work);
LOG_DBG("Config write received, delegated to workqueue.");
return len;
}
/* Simple value handlers for name and type */
static ssize_t read_simple_val(struct bt_conn *conn, const struct bt_gatt_attr *attr,
void *buf, uint16_t len, uint16_t offset)
{
const char *val_ptr = NULL;
size_t val_len = 0;
if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PROV_TYPE_CHAR) == 0)
{
if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PROV_TYPE_CHAR) == 0) {
val_ptr = (char *)&device_role;
val_len = sizeof(device_role);
}
else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PROV_NAME_CHAR) == 0)
{
} else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PROV_NAME_CHAR) == 0) {
val_ptr = lasertag_get_device_name();
val_len = strlen(val_ptr);
}
else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PROV_PANID_CHAR) == 0)
{
static uint16_t pan_id;
pan_id = lasertag_get_thread_pan_id();
val_ptr = (char *)&pan_id;
val_len = sizeof(pan_id);
}
else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PROV_CHAN_CHAR) == 0)
{
static uint8_t chan;
chan = lasertag_get_thread_channel();
val_ptr = (char *)&chan;
val_len = sizeof(chan);
}
else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PROV_EXTPAN_CHAR) == 0)
{
val_ptr = (char *)lasertag_get_thread_ext_pan_id();
val_len = 8;
}
else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PROV_NETKEY_CHAR) == 0)
{
val_ptr = (char *)lasertag_get_thread_network_key();
val_len = 16;
}
else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PROV_NETNAME_CHAR) == 0)
{
val_ptr = lasertag_get_thread_network_name();
val_len = strlen(val_ptr);
}
return bt_gatt_attr_read(conn, attr, buf, len, offset, val_ptr, val_len);
}
static ssize_t write_lasertag_val(struct bt_conn *conn, const struct bt_gatt_attr *attr,
const void *buf, uint16_t len, uint16_t offset, uint8_t flags)
static ssize_t write_name(struct bt_conn *conn, const struct bt_gatt_attr *attr,
const void *buf, uint16_t len, uint16_t offset, uint8_t flags)
{
int rc = 0;
if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PROV_NAME_CHAR) == 0)
{
rc = lasertag_set_device_name(buf, len);
}
else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PROV_PANID_CHAR) == 0)
{
if (len != 2)
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
rc = lasertag_set_thread_pan_id(*(uint16_t *)buf);
}
else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PROV_CHAN_CHAR) == 0)
{
if (len != 1)
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
rc = lasertag_set_thread_channel(*(uint8_t *)buf);
}
else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PROV_EXTPAN_CHAR) == 0)
{
if (len != 8)
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
rc = lasertag_set_thread_ext_pan_id(buf);
}
else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PROV_NETKEY_CHAR) == 0)
{
if (len != 16)
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
rc = lasertag_set_thread_network_key(buf);
}
else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PROV_NETNAME_CHAR) == 0)
{
rc = lasertag_set_thread_network_name(buf, len);
}
if (rc)
return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY);
return len;
int rc = lasertag_set_device_name(buf, len);
if (rc == 0) bt_set_name(lasertag_get_device_name());
return rc ? BT_GATT_ERR(BT_ATT_ERR_UNLIKELY) : len;
}
static ssize_t read_discovered_nodes(struct bt_conn *conn, const struct bt_gatt_attr *attr,
void *buf, uint16_t len, uint16_t offset)
{
const char *list = thread_mgmt_get_discovered_list();
return bt_gatt_attr_read(conn, attr, buf, len, offset, list, strlen(list));
}
static ssize_t write_discover_cmd(struct bt_conn *conn, const struct bt_gatt_attr *attr,
const void *buf, uint16_t len, uint16_t offset, uint8_t flags)
{
/* If anything is written, trigger discovery in Thread Mesh */
thread_mgmt_discover_nodes();
return len;
}
/* Service Definition */
/* ============================================================================
Service Definition
============================================================================ */
BT_GATT_SERVICE_DEFINE(provisioning_svc,
BT_GATT_PRIMARY_SERVICE(BT_UUID_LT_PROV_SERVICE),
BT_GATT_PRIMARY_SERVICE(BT_UUID_LT_PROV_SERVICE),
BT_GATT_CHARACTERISTIC(BT_UUID_LT_PROV_NAME_CHAR,
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
read_simple_val, write_name, NULL),
BT_GATT_CHARACTERISTIC(BT_UUID_LT_PROV_TYPE_CHAR,
BT_GATT_CHRC_READ, BT_GATT_PERM_READ,
read_simple_val, NULL, NULL),
BT_GATT_CHARACTERISTIC(BT_UUID_LT_PROV_CONFIG_CHAR,
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
read_leader_config, write_leader_config, NULL),
);
/* Device name */
BT_GATT_CHARACTERISTIC(BT_UUID_LT_PROV_NAME_CHAR,
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
read_lasertag_val, write_lasertag_val, NULL),
/* Device Type (Read-only) */
BT_GATT_CHARACTERISTIC(BT_UUID_LT_PROV_TYPE_CHAR,
BT_GATT_CHRC_READ,
BT_GATT_PERM_READ,
read_lasertag_val, NULL, NULL),
/* Thread PAN ID */
BT_GATT_CHARACTERISTIC(BT_UUID_LT_PROV_PANID_CHAR,
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
read_lasertag_val, write_lasertag_val, NULL),
/* Thread Channel */
BT_GATT_CHARACTERISTIC(BT_UUID_LT_PROV_CHAN_CHAR,
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
read_lasertag_val, write_lasertag_val, NULL),
/* Extended PAN ID */
BT_GATT_CHARACTERISTIC(BT_UUID_LT_PROV_EXTPAN_CHAR,
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
read_lasertag_val, write_lasertag_val, NULL),
/* Network Key */
BT_GATT_CHARACTERISTIC(BT_UUID_LT_PROV_NETKEY_CHAR,
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
read_lasertag_val, write_lasertag_val, NULL),
/* Thread Network Name */
BT_GATT_CHARACTERISTIC(BT_UUID_LT_PROV_NETNAME_CHAR,
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
read_lasertag_val, write_lasertag_val, NULL),
/* Node List / Discovery Trigger */
BT_GATT_CHARACTERISTIC(BT_UUID_LT_PROV_NODES_CHAR,
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
read_discovered_nodes, write_discover_cmd, NULL), );
static uint8_t mfg_data[] = { 0xff, 0xff, 0x00 }; // Last byte for device role
/* ============================================================================
Advertising & Management
============================================================================ */
static uint8_t mfg_data[] = { 0xff, 0xff, 0x00 };
static const struct bt_data ad[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA_BYTES(BT_DATA_UUID128_ALL,
@@ -217,47 +186,78 @@ static const struct bt_data ad[] = {
int ble_mgmt_init(uint8_t device_type)
{
device_role = device_type;
/* Initialize work structures */
k_work_init_delayable(&adv_restart_work, adv_restart_work_handler);
k_work_init(&config_apply_work, config_apply_work_handler);
int err = bt_enable(NULL);
if (err)
return err;
LOG_INF("Bluetooth initialized");
if (err) return err;
LOG_DBG("Bluetooth initialized successfully.");
return 0;
}
int ble_mgmt_adv_start(void)
{
const char set_device_role = device_role;
mfg_data[2] = set_device_role; // Update device role in advertising data
mfg_data[2] = device_role;
const char *name = lasertag_get_device_name();
bt_set_name(name);
struct bt_data dynamic_sd[] = {
BT_DATA(BT_DATA_NAME_COMPLETE, name, strlen(name)),
};
struct bt_le_adv_param adv_param = {
.id = BT_ID_DEFAULT,
struct bt_data sd[] = { BT_DATA(BT_DATA_NAME_COMPLETE, name, strlen(name)) };
struct bt_le_adv_param param = {
.options = (BT_LE_ADV_OPT_CONN | BT_LE_ADV_OPT_SCANNABLE),
.interval_min = BT_GAP_ADV_FAST_INT_MIN_2,
.interval_max = BT_GAP_ADV_FAST_INT_MAX_2,
};
int err = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), dynamic_sd, ARRAY_SIZE(dynamic_sd));
if (!err)
{
LOG_INF("Advertising started as: %s, type: %d", name, device_role);
}
int err = bt_le_adv_start(&param, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
if (!err) adv_enabled = 1;
return err;
}
/**
* Stop BLE advertising.
*
* @return 0 on success, negative error code on failure
*/
int ble_mgmt_adv_stop(void)
{
int err = bt_le_adv_stop();
if (!err)
{
LOG_INF("Advertising stopped");
LOG_DBG("Advertising stopped");
adv_enabled = 0;
}
return err;
}
}
/* ============================================================================
BLE Connection Event Handlers
============================================================================ */
/**
* Callback for when a device connects.
* Logs the connection and updates advertising state.
*/
static void connected(struct bt_conn *conn, uint8_t err)
{
if (err) {
LOG_ERR("Verbindung fehlgeschlagen (err %u)", err);
} else {
LOG_DBG("Host verbunden");
adv_enabled = 0;
}
}
/**
* Callback for when a device disconnects.
* Logs the disconnection and schedules advertising restart.
*/
static void disconnected(struct bt_conn *conn, uint8_t reason)
{
LOG_DBG("Verbindung getrennt (Grund %u)", reason);
k_work_reschedule(&adv_restart_work, K_MSEC(100));
}
/* Connection callbacks structure */
BT_CONN_CB_DEFINE(conn_callbacks) = {
.connected = connected,
.disconnected = disconnected,
};

View File

@@ -0,0 +1,17 @@
if(CONFIG_FS_MGMT)
zephyr_library()
zephyr_library_sources(src/fs_mgmt.c)
zephyr_include_directories(include)
if(CONFIG_FILE_SYSTEM_LITTLEFS)
if(DEFINED ZEPHYR_LITTLEFS_MODULE_DIR)
zephyr_include_directories(${ZEPHYR_LITTLEFS_MODULE_DIR})
elseif(DEFINED WEST_TOPDIR)
zephyr_include_directories(${WEST_TOPDIR}/modules/fs/littlefs)
endif()
if(DEFINED ZEPHYR_BASE)
zephyr_include_directories(${ZEPHYR_BASE}/modules/littlefs)
endif()
endif()
endif()

View File

@@ -0,0 +1,30 @@
menuconfig FS_MGMT
bool "File System Management"
select FLASH
select FLASH_MAP
select FILE_SYSTEM
select FILE_SYSTEM_LITTLEFS
select FILE_SYSTEM_MKFS
help
Library for initializing and managing the file system.
if FS_MGMT
config FS_MGMT_MOUNT_POINT
string "Littlefs Mount Point"
default "/lfs"
help
Set the mount point for the Littlefs file system. Default is "/lfs".
config FS_MGMT_MCUMGR_HANDLER
bool "Enable Custom MCUMGR FS Handlers"
default y
depends on MCUMGR_GRP_FS
help
Enables the custom MCUMGR group (ID 64) for listing (ls)
and removing (rm) files via SMP.
# Logging configuration for the File System Management module
module = FS_MGMT
module-str = fs_mgmt
source "subsys/logging/Kconfig.template.log_config"
endif # FS_MGMT

View File

@@ -0,0 +1,7 @@
#ifndef FS_MGMT_H
#define FS_MGMT_H
int fs_mgmt_init(void);
#endif

View File

@@ -0,0 +1,327 @@
#include <zephyr/fs/fs.h>
#include <zephyr/fs/littlefs.h>
#include <zephyr/storage/flash_map.h>
#include <zephyr/logging/log.h>
#include <fs_mgmt.h>
LOG_MODULE_REGISTER(fs_mgmt, CONFIG_FS_MGMT_LOG_LEVEL);
#define STORAGE_PARTITION_ID FIXED_PARTITION_ID(littlefs_storage)
FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(fs_storage_data);
static struct fs_mount_t fs_storage_mnt = {
.type = FS_LITTLEFS,
.fs_data = &fs_storage_data,
.storage_dev = (void *)STORAGE_PARTITION_ID,
.mnt_point = CONFIG_FS_MGMT_MOUNT_POINT,
};
#ifdef CONFIG_FS_MGMT_MCUMGR_HANDLER
#include <zephyr/mgmt/mcumgr/mgmt/mgmt.h>
#include <zephyr/mgmt/mcumgr/smp/smp.h>
#include <zephyr/fs/fs.h>
#include <zcbor_decode.h>
#include <zcbor_encode.h>
#include <mgmt/mcumgr/util/zcbor_bulk.h>
#define CUSTOM_GROUP_ID 64
#define CMD_LS 0
#define CMD_RM 1
#define FS_MGMT_MAX_PATH_LEN 128
static int fs_mgmt_count_entries(const char *abs_path, bool recursive, int *count)
{
struct fs_dir_t dirp;
struct fs_dirent entry;
fs_dir_t_init(&dirp);
if (fs_opendir(&dirp, abs_path) != 0)
{
return -ENOENT;
}
while (fs_readdir(&dirp, &entry) == 0 && entry.name[0] != '\0')
{
(*count)++;
if (recursive && entry.type == FS_DIR_ENTRY_DIR)
{
char child_path[FS_MGMT_MAX_PATH_LEN];
int len = snprintk(child_path, sizeof(child_path), "%s/%s", abs_path, entry.name);
if (len <= 0 || len >= sizeof(child_path))
{
fs_closedir(&dirp);
return -ENAMETOOLONG;
}
int rc = fs_mgmt_count_entries(child_path, true, count);
if (rc != 0)
{
fs_closedir(&dirp);
return rc;
}
}
}
fs_closedir(&dirp);
return 0;
}
static bool fs_mgmt_encode_entries(zcbor_state_t *zse, const char *abs_path, const char *rel_prefix, bool recursive)
{
struct fs_dir_t dirp;
struct fs_dirent entry;
fs_dir_t_init(&dirp);
if (fs_opendir(&dirp, abs_path) != 0)
{
return false;
}
bool ok = true;
while (ok && fs_readdir(&dirp, &entry) == 0 && entry.name[0] != '\0')
{
const char *type_char = (entry.type == FS_DIR_ENTRY_DIR) ? "d" : "f";
char rel_name[FS_MGMT_MAX_PATH_LEN];
if (rel_prefix[0] == '\0')
{
int len = snprintk(rel_name, sizeof(rel_name), "%s", entry.name);
if (len <= 0 || len >= sizeof(rel_name))
{
ok = false;
break;
}
}
else
{
int len = snprintk(rel_name, sizeof(rel_name), "%s/%s", rel_prefix, entry.name);
if (len <= 0 || len >= sizeof(rel_name))
{
ok = false;
break;
}
}
ok = ok && zcbor_map_start_encode(zse, 2) &&
zcbor_tstr_put_lit(zse, "n") &&
zcbor_tstr_encode(zse, &(struct zcbor_string){.value = (const uint8_t *)rel_name, .len = strlen(rel_name)}) &&
zcbor_tstr_put_lit(zse, "t") &&
zcbor_tstr_encode(zse, &(struct zcbor_string){.value = (const uint8_t *)type_char, .len = 1}) &&
zcbor_map_end_encode(zse, 2);
if (ok && recursive && entry.type == FS_DIR_ENTRY_DIR)
{
char child_abs_path[FS_MGMT_MAX_PATH_LEN];
int len = snprintk(child_abs_path, sizeof(child_abs_path), "%s/%s", abs_path, entry.name);
if (len <= 0 || len >= sizeof(child_abs_path))
{
ok = false;
break;
}
ok = fs_mgmt_encode_entries(zse, child_abs_path, rel_name, true);
}
}
fs_closedir(&dirp);
return ok;
}
static int custom_ls_handler(struct smp_streamer *ctxt)
{
int file_count = 0;
char path[FS_MGMT_MAX_PATH_LEN] = "";
zcbor_state_t *zsd = ctxt->reader->zs;
zcbor_state_t *zse = ctxt->writer->zs;
/* --- DECODING --- */
struct zcbor_string res_path = {0};
bool path_found = false;
if (zcbor_map_start_decode(zsd))
{
while (zcbor_tstr_decode(zsd, &res_path))
{
if (res_path.len == 4 && memcmp(res_path.value, "path", 4) == 0)
{
struct zcbor_string path_val;
if (zcbor_tstr_decode(zsd, &path_val))
{
int len = MIN(path_val.len, sizeof(path) - 1);
memcpy(path, path_val.value, len);
path[len] = '\0';
path_found = true;
}
}
else
{
zcbor_any_skip(zsd, NULL);
}
}
zcbor_map_end_decode(zsd);
}
// If no path is provided, default to root
if (!path_found || strlen(path) == 0)
{
strcpy(path, "/");
}
/* --- PROCESSING & ENCODING --- */
int rc = fs_mgmt_count_entries(path, false, &file_count);
if (rc != 0)
{
return (rc == -ENOENT) ? MGMT_ERR_ENOENT : MGMT_ERR_EUNKNOWN;
}
bool ok = zcbor_tstr_put_lit(zse, "files") && zcbor_list_start_encode(zse, file_count);
ok = ok && fs_mgmt_encode_entries(zse, path, "", false);
ok = ok && zcbor_list_end_encode(zse, file_count);
return ok ? 0 : MGMT_ERR_ENOMEM;
}
static int custom_rm_handler(struct smp_streamer *ctxt)
{
char path[64] = {0};
zcbor_state_t *zsd = ctxt->reader->zs;
zcbor_state_t *zse = ctxt->writer->zs;
bool path_found = false;
struct zcbor_string key;
/* --- DECODING --- */
if (!zcbor_map_start_decode(zsd))
{
return MGMT_ERR_EINVAL;
}
while (zcbor_tstr_decode(zsd, &key))
{
if (key.len == 4 && memcmp(key.value, "path", 4) == 0)
{
struct zcbor_string val;
if (zcbor_tstr_decode(zsd, &val))
{
size_t len = MIN(val.len, sizeof(path) - 1);
memcpy(path, val.value, len);
path[len] = '\0';
path_found = true;
}
}
else
{
zcbor_any_skip(zsd, NULL);
}
}
zcbor_map_end_decode(zsd);
if (!path_found)
{
return MGMT_ERR_EINVAL;
}
/* --- PROCESSING --- */
int rc = fs_unlink(path);
/* --- ENCODING RESPONSE --- */
// Return the filesystem result code (0 = success)
bool ok = zcbor_tstr_put_lit(zse, "rc") && zcbor_int32_put(zse, rc);
if (rc != 0)
{
LOG_WRN("Failed to remove %s: %d", path, rc);
// Optional: map to more specific mgmt errors
return (rc == -ENOENT) ? MGMT_ERR_ENOENT : MGMT_ERR_EUNKNOWN;
}
return ok ? 0 : MGMT_ERR_ENOMEM;
}
static const struct mgmt_handler custom_handlers[] = {
[CMD_LS] = {
.mh_read = custom_ls_handler,
.mh_write = NULL},
[CMD_RM] = {
.mh_read = NULL,
.mh_write = custom_rm_handler // Use WRITE for delete operations
},
};
static struct mgmt_group custom_group = {
.mg_handlers = custom_handlers,
.mg_handlers_count = ARRAY_SIZE(custom_handlers),
.mg_group_id = CUSTOM_GROUP_ID,
};
#endif /* CONFIG_FS_MGMT_MCUMGR_HANDLER */
int fs_mgmt_init(void)
{
int rc;
LOG_DBG("Initializing filesystem management module");
rc = fs_mount(&fs_storage_mnt);
if (rc)
{
LOG_ERR("Filesystem mount failed (err %d)", rc);
return rc;
}
else
{
struct fs_statvfs stat;
uint64_t total;
uint64_t free;
LOG_DBG("Filesystem mounted successfully at %s", fs_storage_mnt.mnt_point);
rc = fs_statvfs(fs_storage_mnt.mnt_point, &stat);
if (rc == 0)
{
total = (uint64_t)stat.f_blocks * (uint64_t)stat.f_frsize;
free = (uint64_t)stat.f_bfree * (uint64_t)stat.f_frsize;
uint64_t total_kb = total / 1024;
uint64_t free_kb = free / 1024;
uint64_t used_kb = total_kb - free_kb;
uint64_t used_pct_x10 = 0;
LOG_DBG("Filesystem total size/used/free size: %4llu/%4llu/%4llu KB",
(unsigned long long)total_kb,
(unsigned long long)used_kb,
(unsigned long long)free_kb);
if (total_kb > 0)
{
used_pct_x10 = (used_kb * 1000) / total_kb;
}
if (used_pct_x10 >= 900)
{
LOG_ERR("Filesystem used: %llu.%llu%%",
(unsigned long long)(used_pct_x10 / 10),
(unsigned long long)(used_pct_x10 % 10));
}
else if (used_pct_x10 >= 750)
{
LOG_WRN("Filesystem used: %llu.%llu%%",
(unsigned long long)(used_pct_x10 / 10),
(unsigned long long)(used_pct_x10 % 10));
}
else
{
LOG_DBG("Filesystem used: %llu.%llu%%",
(unsigned long long)(used_pct_x10 / 10),
(unsigned long long)(used_pct_x10 % 10));
}
}
else
{
LOG_WRN("Filesystem statvfs failed (err %d)", rc);
}
}
#ifdef CONFIG_FS_MGMT_MCUMGR_HANDLER
mgmt_register_group(&custom_group);
LOG_DBG("Custom MCUMGR group registered with ID %d", CUSTOM_GROUP_ID);
#endif /* CONFIG_FS_MGMT_MCUMGR_HANDLER */
return 0;
}

View File

@@ -0,0 +1,16 @@
if(CONFIG_GAME_MGMT)
zephyr_library()
zephyr_library_sources(
src/game_mgmt.c
src/game_mgmt_coap.c
src/game_mgmt_timing.c
)
if(CONFIG_LASERTAG_ROLE_LEADER)
zephyr_library_sources(src/game_mgmt_thread.c)
endif()
if(CONFIG_LASERTAG_ROLE_LEADER)
zephyr_library_sources(src/game_mgmt_device_list.c)
endif()
zephyr_include_directories(include)
zephyr_library_include_directories(include_lib_only)
endif()

View File

@@ -0,0 +1,41 @@
menuconfig GAME_MGMT
bool "Game Management"
# select BT
select OPENTHREAD
select OPENTHREAD_COAP
select LASERTAG_UTILS
select AUDIO
select ENTROPY_GENERATOR
help
Library for managing game states and logic in the lasertag device.
if GAME_MGMT
config GAME_MGMT_SHELL
bool "Enable shell commands for Game Management"
select SHELL
default n
config GAME_MGMT_BEACON_INTERVAL_S
int "Game Management beacon interval (s)"
default 5
range 1 60
help
Interval in milliseconds for sending leader beacons.
config GAME_MGMT_BEACON_THREAD_PRIORITY
int "Game Management beacon thread priority"
default 10
range 0 10
help
Thread priority for the Game Management beaconing thread (leader device).
config GAME_MGMT_BEACON_THREAD_STACK_SIZE
int "Game Management beacon thread stack size"
default 1024
range 256 4096
help
Stack size for the Game Management beaconing thread (leader device).
# Logging configuration for the Game Management module
module = GAME_MGMT
module-str = game_mgmt
source "subsys/logging/Kconfig.template.log_config"
endif

View File

@@ -0,0 +1,200 @@
#ifndef GAME_MGMT_H
#define GAME_MGMT_H
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include <openthread/thread.h>
#include <lasertag_utils.h>
/**
* @brief System states for the Lasertag devices.
* These states define the behavior of the device in the network.
*/
typedef enum {
SYS_STATE_NO_CHANGE = 0x00, /* Placeholder for no state change */
SYS_STATE_IDLE = 0x01, /* Device is on but inactive */
SYS_STATE_LOBBY = 0x02, /* Discovery phase: Leader sends beacon, Nodes reply */
SYS_STATE_STARTING = 0x03, /* Countdown phase before match start */
SYS_STATE_RUNNING = 0x04, /* Match is active: IR and local logic enabled */
SYS_STATE_POST_GAME = 0x05 /* Match ended: Data collection and review */
} sys_state_t;
/**
* @brief Game control command structure for THREAD communication.
* This structure is used to send control commands (start/end game, set ID)
* over the Thread network. It is designed to be flexible for future
* expansion.
*/
typedef enum
{
GAME_CTRL_CMD_START_GAME = 0x00, /* Command to start a game, includes start time and duration */
GAME_CTRL_CMD_REQUEST_ABORT_START_NO_TIME_SYNC = 0x01, /* Request to abort a pending game start when sender has no time sync */
GAME_CTRL_CMD_ABORT_START = 0x0F, /* Command to abort a pending game start */
GAME_CTRL_CMD_END_GAME = 0x20, /* Command to end the current game */
GAME_CTRL_CMD_SET_ID = 0x30, /* Command to set the game ID */
// Future commands can be added here
} game_ctrl_command_t;
/**
* @brief Payload structure for game control messages sent over Thread.
* The union allows for different data formats based on the command type.
*/
typedef struct __packed
{
game_ctrl_command_t command;
union
{
struct
{
uint32_t start_time; // lower 32 bits of OpenThread network time in microseconds
uint32_t duration; // in seconds, maximum 49 days (2^32 seconds), should be sufficient for any match ;)
} start_game;
struct
{
uint64_t game_id; // Unique identifier for the game, can be used for stats tracking or match history
} game_id;
uint8_t raw[16]; // For future expansion or custom commands
} data;
} game_control_payload_t;
/**
* @brief Leader beacon payload sent periodically during lobby/discovery.
*/
typedef struct __packed
{
uint64_t game_id;
} game_leader_beacon_payload_t;
/**
* @brief Player presence payload sent as unicast response to a leader beacon.
*/
typedef struct __packed
{
lasertag_device_type_t device_type;
uint8_t player_id;
uint8_t team_id;
} game_player_presence_payload_t;
typedef struct {
uint8_t eui64[8];
lasertag_device_type_t device_type;
uint8_t player_id;
uint8_t team_id;
uint8_t missed;
} game_device_info_t;
/**
* @brief Callback for state changes.
* Allows apps/modules to react when the system transitions (e.g., UI updates).
*/
typedef void (*game_mgmt_state_cb_t)(sys_state_t new_state);
/**
* @brief Initialize the game management module.
* Sets the initial state and prepares timers/workqueues.
* @return 0 on success.
*/
int game_mgmt_init(void);
/**
* @brief Send a game control payload as Thread multicast CoAP message.
* Uses realm-local all-nodes multicast address `ff03::1`.
* @param payload Payload to send.
* @return 0 on success, negative errno-style value on failure.
*/
int game_mgmt_send_control_multicast(const game_control_payload_t *payload);
/**
* @brief Send a game control payload as Thread unicast CoAP message.
* @param peer_addr_str IPv6 destination address string.
* @param payload Payload to send.
* @return 0 on success, negative errno-style value on failure.
*/
int game_mgmt_send_control_unicast(const otIp6Address *peer_addr, const game_control_payload_t *payload);
/**
* @brief Generic multicast sender for Thread CoAP messages.
* Uses realm-local all-nodes multicast address `ff03::1`.
* @param uri_path CoAP URI path (without leading slash), e.g. `g`.
* @param payload Raw payload buffer.
* @param payload_len Payload size in bytes.
* @return 0 on success, negative errno-style value on failure.
*/
int game_mgmt_send_multicast(const char *uri_path, const void *payload, size_t payload_len);
/**
* @brief Generic unicast sender for Thread CoAP messages.
* @param peer_addr_str IPv6 destination address string.
* @param uri_path CoAP URI path (without leading slash), e.g. `g`.
* @param payload Raw payload buffer.
* @param payload_len Payload size in bytes.
* @return 0 on success, negative errno-style value on failure.
*/
int game_mgmt_send_unicast(const char *peer_addr_str,
const char *uri_path,
const void *payload,
size_t payload_len);
/**
* @brief Send leader beacon via multicast.
* @param payload Beacon payload.
* @return 0 on success, negative errno-style value on failure.
*/
int game_mgmt_send_leader_beacon_multicast(const game_leader_beacon_payload_t *payload);
/**
* @brief Send player presence as unicast response to leader.
* @param leader_addr_str IPv6 address string of the leader.
* @param payload Player presence payload.
* @return 0 on success, negative errno-style value on failure.
*/
int game_mgmt_send_player_presence_unicast(const char *leader_addr_str,
const game_player_presence_payload_t *payload);
/**
* @brief Set the system state and trigger corresponding actions.
* Leader: Starts/stops beacons. Nodes: Starts/stops heartbeats.
* @param state The target state to transition to.
*/
void game_mgmt_set_state(sys_state_t state);
/**
* @brief Returns the current system state.
* @return Current sys_state_t value.
*/
sys_state_t game_mgmt_get_state(void);
/**
* @brief Set the current game ID.
* @param id The game ID to set.
*/
void game_mgmt_set_game_id(uint64_t id);
/**
* @brief Get the current game ID.
* @return The current game ID.
*/
uint64_t game_mgmt_get_game_id(void);
/**
* @brief Registers a callback for state changes.
* @param cb Function to be called on transition.
*/
void game_mgmt_register_state_cb(game_mgmt_state_cb_t cb);
/**
* @brief Set the unicast address of the current leader.
* This is used for sending unicast messages (e.g., presence responses).
* @param addr Pointer to the leader's IPv6 address, or NULL to clear.
*/
void game_mgmt_set_leader_unicast_addr(const otIp6Address *addr);
/**
* @brief Get the unicast address of the current leader.
* @param addr Pointer to an otIp6Address struct to be filled with the leader's address.
* @return true if a valid leader address is set, false if not.
*/
bool game_mgmt_get_leader_unicast_addr(otIp6Address *addr);
#endif /* GAME_MGMT_H */

View File

@@ -0,0 +1,171 @@
#include <zephyr/logging/log.h>
#include <errno.h>
#include <stdlib.h>
#include <thread_mgmt.h>
#include <game_mgmt.h>
#include <lasertag_utils.h>
#include <game_mgmt_coap.h>
#include <game_mgmt_timing.h>
#include <game_mgmt_device_list.h>
LOG_MODULE_REGISTER(game_mgmt, CONFIG_GAME_MGMT_LOG_LEVEL);
static sys_state_t g_current_state = SYS_STATE_IDLE;
static uint64_t g_current_game_id = 0;
static otIp6Address g_leader_unicast_addr;
int game_mgmt_init(void)
{
int err = game_mgmt_coap_init();
if (err)
{
LOG_ERR("Failed to initialize CoAP service: %d", err);
return err;
}
#if IS_ENABLED(CONFIG_LASERTAG_ROLE_LEADER)
static const char* device_type = "Leader";
game_mgmt_device_list_init();
#elif IS_ENABLED(CONFIG_LASERTAG_ROLE_WEAPON)
static const char* device_type = "Weapon";
#elif IS_ENABLED(CONFIG_LASERTAG_ROLE_VEST)
static const char* device_type = "Vest";
#else
static const char* device_type = "Unknown";
#endif
LOG_INF("Game management initialized. Device type: " FORMAT_BRIGHT_GREEN_BOLD("%s"), device_type);
return 0;
}
void game_mgmt_set_leader_unicast_addr(const otIp6Address *addr)
{
if (addr)
{
g_leader_unicast_addr = *addr;
}
else
{
memset(&g_leader_unicast_addr, 0, sizeof(g_leader_unicast_addr));
}
}
bool game_mgmt_get_leader_unicast_addr(otIp6Address *addr)
{
if (addr && !otIp6IsAddressUnspecified(&g_leader_unicast_addr))
{
*addr = g_leader_unicast_addr;
return true;
}
return false;
}
void game_mgmt_set_state(sys_state_t state)
{
if (g_current_state == state)
{
return;
}
LOG_DBG("State change: %d -> %d", g_current_state, state);
g_current_state = state;
}
sys_state_t game_mgmt_get_state(void)
{
return g_current_state;
}
void game_mgmt_set_game_id(uint64_t id)
{
if (g_current_game_id == id)
{
return;
}
g_current_game_id = id;
LOG_DBG("Game ID updated: 0x%llx", id);
}
uint64_t game_mgmt_get_game_id(void)
{
return g_current_game_id;
}
#if IS_ENABLED(CONFIG_GAME_MGMT_SHELL)
#include <zephyr/shell/shell.h>
static int cmd_game_start(const struct shell *sh, size_t argc, char **argv)
{
char *endptr = NULL;
uint32_t delay_s = (uint32_t)strtoul(argv[1], &endptr, 10);
if ((endptr == argv[1]) || (*endptr != '\0'))
{
shell_error(sh, "Invalid delay_s: %s", argv[1]);
return -EINVAL;
}
uint32_t duration_s = 600;
if (argc > 2)
{
endptr = NULL;
duration_s = (uint32_t)strtoul(argv[2], &endptr, 10);
if ((endptr == argv[2]) || (*endptr != '\0'))
{
shell_error(sh, "Invalid duration_s: %s", argv[2]);
return -EINVAL;
}
}
game_control_payload_t payload = {.command = GAME_CTRL_CMD_START_GAME};
uint64_t now = thread_mgmt_get_network_time();
if (now == 0)
{
shell_error(sh, "Network time not synchronized, cannot start game.");
return -EAGAIN;
}
payload.data.start_game.start_time = (uint32_t)(now + (delay_s * 1000000ULL));
payload.data.start_game.duration = duration_s;
int err = game_mgmt_send_control_multicast(&payload);
if (err)
{
shell_error(sh, "game_mgmt_send_control_multicast failed: %d", err);
return err;
}
shell_print(sh, "Start broadcast sent for T+%u s (duration: %u s).", delay_s, duration_s);
game_mgmt_schedule_start(game_mgmt_expand_t32_us(payload.data.start_game.start_time, now), duration_s);
return 0;
}
static int cmd_game_abort(const struct shell *sh, size_t argc, char **argv)
{
ARG_UNUSED(argc);
ARG_UNUSED(argv);
game_control_payload_t payload = {
.command = GAME_CTRL_CMD_ABORT_START,
};
int err = game_mgmt_send_control_multicast(&payload);
if (err)
{
shell_error(sh, "Failed to send abort broadcast: %d", err);
return err;
}
game_mgmt_cancel_scheduled_start("manual shell abort");
shell_print(sh, "Abort broadcast sent.");
return 0;
}
SHELL_STATIC_SUBCMD_SET_CREATE(game_sub,
SHELL_CMD_ARG(start, NULL, "<delay_s> [duration_s]", cmd_game_start, 2, 1),
SHELL_CMD_ARG(abort, NULL, "", cmd_game_abort, 1, 0),
SHELL_SUBCMD_SET_END);
SHELL_CMD_REGISTER(game, &game_sub, "Game Management", NULL);
#endif

View File

@@ -1,2 +1,9 @@
add_subdirectory(send)
add_subdirectory(recv)
if(CONFIG_IR_SEND)
add_subdirectory(send)
zephyr_include_directories(include)
endif()
if(CONFIG_IR_RECV)
add_subdirectory(recv)
zephyr_include_directories(include)
endif()

View File

@@ -1,73 +1,2 @@
menu "IR protocol"
config IR_PROTO
bool "IR protocol core"
help
Enable shared IR protocol settings used by send/receive libraries.
if IR_PROTO
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.
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_PROTO_BASE_US
int "IR base period (microseconds)"
default 600
range 300 1000
help
Base timing used for start/mark/space. Default 600 µs (similar to Sony SIRC).
config IR_PROTO_START_MULT
int "Start burst factor"
default 4
range 2 8
help
Start burst duration = base × factor. Default 4× for robust sync.
config IR_PROTO_GAP_MULT
int "Start gap factor"
default 1
range 0 4
help
Gap after start burst (carrier off) = base × factor. 0 disables the gap.
config IR_PROTO_MARK_MULT
int "Mark factor"
default 1
range 1 2
help
Mark duration = base × factor. Default 1×.
config IR_PROTO_SPACE0_MULT
int "Space0 factor (bit 0)"
default 1
range 1 3
help
Space for bit 0 = base × factor. Default 1×.
config IR_PROTO_SPACE1_MULT
int "Space1 factor (bit 1)"
default 2
range 1 4
help
Space for bit 1 = base × factor. Default 2× (double base time).
endmenu
endif
endmenu
rsource "recv/Kconfig"
rsource "send/Kconfig"

View File

@@ -0,0 +1,19 @@
#ifndef IR_H
#define IR_H
#include <stdint.h>
// structure representing a decoded IR packet
typedef struct {
union {
uint8_t bytes[3];
struct {
uint32_t type : 3;
uint32_t id : 8; /* Ersetzt shooter_id / healer_id */
uint32_t value : 5; /* Ersetzt damage / amount */
uint32_t crc : 8;
} fields;
} data;
} __attribute__((packed)) ir_packet_t;
#endif /* IR_H */

View File

@@ -1,5 +1,6 @@
if(CONFIG_IR_RECV)
zephyr_library()
zephyr_sources(src/ir_recv.c)
zephyr_library_sources(src/ir_recv.c)
zephyr_include_directories(include)
zephyr_library_link_libraries_ifdef(CONFIG_NRFX nrfx)
endif()

View File

@@ -1,6 +1,89 @@
config IR_RECV
bool "IR Receive Library"
select IR_PROTO
menuconfig IR_RECV
bool "IR Receiver"
help
Enable IR receive library for the laser tag system.
Placeholder implementation; timing parameters come from IR_PROTO.*
Enable support for receiving IR signals using the ADC and DMA.
if IR_RECV
config IR_RECV_HW_DEPENDENCIES
bool
default y if !IR_RECV_SIMULATOR
select ADC
select NRFX_GPPI
select NRFX_TIMER
select NRFX_TIMER1
select NRFX_PPI
select NRFX_SAADC
config IR_RECV_SIMULATOR
bool "Enable IR receiver simulator"
select ENTROPY_GENERATOR
help
Replaces real ADC/Hardware input with a software-based sample generator
for protocol testing.
config IR_RECV_BUFFER_COUNT
int "Number of DMA buffers"
default 8
range 2 16
help
Number of buffers in the circular chain to handle CPU jitter.
config IR_RECV_SAMPLES_PER_BUFFER
int "Samples per channel per buffer"
default 32
help
Number of samples for each of the 4 channels (3x IR, 1x Vbat) per buffer.
config IR_RECV_INVERT_SIGNAL
bool "Invert IR input signal"
default n
help
Invert logic: High-level means IR carrier detected.
config IR_RECV_THREAD_PRIO_SIM
int "Simulator thread priority"
default 4
range 0 20
help
Thread priority for the IR receive simulator.
config IR_RECV_THREAD_STACK_SIM
int "Simulator thread stack size"
default 1024
range 256 8192
help
Stack size in bytes for the IR receive simulator thread.
config IR_RECV_THREAD_PRIO_ADC
int "ADC sampling thread priority"
default 2
range 0 20
help
Thread priority for hardware ADC sampling.
config IR_RECV_THREAD_STACK_ADC
int "ADC sampling thread stack size"
default 1024
range 256 8192
help
Stack size in bytes for the hardware ADC sampling thread.
config IR_RECV_THREAD_PRIO_PROCESS
int "IR processing thread priority"
default 3
range 0 20
help
Thread priority for IR buffer processing.
config IR_RECV_THREAD_STACK_PROCESS
int "IR processing thread stack size"
default 2048
range 256 8192
help
Stack size in bytes for the IR processing thread.
# Logging configuration for the IR Receiver module
module = IR_RECV
module-str = ir_recv
source "subsys/logging/Kconfig.template.log_config"
endif

View File

@@ -1,7 +1,7 @@
#ifndef IR_RECV_H
#define IR_RECV_H
#include <zephyr/device.h>
#include <ir.h>
/**
* @brief Initialize IR receive pipeline (stub).
@@ -11,4 +11,19 @@
*/
int ir_recv_init(void);
#ifdef CONFIG_IR_RECV_SIMULATOR
/* Configuration for injecting signal errors */
typedef struct {
uint8_t noise_flips_per_8; /* Noise: number of inverted samples per block of 8 (0, 1, 2) */
uint8_t jitter_mark; /* Jitter for mark pulse: +/- samples (e.g. 2) */
uint8_t jitter_space_0; /* Jitter for space0: +/- samples (e.g. 4) */
uint8_t jitter_space_1; /* Jitter for space1: +/- samples (e.g. 2) */
} ir_sim_error_t;
/* err can be NULL to send a perfect signal */
void ir_recv_sim_send_packet(ir_packet_t *packet, ir_sim_error_t *err);
#endif
#endif /* IR_RECV_H */

View File

@@ -1,12 +1,483 @@
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <string.h>
#include <ir_recv.h>
#include <ir.h>
#include <lasertag_utils.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include "ir_recv.h"
#include "lasertag_utils.h"
LOG_MODULE_REGISTER(ir_recv, LOG_LEVEL_INF);
LOG_MODULE_REGISTER(ir_recv, CONFIG_IR_RECV_LOG_LEVEL);
int ir_recv_init(void)
#define ADC_CHANNELS 4
#define SAMPLES_PER_BUFFER CONFIG_IR_RECV_SAMPLES_PER_BUFFER
#define BUFFER_COUNT CONFIG_IR_RECV_BUFFER_COUNT
static int16_t adc_buffers[BUFFER_COUNT][SAMPLES_PER_BUFFER * ADC_CHANNELS];
static uint8_t write_idx = 0, read_idx = 0;
static struct k_sem adc_sem;
/* --- Enhanced Simulator --- */
#ifdef CONFIG_IR_RECV_SIMULATOR
#include <zephyr/random/random.h>
K_SEM_DEFINE(sim_start_sem, 0, 1);
#define SIM_MAX_SAMPLES 1000
static bool sim_buffer[SIM_MAX_SAMPLES];
static uint32_t sim_total_samples = 0;
static bool sim_trigger = false;
static uint32_t sim_sample_pos = 0;
/* Hilfsfunktion für den Jitter: Liefert einen Zufallswert zwischen -max und +max */
static int get_jitter(uint8_t max_jitter)
{
LOG_INF("IR receive stub initialized (no implementation yet)");
if (max_jitter == 0)
return 0;
return (int)(sys_rand32_get() % (max_jitter * 2 + 1)) - max_jitter;
}
void ir_recv_sim_send_packet(ir_packet_t *packet, ir_sim_error_t *err)
{
/* Blockieren, bis vorheriges Paket abgearbeitet ist */
while (sim_trigger)
{
k_msleep(5);
}
ir_packet_t pkt;
memcpy(&pkt, packet, sizeof(ir_packet_t));
pkt.data.fields.crc = lastertag_crc8(pkt.data.bytes, 2);
ir_sim_error_t default_err = {0};
if (err == NULL)
{
err = &default_err;
}
sim_total_samples = 0;
/* 1. Header Mark */
int m_len = 32 + get_jitter(err->jitter_mark);
for (int i = 0; i < m_len && sim_total_samples < SIM_MAX_SAMPLES; i++)
{
sim_buffer[sim_total_samples++] = 1;
}
/* 2. Header Gap */
int g_len = 8 + get_jitter(err->jitter_space_0);
for (int i = 0; i < g_len && sim_total_samples < SIM_MAX_SAMPLES; i++)
{
sim_buffer[sim_total_samples++] = 0;
}
/* 3. Payload Bits (24 Bit) */
for (int i = 0; i < 24; i++)
{
bool bit = (pkt.data.bytes[i / 8] >> (i % 8)) & 1;
/* Space Phase */
int s_base = bit ? 16 : 8;
int s_jit = bit ? get_jitter(err->jitter_space_1) : get_jitter(err->jitter_space_0);
int s_len = s_base + s_jit;
for (int j = 0; j < s_len && sim_total_samples < SIM_MAX_SAMPLES; j++)
{
sim_buffer[sim_total_samples++] = 0;
}
/* Mark Phase */
int bm_len = 8 + get_jitter(err->jitter_mark);
for (int j = 0; j < bm_len && sim_total_samples < SIM_MAX_SAMPLES; j++)
{
sim_buffer[sim_total_samples++] = 1;
}
}
/* 4. Rauschen injizieren (Bit Flips) */
if (err->noise_flips_per_8 > 0)
{
for (uint32_t i = 0; i < sim_total_samples; i += 8)
{
for (int f = 0; f < err->noise_flips_per_8; f++)
{
uint32_t flip_idx = i + (sys_rand32_get() % 8);
if (flip_idx < sim_total_samples)
{
sim_buffer[flip_idx] = !sim_buffer[flip_idx]; /* Bit kippen */
}
}
}
}
sim_sample_pos = 0;
sim_trigger = true;
sim_sample_pos = 0;
sim_trigger = true;
k_sem_give(&sim_start_sem);
LOG_DBG("Simulator: Queued (Type: %u, CRC: 0x%02X), Total Samples: %u",
pkt.data.fields.type, pkt.data.fields.crc, sim_total_samples);
}
void ir_recv_sim_thread(void *p1, void *p2, void *p3)
{
while (1)
{
if (!sim_trigger)
{
k_sem_take(&sim_start_sem, K_FOREVER);
}
k_usleep(75 * SAMPLES_PER_BUFFER);
if (!sim_trigger)
continue;
int16_t *buf = adc_buffers[write_idx];
for (int i = 0; i < SAMPLES_PER_BUFFER; i++)
{
bool level = 0;
if (sim_sample_pos < sim_total_samples)
{
level = sim_buffer[sim_sample_pos];
}
sim_sample_pos++; /* Unbedingtes Inkrement */
bool active = IS_ENABLED(CONFIG_IR_RECV_INVERT_SIGNAL) ? !level : level;
buf[i * ADC_CHANNELS] = active ? 0x0FFF : 0x0000;
buf[i * ADC_CHANNELS + 3] = 2400; // VBat Dummy
}
write_idx = (write_idx + 1) % BUFFER_COUNT;
k_sem_give(&adc_sem);
/* Nach dem Puffer noch eine kurze Pause einhalten, um das Paket abzuschließen */
if (sim_sample_pos >= sim_total_samples + 50)
{
sim_trigger = false;
LOG_DBG("Simulation sequence finished.");
}
}
}
K_THREAD_DEFINE(sim_tid, CONFIG_IR_RECV_THREAD_STACK_SIM, ir_recv_sim_thread, NULL, NULL, NULL,
CONFIG_IR_RECV_THREAD_PRIO_SIM, 0, 0);
#endif // CONFIG_IR_RECV_SIMULATOR
#ifndef CONFIG_IR_RECV_SIMULATOR
#include <zephyr/drivers/adc.h>
#include <nrfx_timer.h>
#include <helpers/nrfx_gppi.h>
#include <hal/nrf_saadc.h>
/* ADC-Kanäle dynamisch aus dem DeviceTree (zephyr,user) laden */
static const struct adc_dt_spec adc_channels[] = {
ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 0),
ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 1),
ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 2),
ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), 3)
};
static int hw_adc_setup(void)
{
int err;
for (int i = 0; i < ARRAY_SIZE(adc_channels); i++) {
if (err) {
LOG_ERR("ADC controller for channel %d not ready", i);
return err;
}
err = adc_channel_setup_dt(&adc_channels[i]);
if (err) {
LOG_ERR("Could not setup channel %d (err: %d)", i, err);
return err;
}
}
LOG_DBG("Hardware ADC configured via DeviceTree (EasyDMA Mode).");
return 0;
}
/* Wir reservieren uns Timer 1 für den ADC-Takt */
static nrfx_timer_t adc_timer = NRFX_TIMER_INSTANCE(1);
void hw_adc_thread(void *p1, void *p2, void *p3)
{
/* 1. Hardware Timer auf 1 MHz einstellen */
nrfx_timer_config_t timer_cfg = NRFX_TIMER_DEFAULT_CONFIG(NRF_TIMER_FREQ_1MHz);
timer_cfg.bit_width = NRF_TIMER_BIT_WIDTH_32;
nrfx_timer_init(&adc_timer, &timer_cfg, NULL);
/* Compare-Event bei exakt 75 µs auslösen und Timer danach automatisch nullen */
nrfx_timer_extended_compare(&adc_timer, NRF_TIMER_CC_CHANNEL0, 75,
NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK, false);
/* 2. PPI: Verbinde das Timer-Event direkt in Hardware mit dem ADC-Sample-Trigger */
nrfx_gppi_handle_t ppi_handle;
int err = nrfx_gppi_conn_alloc(
nrfx_timer_event_address_get(&adc_timer, NRF_TIMER_EVENT_COMPARE0),
nrf_saadc_task_address_get(NRF_SAADC, NRF_SAADC_TASK_SAMPLE),
&ppi_handle);
if (err != 0) {
LOG_ERR("GPPI connection alloc failed: %d", err);
return;
}
/* Alles einschalten */
nrfx_gppi_conn_enable(ppi_handle);
nrfx_timer_enable(&adc_timer);
/* 3. Zephyr ADC-Sequenz (Ohne Software-Intervall!) */
struct adc_sequence_options options = {
.extra_samplings = SAMPLES_PER_BUFFER - 1,
.interval_us = 0,
};
struct adc_sequence sequence = {
.options = &options,
.channels = BIT(adc_channels[0].channel_id) |
BIT(adc_channels[1].channel_id) |
BIT(adc_channels[2].channel_id) |
BIT(adc_channels[3].channel_id),
.buffer_size = SAMPLES_PER_BUFFER * ADC_CHANNELS * sizeof(int16_t),
.resolution = adc_channels[0].resolution,
.oversampling = 0,
.calibrate = false,
};
while (1) {
sequence.buffer = adc_buffers[write_idx];
/* Zephyr setzt den DMA-Speicher auf und wartet auf das Ende der 32 Samples.
* Den Startschuss für jedes einzelne Sample feuert ab sofort im Hintergrund
* das PPI-Modul exakt alle 75µs ab. Die CPU schläft hier tief und fest! */
int err = adc_read(adc_channels[0].dev, &sequence);
if (err < 0) {
LOG_ERR("ADC read error: %d", err);
k_msleep(10);
continue;
}
write_idx = (write_idx + 1) % BUFFER_COUNT;
k_sem_give(&adc_sem);
}
}
K_THREAD_DEFINE(hw_adc_tid, CONFIG_IR_RECV_THREAD_STACK_ADC, hw_adc_thread, NULL, NULL, NULL,
CONFIG_IR_RECV_THREAD_PRIO_ADC, 0, 0);
#endif // !CONFIG_IR_RECV_SIMULATOR
typedef enum
{
IR_STATE_IDLE,
IR_STATE_HEADER_SYNC,
IR_STATE_WAIT_SPACE,
IR_STATE_FIND_MARK,
IR_STATE_SYNC_MARK,
IR_STATE_VALIDATE
} ir_state_t;
typedef struct
{
ir_state_t state;
uint32_t sample_window;
uint32_t bit_acc;
uint16_t timer;
uint8_t bit_count;
uint8_t max_energy;
uint16_t timer_at_max;
uint8_t sync_window;
} ir_ctx_t;
static ir_ctx_t channels[3];
static void process_ir_sample(ir_ctx_t *ctx, int16_t raw)
{
bool active = (raw > 2048);
if (IS_ENABLED(CONFIG_IR_RECV_INVERT_SIGNAL))
{
active = !active;
}
/* 32-Bit Shift-Register aktualisieren */
ctx->sample_window = (ctx->sample_window << 1) | (active ? 1 : 0);
/* Energie über 32 Samples (Header) und 8 Samples (Bits) berechnen */
uint8_t energy_32 = __builtin_popcount(ctx->sample_window);
uint8_t energy_8 = __builtin_popcount(ctx->sample_window & 0xFF);
switch (ctx->state)
{
case IR_STATE_IDLE:
if (energy_32 >= 24)
{
ctx->state = IR_STATE_HEADER_SYNC;
ctx->timer = 0;
}
break;
case IR_STATE_HEADER_SYNC:
ctx->timer++;
/* Entspannt: 3 von 8 Samples dürfen verrauscht sein, es gilt trotzdem als Gap */
if (energy_8 <= 3)
{
ctx->state = IR_STATE_FIND_MARK;
ctx->timer = 0;
ctx->bit_count = 0;
ctx->bit_acc = 0;
}
else if (ctx->timer > 50)
{
ctx->state = IR_STATE_IDLE; /* Timeout */
}
break;
case IR_STATE_FIND_MARK:
ctx->timer++;
/* Trigger bei 4 bleibt. Ab 4 Einsen fangen wir an, das Maximum zu suchen */
if (energy_8 >= 4)
{
ctx->state = IR_STATE_SYNC_MARK;
ctx->max_energy = energy_8;
ctx->timer_at_max = ctx->timer;
ctx->sync_window = 0;
}
else if (ctx->timer > 40)
{
ctx->state = IR_STATE_IDLE;
}
break;
case IR_STATE_SYNC_MARK:
ctx->timer++;
ctx->sync_window++;
if (energy_8 > ctx->max_energy)
{
ctx->max_energy = energy_8;
ctx->timer_at_max = ctx->timer;
}
if (ctx->sync_window >= 10)
{
/* Entspannt: Wenn der Peak mindestens 5 Einsen hat, ist es ein validierter Puls */
if (ctx->max_energy >= 5)
{
bool bit = (ctx->timer_at_max >= 20);
ctx->bit_acc = (ctx->bit_acc >> 1) | (bit ? (1 << 23) : 0);
LOG_DBG("Bit %u: %d (T_Max:%u, E_Max:%u)",
ctx->bit_count, bit, ctx->timer_at_max, ctx->max_energy);
if (++ctx->bit_count >= 24)
{
ctx->state = IR_STATE_VALIDATE;
}
else
{
ctx->state = IR_STATE_WAIT_SPACE;
/* BITWEISER RESYNC: Ja, das funktioniert exakt wie gewünscht.
* Der Timer wird so weit zurückgeschraubt, als ob er GENAU am
* Peak (timer_at_max) auf 0 gesetzt worden wäre. */
ctx->timer = ctx->timer - ctx->timer_at_max;
}
}
else
{
ctx->state = IR_STATE_IDLE;
LOG_DBG("Mark sync failed. Max Energy: %u", ctx->max_energy);
}
}
break;
case IR_STATE_WAIT_SPACE:
ctx->timer++;
/* Entspannt: Auch hier lassen wir bis zu 3 Stör-Samples im Space zu */
if (energy_8 <= 3)
{
ctx->state = IR_STATE_FIND_MARK;
}
else if (ctx->timer > 30)
{
ctx->state = IR_STATE_IDLE;
LOG_DBG("Wait space timeout.");
}
break;
case IR_STATE_VALIDATE:
{
ir_packet_t p;
p.data.bytes[0] = ctx->bit_acc & 0xFF;
p.data.bytes[1] = (ctx->bit_acc >> 8) & 0xFF;
p.data.bytes[2] = (ctx->bit_acc >> 16) & 0xFF;
if (lastertag_crc8(p.data.bytes, 2) == p.data.fields.crc)
{
LOG_DBG(FORMAT_BLUE_BOLD("VALID: Type %u, ID %u, Val %u"),
p.data.fields.type,
p.data.fields.id,
p.data.fields.value);
}
else
{
LOG_WRN("CRC Error! Acc: 0x%06X", ctx->bit_acc);
}
ctx->state = IR_STATE_IDLE;
}
break;
}
}
/**
* @brief Main processing thread for incoming ADC buffers.
*/
void ir_recv_thread(void *arg1, void *arg2, void *arg3)
{
while (1)
{
k_sem_take(&adc_sem, K_FOREVER);
while (read_idx != write_idx)
{
int16_t *buf = adc_buffers[read_idx];
for (int i = 0; i < SAMPLES_PER_BUFFER; i++)
{
/* Now this call matches the static function name above */
process_ir_sample(&channels[0], buf[i * ADC_CHANNELS + 0]);
process_ir_sample(&channels[1], buf[i * ADC_CHANNELS + 1]);
process_ir_sample(&channels[2], buf[i * ADC_CHANNELS + 2]);
}
read_idx = (read_idx + 1) % BUFFER_COUNT;
}
}
}
K_THREAD_DEFINE(ir_recv_tid, CONFIG_IR_RECV_THREAD_STACK_PROCESS, ir_recv_thread, NULL, NULL, NULL,
CONFIG_IR_RECV_THREAD_PRIO_PROCESS, 0, 0);
/**
* @brief Initialization of the IR receiver module.
*/
int ir_recv_init(void)
{
k_sem_init(&adc_sem, 0, BUFFER_COUNT);
for (int i = 0; i < 3; i++)
{
channels[i].state = IR_STATE_IDLE;
}
#ifndef CONFIG_IR_RECV_SIMULATOR
int err = hw_adc_setup();
if (err) {
return err;
}
#endif
LOG_DBG("IR Receiver initialized. Mode: %s",
IS_ENABLED(CONFIG_IR_RECV_SIMULATOR) ? "Simulator" : "Hardware");
return 0;
}

View File

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

View File

@@ -1,71 +0,0 @@
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_PROTO_BASE_US
int "IR Basistakt (Mikrosekunden)"
default 600
range 300 1000
help
Gemeinsamer Basistakt für Mark/Space/Start. Standard 600 µs (Sony SIRC ähnlich).
config IR_PROTO_START_MULT
int "Startburst Faktor"
default 4
range 2 8
help
Startburst-Dauer = Basistakt × Faktor. Standard 4× zur sicheren Synchronisation.
config IR_PROTO_GAP_MULT
int "Start-Gap Faktor"
default 1
range 0 4
help
Gap nach Startburst (Träger AUS) = Basistakt × Faktor. 0 deaktiviert Gap.
config IR_PROTO_MARK_MULT
int "Mark Faktor"
default 1
range 1 2
help
Mark-Dauer = Basistakt × Faktor. Standard 1×.
config IR_PROTO_SPACE0_MULT
int "Space0 Faktor (Bit 0)"
default 1
range 1 3
help
Space für Bit 0 = Basistakt × Faktor. Standard 1×.
config IR_PROTO_SPACE1_MULT
int "Space1 Faktor (Bit 1)"
default 2
range 1 4
help
Space für Bit 1 = Basistakt × Faktor. Standard 2× (doppelte Basiszeit).
endmenu
endif

View File

@@ -1,10 +1,5 @@
if(CONFIG_LASERTAG_UTILS)
# Register this as a Zephyr library
zephyr_library()
# Add source files
zephyr_library_sources(src/lasertag_utils.c)
# Export the include directory to all applications
zephyr_include_directories(include)
endif()

View File

@@ -1,15 +1,20 @@
menuconfig LASERTAG_UTILS
bool "Lasertag Utilities"
depends on BLE_MGMT
select SETTINGS
select FLASH
select NVS
help
Enable shared logic for all lasertag devices (Vest, Weapon, Leader).
if LASERTAG_UTILS
config LASERTAG_UTILS_LOG_LEVEL
int "Utility Log Level"
default 3
config LASERTAG_UTILS_WD_TIMEOUT
int "Watchdog Timeout (ms)"
range 1000 60000
default 5000
help
Set the verbosity of the lasertag utility library.
Set the timeout for the lasertag utility watchdog timer.
Minimum is 1000 ms, maximum is 60000 ms (1 minute). Default is 5000 ms (5 seconds).
config LASERTAG_SHELL
bool "Enable Lasertag Shell Commands"
@@ -17,4 +22,9 @@ if LASERTAG_UTILS
depends on SHELL
help
Provides commands like 'lasertag name' and 'lasertag reboot'.
endif
# Logging configuration for the Lasertag Utilities module
module = LASERTAG_UTILS
module-str = lt_utils
source "subsys/logging/Kconfig.template.log_config"
endif

View File

@@ -3,6 +3,17 @@
#include <stdint.h>
/**
* @brief Enumeration for Lasertag device types.
* This is used to identify the role of each device in the game (leader, weapon, vest, or none). It can be extended in the future if needed.
*/
typedef enum {
DEVICE_TYPE_NONE = 0x00,
DEVICE_TYPE_LEADER = 0x10,
DEVICE_TYPE_WEAPON = 0x20,
DEVICE_TYPE_VEST = 0x30,
} lasertag_device_type_t;
/**
* @file lasertag_utils.h
* @brief Common utility functions for the lasertag system.
@@ -19,36 +30,6 @@ void lasertag_utils_init(void);
*/
const char* lasertag_get_device_name(void);
/**
* @brief Get the configured Thread PAN ID.
* @return 16-bit PAN ID.
*/
uint16_t lasertag_get_thread_pan_id(void);
/**
* @brief Get the configured Thread Network Name.
* @return Pointer to the network name string.
*/
const char* lasertag_get_thread_network_name(void);
/**
* @brief Get the configured Thread Channel.
* @return 8-bit channel (usually 11-26).
*/
uint8_t lasertag_get_thread_channel(void);
/**
* @brief Get the configured Thread Extended PAN ID.
* @return Pointer to the 8-byte extended PAN ID.
*/
const uint8_t* lasertag_get_thread_ext_pan_id(void);
/**
* @brief Get the configured Thread Network Key.
* @return Pointer to the 16-byte network key.
*/
const uint8_t* lasertag_get_thread_network_key(void);
/**
* @brief Set the device name.
* @param name Pointer to the name string.
@@ -58,39 +39,72 @@ const uint8_t* lasertag_get_thread_network_key(void);
int lasertag_set_device_name(const char *name, size_t len);
/**
* @brief Set the Thread PAN ID.
* @param pan_id 16-bit PAN ID.
* @return 0 on success, negative error code otherwise.
* @brief Get the device type (leader, weapon, vest).
* @return Integer representing the device type.
*/
int lasertag_set_thread_pan_id(uint16_t pan_id);
int lasertag_get_device_type(void);
/**
* @brief Set the Thread Network Name.
* @param name Pointer to the network name string.
* @param len Length of the name.
* @return 0 on success, negative error code otherwise.
* @brief Get the player ID.
* @return Integer representing the player ID.
*/
int lasertag_set_thread_network_name(const char *name, size_t len);
int lasertag_get_player_id(void);
/**
* @brief Set the Thread Channel.
* @param channel 8-bit channel (usually 11-26).
* @return 0 on success, negative error code otherwise.
* @brief Get the team ID.
* @return Integer representing the team ID.
*/
int lasertag_set_thread_channel(uint8_t channel);
int lasertag_get_team_id(void);
/**
* @brief Set the Thread Extended PAN ID.
* @param ext_id Pointer to the 8-byte extended PAN ID.
* @brief Initialize the watchdog timer for lasertag utilities.
* @return 0 on success, negative error code otherwise.
*/
int lasertag_set_thread_ext_pan_id(const uint8_t *ext_id);
int lasertag_init_watchdog(void);
/**
* @brief Set the Thread Network Key.
* @param key Pointer to the 16-byte network key.
* @return 0 on success, negative error code otherwise.
* @brief Feed the watchdog timer to prevent a system reset.
*/
int lasertag_set_thread_network_key(const uint8_t *key);
void lasertag_feed_watchdog(void);
#endif /* LASERTAG_UTILS_H */
/**
* @brief Calculate CRC8 checksum for the given data.
* @param data Pointer to the data buffer.
* @param len Length of the data buffer.
* @return Calculated CRC8 checksum.
*/
uint8_t lastertag_crc8 (const uint8_t *data, size_t len);
/**
* ANSI Defines for text formatting in logs.
*/
#define ANSI_RESET "\x1b[0m"
#define ANSI_BOLD "\x1b[1m"
#define ANSI_BRIGHT "\x1b[97m"
#define ANSI_GREEN "\x1b[32m"
#define ANSI_BRIGHT_GREEN "\x1b[92m"
#define ANSI_YELLOW "\x1b[33m"
#define ANSI_RED "\x1b[31m"
#define ANSI_BLUE "\x1b[34m"
#define FORMAT_BOLD(s) ANSI_BOLD s ANSI_RESET
#define FORMAT_BRIGHT(s) ANSI_BRIGHT s ANSI_RESET
#define FORMAT_BRIGHT_BOLD(s) ANSI_BRIGHT ANSI_BOLD s ANSI_RESET
#define FORMAT_GREEN(s) ANSI_GREEN s ANSI_RESET
#define FORMAT_GREEN_BOLD(s) ANSI_GREEN ANSI_BOLD s ANSI_RESET
#define FORMAT_BRIGHT_GREEN(s) ANSI_BRIGHT_GREEN s ANSI_RESET
#define FORMAT_BRIGHT_GREEN_BOLD(s) ANSI_BRIGHT_GREEN ANSI_BOLD s ANSI_RESET
#define FORMAT_YELLOW(s) ANSI_YELLOW s ANSI_RESET
#define FORMAT_YELLOW_BOLD(s) ANSI_YELLOW ANSI_BOLD s ANSI_RESET
#define FORMAT_RED(s) ANSI_RED s ANSI_RESET
#define FORMAT_RED_BOLD(s) ANSI_RED ANSI_BOLD s ANSI_RESET
#define FORMAT_BLUE(s) ANSI_BLUE s ANSI_RESET
#define FORMAT_BLUE_BOLD(s) ANSI_BLUE ANSI_BOLD s ANSI_RESET
#endif /* LASERTAG_UTILS_H */

View File

@@ -5,179 +5,152 @@
#include <zephyr/settings/settings.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <lasertag_utils.h>
#include <ble_mgmt.h>
#include <thread_mgmt.h>
#include <stdio.h>
LOG_MODULE_REGISTER(lasertag_utils, CONFIG_LASERTAG_UTILS_LOG_LEVEL);
LOG_MODULE_REGISTER(lt_utils, CONFIG_LASERTAG_UTILS_LOG_LEVEL);
static char device_name[32] = "UnknownDevice";
static uint16_t thread_pan_id = 0xabcd;
static char thread_network_name[17] = "OpenThread-nRF";
static uint8_t thread_channel = 15;
static uint8_t thread_ext_pan_id[8] = {0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe};
static uint8_t thread_network_key[16] = {
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff
};
static char device_name[32] = "Eriks Lasertag Device";
/* --- Settings Handler --- */
static int lasertag_settings_set(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg)
{
const char *next;
if (settings_name_steq(name, "name", &next) && !next) {
if (len > sizeof(device_name) - 1) return -EINVAL;
if (settings_name_steq(name, "name", &next) && !next)
{
if (len > sizeof(device_name) - 1)
return -EINVAL;
ssize_t rc = read_cb(cb_arg, device_name, len);
if (rc >= 0) { device_name[rc] = '\0'; return 0; }
}
if (settings_name_steq(name, "pan_id", &next) && !next) {
return read_cb(cb_arg, &thread_pan_id, sizeof(thread_pan_id)) >= 0 ? 0 : -EIO;
}
if (settings_name_steq(name, "net_name", &next) && !next) {
if (len > sizeof(thread_network_name) - 1) return -EINVAL;
ssize_t rc = read_cb(cb_arg, thread_network_name, len);
if (rc >= 0) { thread_network_name[rc] = '\0'; return 0; }
}
if (settings_name_steq(name, "channel", &next) && !next) {
return read_cb(cb_arg, &thread_channel, sizeof(thread_channel)) >= 0 ? 0 : -EIO;
}
if (settings_name_steq(name, "ext_pan_id", &next) && !next) {
return read_cb(cb_arg, thread_ext_pan_id, sizeof(thread_ext_pan_id)) >= 0 ? 0 : -EIO;
}
if (settings_name_steq(name, "net_key", &next) && !next) {
return read_cb(cb_arg, thread_network_key, sizeof(thread_network_key)) >= 0 ? 0 : -EIO;
if (rc >= 0)
{
device_name[rc] = '\0';
return 0;
}
}
return -ENOENT;
}
struct settings_handler lasertag_conf = { .name = "lasertag", .h_set = lasertag_settings_set };
struct settings_handler lasertag_conf = {.name = "lasertag", .h_set = lasertag_settings_set};
void lasertag_utils_init(void)
{
LOG_INF("==========================================");
LOG_INF("Lasertag System - Common Lib v0.0.1");
settings_subsys_init();
settings_register(&lasertag_conf);
settings_load();
LOG_INF("Device Name : %s", device_name);
LOG_INF("Thread PAN : 0x%04x", thread_pan_id);
LOG_INF("Thread Name : %s", thread_network_name);
LOG_INF("Thread Chan : %d", thread_channel);
LOG_INF("==========================================");
}
/* Getters */
const char* lasertag_get_device_name(void) { return device_name; }
uint16_t lasertag_get_thread_pan_id(void) { return thread_pan_id; }
const char* lasertag_get_thread_network_name(void) { return thread_network_name; }
uint8_t lasertag_get_thread_channel(void) { return thread_channel; }
const uint8_t* lasertag_get_thread_ext_pan_id(void) { return thread_ext_pan_id; }
const uint8_t* lasertag_get_thread_network_key(void) { return thread_network_key; }
const char *lasertag_get_device_name(void) { return device_name; }
/* Setters */
int lasertag_set_device_name(const char *name, size_t len) {
if (len >= sizeof(device_name)) len = sizeof(device_name) - 1;
int lasertag_set_device_name(const char *name, size_t len)
{
if (len >= sizeof(device_name))
len = sizeof(device_name) - 1;
memcpy(device_name, name, len);
device_name[len] = '\0';
return settings_save_one("lasertag/name", device_name, len);
}
int lasertag_set_thread_pan_id(uint16_t pan_id) {
thread_pan_id = pan_id;
return settings_save_one("lasertag/pan_id", &thread_pan_id, sizeof(thread_pan_id));
}
int lasertag_set_thread_network_name(const char *name, size_t len) {
if (len >= sizeof(thread_network_name)) len = sizeof(thread_network_name) - 1;
memcpy(thread_network_name, name, len);
thread_network_name[len] = '\0';
return settings_save_one("lasertag/net_name", thread_network_name, len);
}
int lasertag_set_thread_channel(uint8_t channel) {
thread_channel = channel;
return settings_save_one("lasertag/channel", &thread_channel, sizeof(thread_channel));
}
int lasertag_set_thread_ext_pan_id(const uint8_t *ext_id) {
memcpy(thread_ext_pan_id, ext_id, 8);
return settings_save_one("lasertag/ext_pan_id", thread_ext_pan_id, 8);
}
int lasertag_set_thread_network_key(const uint8_t *key) {
memcpy(thread_network_key, key, 16);
return settings_save_one("lasertag/net_key", thread_network_key, 16);
int lasertag_get_device_type(void)
{
#if IS_ENABLED(CONFIG_LASERTAG_ROLE_VEST)
return DEVICE_TYPE_VEST;
#elif IS_ENABLED(CONFIG_LASERTAG_ROLE_WEAPON)
return DEVICE_TYPE_WEAPON;
#elif IS_ENABLED(CONFIG_LASERTAG_ROLE_LEADER)
return DEVICE_TYPE_LEADER;
#else
return DEVICE_TYPE_NONE;
#endif
return 0;
}
/* --- Shell Commands --- */
/* --- Watchdog --- */
#ifdef CONFIG_WATCHDOG
#include <zephyr/drivers/watchdog.h>
#include <zephyr/logging/log_ctrl.h>
#if CONFIG_LASERTAG_SHELL
#define WDT_NODE DT_ALIAS(watchdog0)
static int lasertag_hex2bin(const char *hex, uint8_t *bin, size_t bin_len) {
for (size_t i = 0; i < bin_len; i++) {
char buf[3] = { hex[i*2], hex[i*2+1], '\0' };
bin[i] = (uint8_t)strtoul(buf, NULL, 16);
static int wdt_channel_id;
static const struct device *wdt = DEVICE_DT_GET(WDT_NODE);
int lasertag_init_watchdog(void)
{
struct wdt_timeout_cfg wdt_config = {
.window.min = 0U,
.window.max = CONFIG_LASERTAG_UTILS_WD_TIMEOUT,
.flags = WDT_FLAG_RESET_SOC,
};
if (!device_is_ready(wdt)) {
LOG_ERR("Watchdog device is not ready");
return -ENODEV;
}
wdt_channel_id = wdt_install_timeout(wdt, &wdt_config);
if (wdt_channel_id == -EBUSY) {
wdt_channel_id = 0;
LOG_WRN("Watchdog already running, using channel %d", wdt_channel_id);
return 0;
}
if (wdt_channel_id < 0) {
LOG_ERR("Failed to install watchdog timeout");
return wdt_channel_id;
}
int rc = wdt_setup(wdt, 0);
if (rc < 0) {
LOG_ERR("Failed to setup watchdog");
return rc;
}
LOG_DBG("Watchdog '%s' initialized channel %d with a %d ms timeout", wdt->name, wdt_channel_id, CONFIG_LASERTAG_UTILS_WD_TIMEOUT);
return 0;
}
static int cmd_reboot(const struct shell *sh, size_t argc, char **argv) {
shell_print(sh, "Rebooting...");
sys_reboot(SYS_REBOOT_COLD);
return 0;
void lasertag_feed_watchdog(void)
{
int rc = wdt_feed(wdt, wdt_channel_id);
if (rc < 0) {
LOG_ERR("Failed to feed watchdog, error code: %d", rc);
} else {
LOG_DBG("Watchdog '%s' fed successfully", wdt->name);
}
}
#endif /* CONFIG_WATCHDOG */
static int cmd_name_set(const struct shell *sh, size_t argc, char **argv) {
lasertag_set_device_name(argv[1], strlen(argv[1]));
shell_print(sh, "Name gespeichert.");
return 0;
}
static int cmd_thread_ping(const struct shell *sh, size_t argc, char **argv) {
char msg[64];
snprintf(msg, sizeof(msg), "Ping von %s", device_name);
shell_print(sh, "Sende Multicast-Ping an ff03::1...");
int rc = thread_mgmt_send_udp("ff03::1", (uint8_t*)msg, strlen(msg));
if (rc) shell_error(sh, "Ping fehlgeschlagen (%d)", rc);
return 0;
}
/* --- Utility Functions --- */
/* Subcommands definitions omitted for brevity, but they should include 'ping' */
static int cmd_thread_set_panid(const struct shell *sh, size_t argc, char **argv) {
uint16_t pan = (uint16_t)strtoul(argv[1], NULL, 0);
lasertag_set_thread_pan_id(pan);
return 0;
}
/* CRC8 CCITT polynome lookup table (polynome: 0x07, initial: 0x00) */
static const uint8_t crc8_table[256] = {
0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,
0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,
0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA,
0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,
0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A,
0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A,
0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,
0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4,
0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C, 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44,
0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,
0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63,
0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13,
0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3
};
static int cmd_thread_set_chan(const struct shell *sh, size_t argc, char **argv) {
uint8_t chan = (uint8_t)strtoul(argv[1], NULL, 10);
lasertag_set_thread_channel(chan);
return 0;
}
SHELL_STATIC_SUBCMD_SET_CREATE(sub_thread,
SHELL_CMD_ARG(panid, NULL, "Set PAN ID", cmd_thread_set_panid, 2, 0),
SHELL_CMD_ARG(chan, NULL, "Set channel", cmd_thread_set_chan, 2, 0),
SHELL_CMD(ping, NULL, "Send multicast ping", cmd_thread_ping),
SHELL_SUBCMD_SET_END
);
static int cmd_ble_start(const struct shell *sh, size_t argc, char **argv) {
return ble_mgmt_adv_start();
}
SHELL_STATIC_SUBCMD_SET_CREATE(sub_ble,
SHELL_CMD(start, NULL, "Start BLE", cmd_ble_start),
SHELL_SUBCMD_SET_END
);
SHELL_STATIC_SUBCMD_SET_CREATE(sub_lasertag,
SHELL_CMD_ARG(name, NULL, "Set name", cmd_name_set, 2, 0),
SHELL_CMD(thread, &sub_thread, "Thread configuration", NULL),
SHELL_CMD(ble, &sub_ble, "BLE Management", NULL),
SHELL_CMD(reboot, NULL, "Reboot", cmd_reboot),
SHELL_SUBCMD_SET_END
);
SHELL_CMD_REGISTER(lasertag, &sub_lasertag, "Lasertag Befehle", NULL);
#endif
uint8_t lastertag_crc8(const uint8_t *data, size_t len)
{
uint8_t crc = 0x00;
for (size_t i = 0; i < len; i++) {
crc = crc8_table[crc ^ data[i]];
}
return crc;
}

View File

@@ -1,5 +1,5 @@
if(CONFIG_THREAD_MGMT)
zephyr_library()
zephyr_sources(src/thread_mgmt.c)
zephyr_library_sources(src/thread_mgmt.c)
zephyr_include_directories(include)
endif()

View File

@@ -1,14 +1,31 @@
menuconfig THREAD_MGMT
bool "Thread Management"
depends on OPENTHREAD
depends on BLE_MGMT
select LASERTAG_UTILS
select NETWORKING
select NET_L2_OPENTHREAD
select OPENTHREAD
select OPENTHREAD_TIME_SYNC
help
Library for initializing and managing the OpenThread stack.
if THREAD_MGMT
config THREAD_MGMT_LOG_LEVEL
int "Thread Management Log Level"
default 3
# Logging configuration for the Thread Management module
module = THREAD_MGMT
module-str = thread_mgmt
source "subsys/logging/Kconfig.template.log_config"
config THREAD_MGMT_SHELL
bool "Enable shell commands for Thread management"
select SHELL
select OPENTHREAD_SHELL
default n
help
Set the verbosity of the thread management library.
endif
Enable shell commands for managing the Thread network, such as starting/stopping the Thread interface, checking status, etc.
# Openthread configuration options
config OPENTHREAD_DEFAULT_TX_POWER
default 8
range -40 8
help
Default transmit power for OpenThread. The value is in dBm and can be set according to the requirements of your application and regulatory limits.
endif # THREAD_MGMT

View File

@@ -1,26 +1,65 @@
#ifndef THREAD_MGMT_H
#define THREAD_MGMT_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <ble_mgmt.h>
/**
* @brief Initializes the OpenThread stack, UDP and CoAP.
* @brief Initialize and start the OpenThread stack.
*/
int thread_mgmt_init(void);
/**
* @brief Sends a UDP message.
* @brief Restart the Thread stack if dataset fields changed.
* @param pending_config New configuration payload to compare and apply.
* @param force If true, force a full restart even when data is unchanged.
*/
int thread_mgmt_send_udp(const char *addr_str, uint8_t *payload, uint16_t len);
void thread_mgmt_restart_thread_stack(device_config_payload_t *pending_config, bool force);
/**
* @brief Starts device discovery via CoAP Multicast.
* @brief Get the current Thread PAN ID from the active dataset.
* @return PAN ID, or 0 if unavailable.
*/
int thread_mgmt_discover_nodes(void);
uint16_t thread_mgmt_get_pan_id(void);
/**
* @brief Returns the list of discovered node names (comma-separated).
* @brief Get the current Thread channel from the active dataset.
* @return Channel number, or 0 if unavailable.
*/
const char* thread_mgmt_get_discovered_list(void);
uint8_t thread_mgmt_get_channel(void);
/**
* @brief Copy the Extended PAN ID from the active dataset.
* @param dest_8byte Destination buffer with at least 8 bytes.
*/
void thread_mgmt_get_ext_pan_id(uint8_t *dest_8byte);
/**
* @brief Copy the Thread network key from the active dataset.
* @param dest_16byte Destination buffer with at least 16 bytes.
*/
void thread_mgmt_get_network_key(uint8_t *dest_16byte);
/**
* @brief Copy the Thread network name from the active dataset.
* @param dest_str Destination string buffer.
* @param max_len Destination buffer size in bytes.
*/
void thread_mgmt_get_network_name(char *dest_str, size_t max_len);
/**
* @brief Callback type for scheduled network-timed events.
*/
typedef void (*thread_mgmt_timeout_cb_t)(void);
/**
* @brief Return the current Thread network time in microseconds.
* This is the time since the Thread network was formed or last reset, and is
* synchronized across the network.
* @return 64-bit network time, or 0 if the stack is not ready.
*/
uint64_t thread_mgmt_get_network_time(void);
#endif

View File

@@ -1,216 +1,360 @@
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/net/openthread.h>
#include <zephyr/random/random.h>
#include <openthread/thread.h>
#include <openthread/dataset.h>
#include <openthread/instance.h>
#include <openthread/udp.h>
#include <openthread/coap.h>
#include <openthread/ip6.h>
#include <lasertag_utils.h>
#include <thread_mgmt.h>
#include <openthread/network_time.h>
#include <string.h>
#include <stdio.h>
#include <lasertag_utils.h>
#include <thread_mgmt.h>
#include <ble_mgmt.h>
LOG_MODULE_REGISTER(thread_mgmt, CONFIG_THREAD_MGMT_LOG_LEVEL);
#define UDP_PORT 1234
#define MAX_DISCOVERED_NODES 10
#if IS_ENABLED(CONFIG_THREAD_MGMT_SHELL)
void init_custom_ot_commands(void);
#endif
static otUdpSocket s_udp_socket;
static char discovered_nodes_list[256] = "";
static uint8_t node_count = 0;
/* --- CoAP Server Logik --- */
static void coap_id_handler(void *context, otMessage *message, const otMessageInfo *message_info)
#if (CONFIG_THREAD_MGMT_LOG_LEVEL >= LOG_LEVEL_DBG)
static void thread_mgmt_state_notify_callback(otChangedFlags aFlags, void *aContext)
{
otError error = OT_ERROR_NONE;
otMessage *response;
otInstance *instance = (otInstance *)context;
if (otCoapMessageGetCode(message) != OT_COAP_CODE_GET) {
return;
}
LOG_INF("CoAP GET /id received");
response = otCoapNewMessage(instance, NULL);
if (response == NULL) return;
otCoapMessageInitResponse(response, message, OT_COAP_TYPE_ACKNOWLEDGMENT, OT_COAP_CODE_CONTENT);
otCoapMessageSetPayloadMarker(response);
const char *name = lasertag_get_device_name();
error = otMessageAppend(response, name, strlen(name));
if (error != OT_ERROR_NONE) {
otMessageFree(response);
return;
}
error = otCoapSendResponse(instance, response, message_info);
if (error != OT_ERROR_NONE) {
otMessageFree(response);
ARG_UNUSED(aContext);
if (aFlags & OT_CHANGED_THREAD_ROLE)
{
struct otInstance *instance = openthread_get_default_instance();
otDeviceRole role = otThreadGetDeviceRole(instance);
LOG_DBG("Thread Role changed: " BOLD("%s"), otThreadDeviceRoleToString(role));
}
}
static otCoapResource s_id_resource = {
.mUriPath = "id",
.mHandler = coap_id_handler,
.mContext = NULL,
.mNext = NULL
struct openthread_state_changed_callback thread_state_callback = {
.otCallback = thread_mgmt_state_notify_callback,
.user_data = NULL,
};
/* --- CoAP Discovery Client Logik --- */
static void coap_discover_res_handler(void *context, otMessage *message, const otMessageInfo *message_info, otError result)
void thread_mgmt_print_dataset(const otOperationalDataset *dataset)
{
if (result != OT_ERROR_NONE || otCoapMessageGetCode(message) != OT_COAP_CODE_CONTENT) {
if (dataset == NULL)
{
return;
}
char name_buf[32];
uint16_t length = otMessageGetLength(message) - otMessageGetOffset(message);
if (length > sizeof(name_buf) - 1) length = sizeof(name_buf) - 1;
// Network Name
char net_name[OT_NETWORK_NAME_MAX_SIZE + 1] = {0};
memcpy(net_name, dataset->mNetworkName.m8, sizeof(dataset->mNetworkName.m8));
otMessageRead(message, otMessageGetOffset(message), name_buf, length);
name_buf[length] = '\0';
// Convert to hex strings using bin2hex
char ext_pan_id_str[17];
bin2hex(dataset->mExtendedPanId.m8, 8, ext_pan_id_str, sizeof(ext_pan_id_str));
char addr_str[OT_IP6_ADDRESS_STRING_SIZE];
otIp6AddressToString(&message_info->mPeerAddr, addr_str, sizeof(addr_str));
LOG_INF("Node discovered: %s (%s)", name_buf, addr_str);
/* Add to list (simple CSV format for BLE) */
if (node_count < MAX_DISCOVERED_NODES && !strstr(discovered_nodes_list, name_buf)) {
if (node_count > 0) strcat(discovered_nodes_list, ",");
strcat(discovered_nodes_list, name_buf);
node_count++;
}
}
int thread_mgmt_discover_nodes(void)
{
otInstance *instance = openthread_get_default_instance();
otMessage *message;
otMessageInfo message_info;
otError error = OT_ERROR_NONE;
/* Reset list */
discovered_nodes_list[0] = '\0';
node_count = 0;
message = otCoapNewMessage(instance, NULL);
if (message == NULL) return -ENOMEM;
otCoapMessageInit(message, OT_COAP_TYPE_NON_CONFIRMABLE, OT_COAP_CODE_GET);
otCoapMessageAppendUriPathOptions(message, "id");
memset(&message_info, 0, sizeof(message_info));
otIp6AddressFromString("ff03::1", &message_info.mPeerAddr);
message_info.mPeerPort = OT_DEFAULT_COAP_PORT;
error = otCoapSendRequest(instance, message, &message_info, coap_discover_res_handler, NULL);
if (error != OT_ERROR_NONE) {
otMessageFree(message);
return -EIO;
}
LOG_INF("Discovery started...");
return 0;
}
const char* thread_mgmt_get_discovered_list(void)
{
return discovered_nodes_list;
}
/* --- UDP & Init --- */
static void udp_receive_cb(void *context, otMessage *message, const otMessageInfo *message_info)
{
uint8_t buf[64];
uint16_t length = otMessageGetLength(message) - otMessageGetOffset(message);
if (length > sizeof(buf) - 1) length = sizeof(buf) - 1;
int read = otMessageRead(message, otMessageGetOffset(message), buf, length);
buf[read] = '\0';
char addr_str[OT_IP6_ADDRESS_STRING_SIZE];
otIp6AddressToString(&message_info->mPeerAddr, addr_str, sizeof(addr_str));
LOG_INF("------------------------------------------");
LOG_INF("UDP DATA RECEIVED!");
LOG_INF("From: [%s]", addr_str);
LOG_INF("Payload: %s", buf);
LOG_INF("------------------------------------------");
}
int thread_mgmt_send_udp(const char *addr_str, uint8_t *payload, uint16_t len)
{
struct otInstance *instance = openthread_get_default_instance();
otMessage *message;
otMessageInfo message_info;
if (!instance) return -ENODEV;
memset(&message_info, 0, sizeof(message_info));
otIp6AddressFromString(addr_str, &message_info.mPeerAddr);
message_info.mPeerPort = UDP_PORT;
message = otUdpNewMessage(instance, NULL);
if (message == NULL) return -ENOMEM;
otMessageAppend(message, payload, len);
otUdpSend(instance, &s_udp_socket, message, &message_info);
LOG_INF("UDP sent to %s", addr_str);
return 0;
char net_key_str[33];
bin2hex(dataset->mNetworkKey.m8, 16, net_key_str, sizeof(net_key_str));
LOG_DBG(" Timestamp: " BOLD("%llu"), dataset->mActiveTimestamp.mSeconds);
LOG_DBG(" Network Name: " BOLD("%s"), net_name);
LOG_DBG(" PAN ID: " BOLD("0x%04X"), dataset->mPanId);
LOG_DBG(" Channel: " BOLD("%u"), dataset->mChannel);
LOG_DBG(" Extended PAN ID: " BOLD("%s"), ext_pan_id_str);
LOG_DBG(" Network Key: " BOLD("%s"), net_key_str);
}
#endif
int thread_mgmt_init(void)
{
struct otInstance *instance = openthread_get_default_instance();
otOperationalDataset dataset;
if (!instance) return -ENODEV;
if (!instance)
return -ENODEV;
#if (CONFIG_THREAD_MGMT_LOG_LEVEL >= LOG_LEVEL_DBG)
openthread_state_changed_callback_register(&thread_state_callback);
#endif
/* Dataset Setup */
memset(&dataset, 0, sizeof(otOperationalDataset));
dataset.mActiveTimestamp.mSeconds = 1;
dataset.mComponents.mIsActiveTimestampPresent = true;
const char *net_name = lasertag_get_thread_network_name();
memcpy(dataset.mNetworkName.m8, net_name, strlen(net_name));
dataset.mComponents.mIsNetworkNamePresent = true;
dataset.mPanId = lasertag_get_thread_pan_id();
dataset.mComponents.mIsPanIdPresent = true;
dataset.mChannel = lasertag_get_thread_channel();
dataset.mComponents.mIsChannelPresent = true;
memcpy(dataset.mExtendedPanId.m8, lasertag_get_thread_ext_pan_id(), 8);
dataset.mComponents.mIsExtendedPanIdPresent = true;
memcpy(dataset.mNetworkKey.m8, lasertag_get_thread_network_key(), 16);
dataset.mComponents.mIsNetworkKeyPresent = true;
uint8_t ml_prefix[] = {0xfd, 0xde, 0xad, 0x00, 0xbe, 0xef, 0x00, 0x00};
memcpy(dataset.mMeshLocalPrefix.m8, ml_prefix, 8);
dataset.mComponents.mIsMeshLocalPrefixPresent = true;
if (otDatasetGetActive(instance, &dataset))
{
// No dataset found, proceed to set up a new one
LOG_INF("No active Thread dataset found, initializing new dataset.");
dataset.mActiveTimestamp.mSeconds = 1;
dataset.mComponents.mIsActiveTimestampPresent = true;
uint8_t network_name[] = "Eriks Lasertag\0";
strncpy(dataset.mNetworkName.m8, network_name, strlen(network_name));
dataset.mComponents.mIsNetworkNamePresent = true;
dataset.mPanId = 0xdead;
dataset.mComponents.mIsPanIdPresent = true;
dataset.mChannel = 15;
dataset.mComponents.mIsChannelPresent = true;
uint8_t ext_pan_id[8] = {0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe}; // Example Extended PAN ID
// sys_csrand_get(ext_pan_id, sizeof(ext_pan_id));
memcpy(dataset.mExtendedPanId.m8, ext_pan_id, 8);
dataset.mComponents.mIsExtendedPanIdPresent = true;
uint8_t network_key[16] = {0x00}; // Initialize with zeros
// sys_csrand_get(network_key, sizeof(network_key));
memcpy(dataset.mNetworkKey.m8, network_key, 16);
dataset.mComponents.mIsNetworkKeyPresent = true;
memset(dataset.mMeshLocalPrefix.m8, 0, 8);
uint8_t mesh_local_prefix[8] = {0xfd, 0x00, 0x03, 0x08, 0x20, 0x13, 0x00, 0x00};
memcpy(dataset.mMeshLocalPrefix.m8, mesh_local_prefix, 8);
dataset.mComponents.mIsMeshLocalPrefixPresent = true;
otDatasetSetActive(instance, &dataset);
}
otDatasetSetActive(instance, &dataset);
otIp6SetEnabled(instance, true);
otThreadSetEnabled(instance, true);
/* UDP Initialisierung */
otSockAddr listen_addr;
memset(&listen_addr, 0, sizeof(listen_addr));
listen_addr.mPort = UDP_PORT;
otUdpOpen(instance, &s_udp_socket, udp_receive_cb, NULL);
otUdpBind(instance, &s_udp_socket, &listen_addr, OT_NETIF_UNSPECIFIED);
#if (CONFIG_THREAD_MGMT_LOG_LEVEL >= LOG_LEVEL_DBG)
memset(&dataset, 0, sizeof(otOperationalDataset));
otDatasetGetActive(instance, &dataset);
LOG_DBG("Thread stack dataset after Thread stack initialization:");
thread_mgmt_print_dataset(&dataset);
#endif
/* CoAP Initialisierung */
otCoapStart(instance, OT_DEFAULT_COAP_PORT);
s_id_resource.mContext = instance;
otCoapAddResource(instance, &s_id_resource);
LOG_INF("Thread MGMT: Initialized, UDP %d & CoAP %d open.", UDP_PORT, OT_DEFAULT_COAP_PORT);
#if IS_ENABLED(CONFIG_THREAD_MGMT_SHELL)
init_custom_ot_commands();
#endif
return 0;
}
}
void thread_mgmt_restart_thread_stack(device_config_payload_t *pending_config, bool force)
{
struct otInstance *instance = openthread_get_default_instance();
otOperationalDataset dataset;
bool changed = false;
memset(&dataset, 0, sizeof(otOperationalDataset));
// Get current active dataset. Force restart if unable to get it.
if (otDatasetGetActive(instance, &dataset) != OT_ERROR_NONE)
{
LOG_WRN("Failed to get active dataset, forcing Thread stack restart.");
force = true;
}
// Compare each relevant field and set 'changed' if any differ
if (force || dataset.mChannel != pending_config->channel)
{
LOG_DBG("Thread channel change detected: %u -> %u", dataset.mChannel, pending_config->channel);
dataset.mChannel = pending_config->channel;
dataset.mComponents.mIsChannelPresent = true;
changed = true;
}
if (force || dataset.mPanId != pending_config->pan_id)
{
LOG_DBG("Thread PAN ID change detected: 0x%04X -> 0x%04X", dataset.mPanId, pending_config->pan_id);
dataset.mPanId = pending_config->pan_id;
dataset.mComponents.mIsPanIdPresent = true;
changed = true;
}
if (force || memcmp(dataset.mExtendedPanId.m8, pending_config->ext_pan_id, 8) != 0)
{
LOG_DBG("Thread extended PAN ID change detected.");
memcpy(dataset.mExtendedPanId.m8, pending_config->ext_pan_id, 8);
dataset.mComponents.mIsExtendedPanIdPresent = true;
changed = true;
}
if (force || memcmp(dataset.mNetworkKey.m8, pending_config->network_key, 16) != 0)
{
LOG_DBG("Thread network key change detected.");
memcpy(dataset.mNetworkKey.m8, pending_config->network_key, 16);
dataset.mComponents.mIsNetworkKeyPresent = true;
changed = true;
}
if (force || strncmp(dataset.mNetworkName.m8, pending_config->network_name, 16) != 0)
{
LOG_DBG("Thread network name change detected: %s -> %s", dataset.mNetworkName.m8, pending_config->network_name);
strncpy(dataset.mNetworkName.m8, pending_config->network_name, 16);
dataset.mComponents.mIsNetworkNamePresent = true;
changed = true;
}
if (changed)
{
LOG_DBG("Thread stack restart required; dataset changed.");
dataset.mActiveTimestamp.mSeconds++;
dataset.mComponents.mIsActiveTimestampPresent = true;
otThreadSetEnabled(instance, false);
otIp6SetEnabled(instance, false);
// Set the network key BEFORE updating the dataset
// This ensures OpenThread's crypto layer has the correct key before we update the operational dataset
otNetworkKey stored_network_key;
if (dataset.mComponents.mIsNetworkKeyPresent)
{
otError key_err = otThreadSetNetworkKey(instance, &dataset.mNetworkKey);
if (key_err != OT_ERROR_NONE)
{
LOG_ERR("Error setting network key: %d", key_err);
}
// Read the key back from the thread stack to ensure it's properly stored
otThreadGetNetworkKey(instance, &stored_network_key);
memcpy(dataset.mNetworkKey.m8, stored_network_key.m8, 16);
// Keep the flag set so the key is included in the persistent dataset
}
otError err = otDatasetSetActive(instance, &dataset);
if (err != OT_ERROR_NONE)
{
LOG_ERR("Error writing dataset: %d", err);
}
otIp6SetEnabled(instance, true);
otThreadSetEnabled(instance, true);
#if (CONFIG_THREAD_MGMT_LOG_LEVEL >= LOG_LEVEL_DBG)
otOperationalDataset new_active_dataset;
memset(&new_active_dataset, 0, sizeof(otOperationalDataset));
if (otDatasetGetActive(instance, &new_active_dataset) == OT_ERROR_NONE)
{
LOG_DBG("Thread stack dataset after Thread stack update:");
thread_mgmt_print_dataset(&new_active_dataset);
}
#endif
LOG_DBG("Thread stack restarted successfully.");
}
else
{
LOG_DBG("Thread stack restart not required; dataset unchanged.");
}
}
uint16_t thread_mgmt_get_pan_id(void)
{
struct otInstance *instance = openthread_get_default_instance();
otOperationalDataset dataset;
if (otDatasetGetActive(instance, &dataset) == OT_ERROR_NONE)
{
return dataset.mPanId;
}
return 0;
}
uint8_t thread_mgmt_get_channel(void)
{
struct otInstance *instance = openthread_get_default_instance();
otOperationalDataset dataset;
if (otDatasetGetActive(instance, &dataset) == OT_ERROR_NONE)
{
return dataset.mChannel;
}
return 0;
}
void thread_mgmt_get_ext_pan_id(uint8_t *dest_8byte)
{
struct otInstance *instance = openthread_get_default_instance();
otOperationalDataset dataset;
if (otDatasetGetActive(instance, &dataset) == OT_ERROR_NONE)
{
memcpy(dest_8byte, dataset.mExtendedPanId.m8, 8);
}
}
void thread_mgmt_get_network_key(uint8_t *dest_16byte)
{
struct otInstance *instance = openthread_get_default_instance();
otOperationalDataset dataset;
if (otDatasetGetActive(instance, &dataset) == OT_ERROR_NONE)
{
memcpy(dest_16byte, dataset.mNetworkKey.m8, 16);
}
}
void thread_mgmt_get_network_name(char *dest_str, size_t max_len)
{
struct otInstance *instance = openthread_get_default_instance();
otOperationalDataset dataset;
if (otDatasetGetActive(instance, &dataset) == OT_ERROR_NONE)
{
// Safe copy with forced null-termination
snprintf(dest_str, max_len, "%s", dataset.mNetworkName.m8);
}
}
uint64_t thread_mgmt_get_network_time(void)
{
struct otInstance *instance = openthread_get_default_instance();
uint64_t networkTime = 0;
if (!instance)
{
return 0;
}
otNetworkTimeStatus status = otNetworkTimeGet(instance, &networkTime);
if (status == OT_NETWORK_TIME_SYNCHRONIZED || status == OT_NETWORK_TIME_RESYNC_NEEDED)
{
return networkTime;
}
return 0;
}
/* --- Shell Commands --- */
#if IS_ENABLED(CONFIG_THREAD_MGMT_SHELL)
#include <zephyr/shell/shell.h>
#include <openthread/cli.h>
otError ot_time_handler(void *aContext, uint8_t aArgsLength, char **aArgs)
{
ARG_UNUSED(aContext);
ARG_UNUSED(aArgsLength);
ARG_UNUSED(aArgs);
uint64_t networkTime;
uint16_t syncPeriod;
otNetworkTimeStatus status;
otInstance *instance = openthread_get_default_instance();
status = otNetworkTimeGet(instance, &networkTime);
syncPeriod = otNetworkTimeGetSyncPeriod(instance);
switch (status)
{
case OT_NETWORK_TIME_SYNCHRONIZED:
otCliOutputFormat("Synchronized: %llu us, Sync Period: %u ms\n", networkTime, syncPeriod);
break;
case OT_NETWORK_TIME_RESYNC_NEEDED:
otCliOutputFormat("Resync needed. Last time: %llu us, Sync Period: %u ms\n", networkTime, syncPeriod);
break;
case OT_NETWORK_TIME_UNSYNCHRONIZED:
otCliOutputFormat(FORMAT_RED_BOLD("Error: Network time not synchronized. Sync Period: %u ms\n"), syncPeriod);
break;
default:
otCliOutputFormat(FORMAT_RED_BOLD("Error: Unknown network time status. Sync Period: %u ms\n"), syncPeriod);
break;
}
return OT_ERROR_NONE;
}
static const otCliCommand ot_commands[] = {
{"time", ot_time_handler},
};
void init_custom_ot_commands(void)
{
struct otInstance *instance = openthread_get_default_instance();
if (instance)
{
otCliSetUserCommands(ot_commands, ARRAY_SIZE(ot_commands), instance);
}
}
#endif // CONFIG_THREAD_MGMT_SHELL

1
firmware/tools/audio/.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
samples/**/* filter=lfs diff=lfs merge=lfs -text

2
firmware/tools/audio/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
cache
previews

View File

@@ -0,0 +1,214 @@
import os
import yaml
import hashlib
import shutil
import subprocess
from pathlib import Path
from kokoro import KPipeline
import soundfile as sf
class AudioBuilder:
def __init__(self):
# 1. Defaults
self.config = {
'paths': {
'cache': Path("./cache"),
'output': Path("./lfs_source"),
'preview': Path("./previews")
},
'settings': { 'sample_rate': 16000 }
}
# 2. Global YAML laden (überschreibt Defaults)
self.load_global_config()
# Pfade sicherstellen
for p in self.config['paths'].values():
Path(p).mkdir(parents=True, exist_ok=True)
self.pipeline = KPipeline(lang_code='a')
def load_global_config(self):
if Path("global.yaml").exists():
with open("global.yaml", "r") as f:
g_cfg = yaml.safe_load(f)
# Pfade überschreiben und sofort in Path-Objekte wandeln
if 'paths' in g_cfg:
for key, value in g_cfg['paths'].items():
self.config['paths'][key] = Path(value)
# Settings (wie Sample Rate) überschreiben
if 'settings' in g_cfg:
self.config['settings'].update(g_cfg['settings'])
# Sicherstellen, dass der Standard-Sample-Pfad existiert, falls nicht in global.yaml
if 'samples' not in self.config['paths']:
self.config['paths']['samples'] = Path("./samples")
def get_hash(self, text, voice, filters, sample_rate):
"""Erzeugt einen MD5-Hash über alle Parameter, die das Audio-Ergebnis beeinflussen."""
# Wir kombinieren alle Parameter zu einem String
data = f"{text}{voice}{''.join(filters)}{sample_rate}"
return hashlib.md5(data.encode()).hexdigest()
def run_ffmpeg(self, cmd):
subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
def process_asset(self, asset, default_voice, global_filters):
# 1. Parameter vorbereiten
asset_id = asset['id']
sr_val = self.config['settings']['sample_rate']
sr_str = str(sr_val)
filters = asset.get('filters', global_filters)
# Weiche: Ist es ein lokales Sample oder KI-Sprache?
is_sample = asset.get('type') == 'sample'
if is_sample:
# Pfad aus global.yaml nutzen
source_file = self.config['paths']['samples'] / asset['source']
# Hash basiert auf Dateiname + Filter + SR
h = self.get_hash(asset['source'], "LOCAL_SAMPLE", filters, sr_str)
input_for_ffmpeg = str(source_file)
else:
# Klassische KI-Sprache
voice = asset.get('voice', default_voice)
text = asset['text']
h = self.get_hash(text, voice, filters, sr_str)
# Input wird später das temp_wav sein
# Pfade definieren
cache_file = self.config['paths']['cache'] / h
preview_file = self.config['paths']['preview'] / f"{asset_id}.wav"
if not cache_file.exists():
print(f" [GEN] {asset_id}...")
# 2. Input-Vorbereitung
if not is_sample:
# KI-Sprache: Temp WAV erzeugen
temp_wav = self.config['paths']['cache'] / f"temp_{h}.wav"
generator = self.pipeline(text, voice=voice, speed=1.0)
for _, _, audio in generator:
sf.write(temp_wav, audio, 24000)
break
current_input = str(temp_wav)
else:
current_input = input_for_ffmpeg
# 3. Filter-String
f_str = ",".join(filters)
# 4. RAW-Export für nRF
self.run_ffmpeg([
'ffmpeg', '-y', '-i', current_input,
'-af', f_str,
'-ar', sr_str, '-ac', '1',
'-f', 's16le', '-acodec', 'pcm_s16le',
str(cache_file)
])
# 5. Preview-WAV
self.run_ffmpeg([
'ffmpeg', '-y', '-i', current_input,
'-af', f_str,
'-ac', '1',
str(preview_file)
])
# Aufräumen (nur wenn es ein KI-Temp-File war)
if not is_sample and Path(current_input).exists():
Path(current_input).unlink()
return cache_file
def generate_countdown(self, config):
"""Erzeugt einen zusammenhängenden 10-Sekunden-Countdown."""
c_id = config['id']
voice = config['voice']
filters = config.get('filters', [])
sr_str = str(self.config['settings']['sample_rate'])
# Eigener Hash für das gesamte Countdown-Objekt
h = self.get_hash(f"COUNTDOWN_LOGIC_V2_{c_id}", voice, filters, sr_str)
final_cache_file = self.config['paths']['cache'] / f"final_{h}"
if not final_cache_file.exists():
print(f" [GEN] Spezial-Asset: {c_id} (10 bis 1)")
# Die Texte für die Zahlen
numbers = [
"TEN!", "NINE!", "EIGHT!", "SEVEN!", "SIX!",
"FIVE!", "FOUR!", "THREE!", "TWO!!", "ONE!!!"
]
parts = []
for i, txt in enumerate(numbers):
part_id = f"cnt_tmp_{i}"
temp_asset = {
'id': part_id,
'text': txt
}
part_file = self.process_asset(temp_asset, voice, filters)
parts.append(part_file)
with open(final_cache_file, 'wb') as outfile:
for p_file in parts:
with open(p_file, 'rb') as infile:
outfile.write(infile.read())
print(f" [DONE] Countdown-Kette generiert: {final_cache_file.name}")
return final_cache_file
def build_target(self, target_name):
print(f"🚀 Baue Assets für Target: {target_name.upper()}")
out_dir = self.config['paths']['output']
out_dir.mkdir(parents=True, exist_ok=True)
countdown_file = Path("countdown.yaml")
if countdown_file.exists():
with open(countdown_file, "r") as f:
cnt_config = yaml.safe_load(f)
# Prüfen, ob der Countdown für das aktuelle Target gebaut werden soll
if target_name in cnt_config.get('targets', []):
# Aufruf der generierenden Methode
countdown_cache = self.generate_countdown(cnt_config)
# Kopieren ins lfs_source Verzeichnis unter dem Namen der ID ("countdown")
dest_file = out_dir / cnt_config['id']
shutil.copy(countdown_cache, dest_file)
for cfg_file in Path(".").glob("*.yaml"):
if cfg_file.name in ["global.yaml", "countdown.yaml"]:
continue
with open(cfg_file, "r") as f:
config = yaml.safe_load(f)
# Falls das YAML eine Liste ist (z.B. [- id: ...])
if isinstance(config, list):
assets = config
voice = None
global_filters = []
else:
# Falls es ein Dictionary ist (z.B. voice: ... assets: ...)
assets = config.get('assets', [])
voice = config.get('voice')
global_filters = config.get('filters', [])
for asset in assets:
if target_name in asset.get('targets', []):
source_cache = self.process_asset(asset, voice, global_filters)
if source_cache:
dest_file = out_dir / asset['id']
shutil.copy(source_cache, dest_file)
if __name__ == "__main__":
import sys
target = sys.argv[1] if len(sys.argv) > 1 else "vest"
AudioBuilder().build_target(target)

View File

@@ -0,0 +1,14 @@
id: "countdown"
voice: "am_michael"
targets: ["vest", "base"]
# Die Filter hier werden auf jede einzelne Zahl angewendet
filters:
- "lowshelf=f=100:g=20:enable='lt(t,0.1)'"
- "asetrate=24000*0.85"
- "atempo=1.17"
- "acontrast=80"
- "aecho=0.8:0.88:60:0.4"
- "atrim=end=1"
- "apad=whole_dur=1"
- "loudnorm=I=-12:TP=-1.0"

View File

@@ -0,0 +1,7 @@
paths:
cache: "./cache"
output: "../littlefs_generator/source_folder/a"
preview: "./previews"
samples: "./samples"
settings:
sample_rate: 16000

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,5 @@
kokoro>=0.1.0 # Die TTS-Pipeline
soundfile # Zum Schreiben der WAV-Dateien (sf.write)
numpy # Basis für Audio-Daten-Arrays
torch # Backend für Kokoro (KI-Modell)
PyYAML # Für deine voice_*.yaml Konfigurationsdateien

Binary file not shown.

View File

@@ -0,0 +1,11 @@
- id: "siren"
type: "sample" # Neu: Unterscheidung zwischen TTS und Datei
source: "horn.ogg" # Die Datei in deinem samples/ Ordner
targets: ["vest", "base"]
filters:
- "atrim=start=0.15"
- "highpass=f=100"
- "lowpass=f=6000"
- "acompressor=threshold=-8dB:ratio=20:attack=1:release=30" # Maximale "Druckluft"
- "amix=inputs=1:weights=1.5" # Sättigung
- "loudnorm=I=-12:TP=-1.0"

View File

@@ -0,0 +1,17 @@
voice: "am_michael"
filters:
- "asetrate=24000*0.85"
- "atempo=1.17"
- "acompressor=threshold=-20dB:ratio=4:attack=5:release=50"
- "highpass=f=150"
- "lowpass=f=4000"
- "loudnorm=I=-14:TP=-1.5"
assets:
- id: "g1"
text: "Welcome to the Arena!"
targets: ["vest", "weapon"]
- id: "dead"
text: "YOU ARE DEAD!"
targets: ["vest"]

View File

@@ -0,0 +1,12 @@
voice: "af_bella"
filters:
- "highpass=f=200"
- "lowpass=f=4500"
- "compand=0.3|0.3:1|1:-90/-60|-60/-40|-40/-30|-20/-20:6:0:-90:0.2"
- "loudnorm=I=-14:TP=-1.5"
assets:
- id: "s1"
text: "Network parameters deployed. Connecting to the game leader..."
targets: ["vest", "weapon"]

Some files were not shown because too many files have changed in this diff Show More