Compare commits
54 Commits
9f25c0540a
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a589e104c | |||
| 07127fb074 | |||
| 17cc33332d | |||
| 5406c9917f | |||
| 37d1bd6db1 | |||
| 8c15260166 | |||
| 6b36435759 | |||
| 675a99930a | |||
| f012ce9fe0 | |||
| 2a4d007805 | |||
| 93b7c5fe9e | |||
| 1ce021c76f | |||
| 480baa97fa | |||
| 8d97a5d33f | |||
| f5b1849ada | |||
| 0113edb816 | |||
| a1b047f4bb | |||
| d51ff27e26 | |||
| ac477e290e | |||
| 4906dc31eb | |||
| be48e8ada7 | |||
| 918092cd9f | |||
| b1f5578be9 | |||
| 9c1d19af67 | |||
| 6376185622 | |||
| 3febb6411e | |||
| 0c3a8bfa39 | |||
| 8305adf917 | |||
| fae79ad8b0 | |||
| 794d8e36c9 | |||
| 0785d9a755 | |||
| 11ad746f04 | |||
| 149a142c22 | |||
| e84efc2e8c | |||
| 76ac36a59b | |||
| 5e7a817e03 | |||
| 1b8d3e17b8 | |||
| 93a7da7855 | |||
| fb4578ac51 | |||
| 63f8f2aaac | |||
| 55b5421671 | |||
| bd0a8cce8d | |||
| 832a60d044 | |||
| a041d5a49c | |||
| 38396738a6 | |||
| 65688b7b99 | |||
| 395d577b78 | |||
| c13b6d73c9 | |||
| 6b1bbca992 | |||
| 9d5dad0e8d | |||
| a6bedb6b79 | |||
| e460aac7a1 | |||
| 5113c0a850 | |||
| 4e6780af6d |
@@ -9,14 +9,20 @@ Der grundsätzliche Ablauf sieht so aus:
|
||||
```mermaid
|
||||
flowchart TD
|
||||
start([App-Start])
|
||||
choose_leader[1. Leader bestimmen]
|
||||
choose_mode{Modus wählen?}
|
||||
choose_leader[Weg A: Leader (Spiel)]
|
||||
maintenance[Weg B: Ausrüstung warten]
|
||||
check_state{Leader-Status?}
|
||||
|
||||
lobby[2a. Lobby-Phase]
|
||||
game[2b. Spiel-Phase]
|
||||
evaluation[2c. Auswertungs-Phase]
|
||||
|
||||
start-->choose_leader
|
||||
start-->choose_mode
|
||||
choose_mode-->|Leader / Spiel|choose_leader
|
||||
choose_mode-->|Wartung|maintenance
|
||||
maintenance-->|Zurück|choose_mode
|
||||
|
||||
choose_leader-->check_state
|
||||
check_state-->|Kein Spiel läuft|lobby
|
||||
check_state-->|Spiel läuft|game
|
||||
|
||||
1
firmware/apps/_samples/audio/.gitignore
vendored
Normal file
1
firmware/apps/_samples/audio/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
build*/
|
||||
10
firmware/apps/_samples/audio/CMakeLists.txt
Normal file
10
firmware/apps/_samples/audio/CMakeLists.txt
Normal 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)
|
||||
44
firmware/apps/_samples/audio/nrf52840dk_nrf52840.overlay
Normal file
44
firmware/apps/_samples/audio/nrf52840dk_nrf52840.overlay
Normal 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";
|
||||
};
|
||||
4
firmware/apps/_samples/audio/pm_static.yml
Normal file
4
firmware/apps/_samples/audio/pm_static.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
littlefs_storage:
|
||||
address: 0x0
|
||||
size: 0x800000
|
||||
region: external_flash
|
||||
30
firmware/apps/_samples/audio/prj.conf
Normal file
30
firmware/apps/_samples/audio/prj.conf
Normal 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
|
||||
68
firmware/apps/_samples/audio/src/main.c
Normal file
68
firmware/apps/_samples/audio/src/main.c
Normal 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;
|
||||
}
|
||||
2
firmware/apps/_samples/audio/tool/requirements.txt
Normal file
2
firmware/apps/_samples/audio/tool/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
pyserial
|
||||
cbor2
|
||||
120
firmware/apps/_samples/audio/tool/tool.py
Normal file
120
firmware/apps/_samples/audio/tool/tool.py
Normal 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()
|
||||
1
firmware/apps/_samples/ir_recv_adc/.gitignore
vendored
Normal file
1
firmware/apps/_samples/ir_recv_adc/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
build*/
|
||||
10
firmware/apps/_samples/ir_recv_adc/CMakeLists.txt
Normal file
10
firmware/apps/_samples/ir_recv_adc/CMakeLists.txt
Normal 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)
|
||||
@@ -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>;
|
||||
};
|
||||
};
|
||||
@@ -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";
|
||||
};
|
||||
4
firmware/apps/_samples/ir_recv_adc/pm_static.yml
Normal file
4
firmware/apps/_samples/ir_recv_adc/pm_static.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
littlefs_storage:
|
||||
address: 0x0
|
||||
size: 0x800000
|
||||
region: external_flash
|
||||
25
firmware/apps/_samples/ir_recv_adc/prj.conf
Normal file
25
firmware/apps/_samples/ir_recv_adc/prj.conf
Normal 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
|
||||
|
||||
15
firmware/apps/_samples/ir_recv_adc/src/main.c
Normal file
15
firmware/apps/_samples/ir_recv_adc/src/main.c
Normal 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;
|
||||
}
|
||||
2
firmware/apps/_samples/ir_recv_adc/tool/requirements.txt
Normal file
2
firmware/apps/_samples/ir_recv_adc/tool/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
pyserial
|
||||
cbor2
|
||||
120
firmware/apps/_samples/ir_recv_adc/tool/tool.py
Normal file
120
firmware/apps/_samples/ir_recv_adc/tool/tool.py
Normal 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()
|
||||
1
firmware/apps/_samples/ir_recv_sim/.gitignore
vendored
Normal file
1
firmware/apps/_samples/ir_recv_sim/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
build*/
|
||||
10
firmware/apps/_samples/ir_recv_sim/CMakeLists.txt
Normal file
10
firmware/apps/_samples/ir_recv_sim/CMakeLists.txt
Normal 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)
|
||||
@@ -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";
|
||||
};
|
||||
4
firmware/apps/_samples/ir_recv_sim/pm_static.yml
Normal file
4
firmware/apps/_samples/ir_recv_sim/pm_static.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
littlefs_storage:
|
||||
address: 0x0
|
||||
size: 0x800000
|
||||
region: external_flash
|
||||
26
firmware/apps/_samples/ir_recv_sim/prj.conf
Normal file
26
firmware/apps/_samples/ir_recv_sim/prj.conf
Normal 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
|
||||
|
||||
31
firmware/apps/_samples/ir_recv_sim/src/main.c
Normal file
31
firmware/apps/_samples/ir_recv_sim/src/main.c
Normal 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;
|
||||
}
|
||||
2
firmware/apps/_samples/ir_recv_sim/tool/requirements.txt
Normal file
2
firmware/apps/_samples/ir_recv_sim/tool/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
pyserial
|
||||
cbor2
|
||||
120
firmware/apps/_samples/ir_recv_sim/tool/tool.py
Normal file
120
firmware/apps/_samples/ir_recv_sim/tool/tool.py
Normal 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()
|
||||
1
firmware/apps/_samples/mcumgr
Submodule
1
firmware/apps/_samples/mcumgr
Submodule
Submodule firmware/apps/_samples/mcumgr added at b9c1c03f6e
1
firmware/apps/_samples/thread/.gitignore
vendored
Normal file
1
firmware/apps/_samples/thread/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
build*/
|
||||
10
firmware/apps/_samples/thread/CMakeLists.txt
Normal file
10
firmware/apps/_samples/thread/CMakeLists.txt
Normal 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)
|
||||
44
firmware/apps/_samples/thread/nrf52840dk_nrf52840.overlay
Normal file
44
firmware/apps/_samples/thread/nrf52840dk_nrf52840.overlay
Normal 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";
|
||||
};
|
||||
4
firmware/apps/_samples/thread/pm_static.yml
Normal file
4
firmware/apps/_samples/thread/pm_static.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
littlefs_storage:
|
||||
address: 0x0
|
||||
size: 0x800000
|
||||
region: external_flash
|
||||
22
firmware/apps/_samples/thread/prj.conf
Normal file
22
firmware/apps/_samples/thread/prj.conf
Normal 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
|
||||
44
firmware/apps/_samples/thread/src/main.c
Normal file
44
firmware/apps/_samples/thread/src/main.c
Normal 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;
|
||||
}
|
||||
2
firmware/apps/_samples/thread/tool/requirements.txt
Normal file
2
firmware/apps/_samples/thread/tool/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
pyserial
|
||||
cbor2
|
||||
120
firmware/apps/_samples/thread/tool/tool.py
Normal file
120
firmware/apps/_samples/thread/tool/tool.py
Normal 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()
|
||||
1
firmware/apps/_samples/utils/.gitignore
vendored
Normal file
1
firmware/apps/_samples/utils/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
build*/
|
||||
10
firmware/apps/_samples/utils/CMakeLists.txt
Normal file
10
firmware/apps/_samples/utils/CMakeLists.txt
Normal 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)
|
||||
44
firmware/apps/_samples/utils/nrf52840dk_nrf52840.overlay
Normal file
44
firmware/apps/_samples/utils/nrf52840dk_nrf52840.overlay
Normal 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";
|
||||
};
|
||||
4
firmware/apps/_samples/utils/pm_static.yml
Normal file
4
firmware/apps/_samples/utils/pm_static.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
littlefs_storage:
|
||||
address: 0x0
|
||||
size: 0x800000
|
||||
region: external_flash
|
||||
14
firmware/apps/_samples/utils/prj.conf
Normal file
14
firmware/apps/_samples/utils/prj.conf
Normal 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
|
||||
|
||||
24
firmware/apps/_samples/utils/src/main.c
Normal file
24
firmware/apps/_samples/utils/src/main.c
Normal 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;
|
||||
}
|
||||
2
firmware/apps/_samples/utils/tool/requirements.txt
Normal file
2
firmware/apps/_samples/utils/tool/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
pyserial
|
||||
cbor2
|
||||
120
firmware/apps/_samples/utils/tool/tool.py
Normal file
120
firmware/apps/_samples/utils/tool/tool.py
Normal 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()
|
||||
1
firmware/apps/_samples/watchdog
Submodule
1
firmware/apps/_samples/watchdog
Submodule
Submodule firmware/apps/_samples/watchdog added at 10ca5017c5
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
5
firmware/apps/vest/VERSION
Normal file
5
firmware/apps/vest/VERSION
Normal file
@@ -0,0 +1,5 @@
|
||||
VERSION_MAJOR = 0
|
||||
VERSION_MINOR = 0
|
||||
PATCHLEVEL = 0
|
||||
VERSION_TWEAK = 0
|
||||
EXTRAVERSION =
|
||||
1
firmware/apps/vest/pm_static.yml
Symbolic link
1
firmware/apps/vest/pm_static.yml
Symbolic link
@@ -0,0 +1 @@
|
||||
../leader/pm_static.yml
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
5
firmware/libs/audio/CMakeLists.txt
Normal file
5
firmware/libs/audio/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
if(CONFIG_AUDIO)
|
||||
zephyr_library()
|
||||
zephyr_library_sources(src/audio.c)
|
||||
zephyr_include_directories(include)
|
||||
endif()
|
||||
76
firmware/libs/audio/Kconfig
Normal file
76
firmware/libs/audio/Kconfig
Normal 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
|
||||
28
firmware/libs/audio/include/audio.h
Normal file
28
firmware/libs/audio/include/audio.h
Normal 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
|
||||
|
||||
271
firmware/libs/audio/src/audio.c
Normal file
271
firmware/libs/audio/src/audio.c
Normal 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");
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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(¶m, 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,
|
||||
};
|
||||
17
firmware/libs/fs_mgmt/CMakeLists.txt
Normal file
17
firmware/libs/fs_mgmt/CMakeLists.txt
Normal 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()
|
||||
30
firmware/libs/fs_mgmt/Kconfig
Normal file
30
firmware/libs/fs_mgmt/Kconfig
Normal 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
|
||||
7
firmware/libs/fs_mgmt/include/fs_mgmt.h
Normal file
7
firmware/libs/fs_mgmt/include/fs_mgmt.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#ifndef FS_MGMT_H
|
||||
#define FS_MGMT_H
|
||||
|
||||
int fs_mgmt_init(void);
|
||||
|
||||
#endif
|
||||
|
||||
327
firmware/libs/fs_mgmt/src/fs_mgmt.c
Normal file
327
firmware/libs/fs_mgmt/src/fs_mgmt.c
Normal 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;
|
||||
}
|
||||
16
firmware/libs/game_mgmt/CMakeLists.txt
Normal file
16
firmware/libs/game_mgmt/CMakeLists.txt
Normal 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()
|
||||
41
firmware/libs/game_mgmt/Kconfig
Normal file
41
firmware/libs/game_mgmt/Kconfig
Normal 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
|
||||
200
firmware/libs/game_mgmt/include/game_mgmt.h
Normal file
200
firmware/libs/game_mgmt/include/game_mgmt.h
Normal 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 */
|
||||
171
firmware/libs/game_mgmt/src/game_mgmt.c
Normal file
171
firmware/libs/game_mgmt/src/game_mgmt.c
Normal 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
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
19
firmware/libs/ir/include/ir.h
Normal file
19
firmware/libs/ir/include/ir.h
Normal 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 */
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
1
firmware/tools/audio/.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
samples/**/* filter=lfs diff=lfs merge=lfs -text
|
||||
2
firmware/tools/audio/.gitignore
vendored
Normal file
2
firmware/tools/audio/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
cache
|
||||
previews
|
||||
214
firmware/tools/audio/build_audio.py
Normal file
214
firmware/tools/audio/build_audio.py
Normal 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)
|
||||
14
firmware/tools/audio/countdown.yaml
Normal file
14
firmware/tools/audio/countdown.yaml
Normal 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"
|
||||
7
firmware/tools/audio/global.yaml
Normal file
7
firmware/tools/audio/global.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
paths:
|
||||
cache: "./cache"
|
||||
output: "../littlefs_generator/source_folder/a"
|
||||
preview: "./previews"
|
||||
samples: "./samples"
|
||||
settings:
|
||||
sample_rate: 16000
|
||||
BIN
firmware/tools/audio/lfs_source/countdown
Normal file
BIN
firmware/tools/audio/lfs_source/countdown
Normal file
Binary file not shown.
BIN
firmware/tools/audio/lfs_source/dead
Normal file
BIN
firmware/tools/audio/lfs_source/dead
Normal file
Binary file not shown.
BIN
firmware/tools/audio/lfs_source/g1
Normal file
BIN
firmware/tools/audio/lfs_source/g1
Normal file
Binary file not shown.
BIN
firmware/tools/audio/lfs_source/game_start
Normal file
BIN
firmware/tools/audio/lfs_source/game_start
Normal file
Binary file not shown.
BIN
firmware/tools/audio/lfs_source/s1
Normal file
BIN
firmware/tools/audio/lfs_source/s1
Normal file
Binary file not shown.
5
firmware/tools/audio/requirements.txt
Normal file
5
firmware/tools/audio/requirements.txt
Normal 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
|
||||
BIN
firmware/tools/audio/samples/horn.ogg
LFS
Normal file
BIN
firmware/tools/audio/samples/horn.ogg
LFS
Normal file
Binary file not shown.
11
firmware/tools/audio/sfx.yaml
Normal file
11
firmware/tools/audio/sfx.yaml
Normal 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"
|
||||
17
firmware/tools/audio/voice_game.yaml
Normal file
17
firmware/tools/audio/voice_game.yaml
Normal 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"]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user