From 8c15260166d7e0f71fde0be3f69b9f2dbe10230d Mon Sep 17 00:00:00 2001 From: Eduard Iten Date: Tue, 17 Feb 2026 12:52:32 +0100 Subject: [PATCH] Sync while working on OT --- firmware/apps/_samples/thread/.gitignore | 1 + firmware/apps/_samples/thread/CMakeLists.txt | 10 ++ firmware/apps/_samples/thread/pm_static.yml | 4 + firmware/apps/_samples/thread/prj.conf | 16 +++ firmware/apps/_samples/thread/src/main.c | 19 +++ .../_samples/thread/tool/requirements.txt | 2 + firmware/apps/_samples/thread/tool/tool.py | 120 ++++++++++++++++++ firmware/libs/thread_mgmt/Kconfig | 10 +- firmware/libs/thread_mgmt/src/thread_mgmt.c | 95 ++++++++++++-- 9 files changed, 262 insertions(+), 15 deletions(-) create mode 100644 firmware/apps/_samples/thread/.gitignore create mode 100644 firmware/apps/_samples/thread/CMakeLists.txt create mode 100644 firmware/apps/_samples/thread/pm_static.yml create mode 100644 firmware/apps/_samples/thread/prj.conf create mode 100644 firmware/apps/_samples/thread/src/main.c create mode 100644 firmware/apps/_samples/thread/tool/requirements.txt create mode 100644 firmware/apps/_samples/thread/tool/tool.py diff --git a/firmware/apps/_samples/thread/.gitignore b/firmware/apps/_samples/thread/.gitignore new file mode 100644 index 0000000..a5309e6 --- /dev/null +++ b/firmware/apps/_samples/thread/.gitignore @@ -0,0 +1 @@ +build*/ diff --git a/firmware/apps/_samples/thread/CMakeLists.txt b/firmware/apps/_samples/thread/CMakeLists.txt new file mode 100644 index 0000000..19f6b90 --- /dev/null +++ b/firmware/apps/_samples/thread/CMakeLists.txt @@ -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) diff --git a/firmware/apps/_samples/thread/pm_static.yml b/firmware/apps/_samples/thread/pm_static.yml new file mode 100644 index 0000000..1f22f30 --- /dev/null +++ b/firmware/apps/_samples/thread/pm_static.yml @@ -0,0 +1,4 @@ +littlefs_storage: + address: 0x0 + size: 0x800000 + region: external_flash diff --git a/firmware/apps/_samples/thread/prj.conf b/firmware/apps/_samples/thread/prj.conf new file mode 100644 index 0000000..c1285e3 --- /dev/null +++ b/firmware/apps/_samples/thread/prj.conf @@ -0,0 +1,16 @@ +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_BLE_MGMT=y +CONFIG_GAME_MGMT=y +CONFIG_THREAD_MGMT=y +CONFIG_THREAD_MGMT_LOG_LEVEL_DBG=y +CONFIG_THREAD_MGMT_SHELL=y \ No newline at end of file diff --git a/firmware/apps/_samples/thread/src/main.c b/firmware/apps/_samples/thread/src/main.c new file mode 100644 index 0000000..928d254 --- /dev/null +++ b/firmware/apps/_samples/thread/src/main.c @@ -0,0 +1,19 @@ +#include +#include +#include +#include + +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."); + return 0; +} diff --git a/firmware/apps/_samples/thread/tool/requirements.txt b/firmware/apps/_samples/thread/tool/requirements.txt new file mode 100644 index 0000000..a1e37cd --- /dev/null +++ b/firmware/apps/_samples/thread/tool/requirements.txt @@ -0,0 +1,2 @@ +pyserial +cbor2 diff --git a/firmware/apps/_samples/thread/tool/tool.py b/firmware/apps/_samples/thread/tool/tool.py new file mode 100644 index 0000000..9cd5273 --- /dev/null +++ b/firmware/apps/_samples/thread/tool/tool.py @@ -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"Fehler: Konnte {port} nicht öffnen ({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 + + # Sortierung: Verzeichnisse zuerst, dann Namen + 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-Art Auswahl + # 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: + # Prefix für die nächste Ebene erweitern + 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="Serieller Port (z.B. /dev/cu.usbmodem...)") + args = parser.parse_args() + + client = nRF_FS_Client(args.port, 115200) + print(f"--- Dateistruktur auf nRF ({args.port}) ---") + try: + # Initialer Aufruf + client.list_recursive("/") + finally: + client.close() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/firmware/libs/thread_mgmt/Kconfig b/firmware/libs/thread_mgmt/Kconfig index 04d6a8c..6264fcb 100644 --- a/firmware/libs/thread_mgmt/Kconfig +++ b/firmware/libs/thread_mgmt/Kconfig @@ -1,10 +1,11 @@ menuconfig THREAD_MGMT bool "Thread Management" + depends on BLE_MGMT select LASERTAG_UTILS select NETWORKING select NET_L2_OPENTHREAD select OPENTHREAD - select OPENTHREAD_SHELL + select OPENTHREAD_TIME_SYNC help Library for initializing and managing the OpenThread stack. @@ -14,6 +15,13 @@ if 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 + 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 diff --git a/firmware/libs/thread_mgmt/src/thread_mgmt.c b/firmware/libs/thread_mgmt/src/thread_mgmt.c index 78a930c..e2d70cc 100644 --- a/firmware/libs/thread_mgmt/src/thread_mgmt.c +++ b/firmware/libs/thread_mgmt/src/thread_mgmt.c @@ -16,6 +16,10 @@ LOG_MODULE_REGISTER(thread_mgmt, CONFIG_THREAD_MGMT_LOG_LEVEL); +#if IS_ENABLED(CONFIG_THREAD_MGMT_SHELL) +void init_custom_ot_commands(void); +#endif + #if (CONFIG_THREAD_MGMT_LOG_LEVEL >= LOG_LEVEL_DBG) static void thread_mgmt_state_notify_callback(otChangedFlags aFlags, void *aContext) { @@ -85,12 +89,12 @@ int thread_mgmt_init(void) dataset.mComponents.mIsPanIdPresent = true; dataset.mChannel = 15; dataset.mComponents.mIsChannelPresent = true; - uint8_t ext_pan_id[8]; - sys_csrand_get(ext_pan_id, sizeof(ext_pan_id)); + 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]; - sys_csrand_get(network_key, sizeof(network_key)); + 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); @@ -109,6 +113,10 @@ int thread_mgmt_init(void) LOG_DBG("Thread stack dataset after Thread stack initialization:"); thread_mgmt_print_dataset(&dataset); #endif + +#if IS_ENABLED(CONFIG_THREAD_MGMT_SHELL) + init_custom_ot_commands(); +#endif return 0; } @@ -170,8 +178,8 @@ void thread_mgmt_restart_thread_stack(device_config_payload_t *pending_config, b if (changed) { LOG_DBG("Thread stack restart required; dataset changed."); - - dataset.mActiveTimestamp.mSeconds++; + + dataset.mActiveTimestamp.mSeconds++; dataset.mComponents.mIsActiveTimestampPresent = true; otThreadSetEnabled(instance, false); @@ -180,12 +188,14 @@ void thread_mgmt_restart_thread_stack(device_config_payload_t *pending_config, b // 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) { + if (dataset.mComponents.mIsNetworkKeyPresent) + { otError key_err = otThreadSetNetworkKey(instance, &dataset.mNetworkKey); - if (key_err != OT_ERROR_NONE) { + 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); @@ -193,7 +203,8 @@ void thread_mgmt_restart_thread_stack(device_config_payload_t *pending_config, b } otError err = otDatasetSetActive(instance, &dataset); - if (err != OT_ERROR_NONE) { + if (err != OT_ERROR_NONE) + { LOG_ERR("Error writing dataset: %d", err); } @@ -203,7 +214,8 @@ void thread_mgmt_restart_thread_stack(device_config_payload_t *pending_config, b #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) { + 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); } @@ -245,7 +257,8 @@ 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) { + if (otDatasetGetActive(instance, &dataset) == OT_ERROR_NONE) + { memcpy(dest_8byte, dataset.mExtendedPanId.m8, 8); } } @@ -255,7 +268,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) { + if (otDatasetGetActive(instance, &dataset) == OT_ERROR_NONE) + { memcpy(dest_16byte, dataset.mNetworkKey.m8, 16); } } @@ -265,8 +279,61 @@ 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) { + if (otDatasetGetActive(instance, &dataset) == OT_ERROR_NONE) + { // Safe copy with forced null-termination snprintf(dest_str, max_len, "%s", dataset.mNetworkName.m8); } } + +/* --- Shell Commands --- */ +#if IS_ENABLED(CONFIG_THREAD_MGMT_SHELL) +#include +#include +#include + +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 \ No newline at end of file