diff --git a/firmware/apps/leader/prj.conf b/firmware/apps/leader/prj.conf index 3a2f1c6..5803d3c 100644 --- a/firmware/apps/leader/prj.conf +++ b/firmware/apps/leader/prj.conf @@ -24,6 +24,9 @@ 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 diff --git a/firmware/libs/ble_mgmt/src/ble_mgmt.c b/firmware/libs/ble_mgmt/src/ble_mgmt.c index 6ecce67..02f0db9 100644 --- a/firmware/libs/ble_mgmt/src/ble_mgmt.c +++ b/firmware/libs/ble_mgmt/src/ble_mgmt.c @@ -7,12 +7,13 @@ #include #include #include +#include #include LOG_MODULE_REGISTER(ble_mgmt, CONFIG_BLE_MGMT_LOG_LEVEL); /** - * Base UUID: 03afe2cf-6c64-4a22-9289-c3ae820cbcxx + * Basis UUID: 03afe2cf-6c64-4a22-9289-c3ae820cbcxx */ #define LT_UUID_BASE_VAL \ BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820cbc00) @@ -24,6 +25,7 @@ LOG_MODULE_REGISTER(ble_mgmt, CONFIG_BLE_MGMT_LOG_LEVEL); #define BT_UUID_LT_EXTPAN_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820cbc04)) #define BT_UUID_LT_NETKEY_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820cbc05)) #define BT_UUID_LT_NETNAME_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820cbc06)) +#define BT_UUID_LT_NODES_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820cbc07)) /* --- GATT Callbacks --- */ @@ -86,39 +88,66 @@ static ssize_t write_lasertag_val(struct bt_conn *conn, const struct bt_gatt_att return 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) +{ + /* Wenn irgendwas geschrieben wird, triggere Discovery im Thread Mesh */ + thread_mgmt_discover_nodes(); + return len; +} + /* Service Definition */ BT_GATT_SERVICE_DEFINE(provisioning_svc, BT_GATT_PRIMARY_SERVICE(BT_UUID_LT_SERVICE), + /* Gerätename */ BT_GATT_CHARACTERISTIC(BT_UUID_LT_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), + /* Thread PAN ID */ BT_GATT_CHARACTERISTIC(BT_UUID_LT_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 Kanal */ BT_GATT_CHARACTERISTIC(BT_UUID_LT_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_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), + /* Netzwerk Key */ BT_GATT_CHARACTERISTIC(BT_UUID_LT_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 Netzwerk Name */ BT_GATT_CHARACTERISTIC(BT_UUID_LT_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), + + /* Knoten-Liste / Discovery Trigger */ + BT_GATT_CHARACTERISTIC(BT_UUID_LT_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 const struct bt_data ad[] = { @@ -132,7 +161,7 @@ int ble_mgmt_init(void) { int err = bt_enable(NULL); if (err) return err; - LOG_INF("Bluetooth initialized"); + LOG_INF("Bluetooth initialisiert"); return 0; } @@ -154,7 +183,7 @@ int ble_mgmt_adv_start(void) 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", name); + LOG_INF("Advertising gestartet als: %s", name); } return err; } @@ -163,7 +192,7 @@ int ble_mgmt_adv_stop(void) { int err = bt_le_adv_stop(); if (!err) { - LOG_INF("Advertising stopped"); + LOG_INF("Advertising gestoppt"); } return err; } \ No newline at end of file diff --git a/firmware/libs/thread_mgmt/include/thread_mgmt.h b/firmware/libs/thread_mgmt/include/thread_mgmt.h index 39fed0d..b4fb0cd 100644 --- a/firmware/libs/thread_mgmt/include/thread_mgmt.h +++ b/firmware/libs/thread_mgmt/include/thread_mgmt.h @@ -1,17 +1,26 @@ #ifndef THREAD_MGMT_H #define THREAD_MGMT_H -/** - * @file thread_mgmt.h - * @brief Thread network management and stack initialization. - */ +#include /** - * @brief Initialize the OpenThread stack with parameters from NVS. - * * This function configures the operational dataset (Network Name, - * PAN ID, Key, etc.) and starts the Thread interface. - * * @return 0 on success, negative error code otherwise. + * @brief Initialisiert den OpenThread-Stack, UDP und CoAP. */ int thread_mgmt_init(void); -#endif /* THREAD_MGMT_H */ \ No newline at end of file +/** + * @brief Sendet eine UDP-Nachricht. + */ +int thread_mgmt_send_udp(const char *addr_str, uint8_t *payload, uint16_t len); + +/** + * @brief Startet die Gerätesuche via CoAP Multicast. + */ +int thread_mgmt_discover_nodes(void); + +/** + * @brief Gibt die Liste der entdeckten Knotennamen zurück (kommagetrennt). + */ +const char* thread_mgmt_get_discovered_list(void); + +#endif \ No newline at end of file diff --git a/firmware/libs/thread_mgmt/src/thread_mgmt.c b/firmware/libs/thread_mgmt/src/thread_mgmt.c index 05c4dee..c0e667f 100644 --- a/firmware/libs/thread_mgmt/src/thread_mgmt.c +++ b/firmware/libs/thread_mgmt/src/thread_mgmt.c @@ -5,28 +5,133 @@ #include #include #include +#include #include #include #include #include +#include LOG_MODULE_REGISTER(thread_mgmt, CONFIG_THREAD_MGMT_LOG_LEVEL); #define UDP_PORT 1234 +#define MAX_DISCOVERED_NODES 10 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) +{ + otError error = OT_ERROR_NONE; + otMessage *response; + otInstance *instance = (otInstance *)context; + + if (otCoapMessageGetCode(message) != OT_COAP_CODE_GET) { + return; + } + + LOG_INF("CoAP GET /id empfangen"); + + 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); + } +} + +static otCoapResource s_id_resource = { + .mUriPath = "id", + .mHandler = coap_id_handler, + .mContext = NULL, + .mNext = NULL +}; + +/* --- CoAP Discovery Client Logik --- */ + +static void coap_discover_res_handler(void *context, otMessage *message, const otMessageInfo *message_info, otError result) +{ + if (result != OT_ERROR_NONE || otCoapMessageGetCode(message) != OT_COAP_CODE_CONTENT) { + return; + } + + char name_buf[32]; + uint16_t length = otMessageGetLength(message) - otMessageGetOffset(message); + if (length > sizeof(name_buf) - 1) length = sizeof(name_buf) - 1; + + otMessageRead(message, otMessageGetOffset(message), name_buf, length); + name_buf[length] = '\0'; + + char addr_str[OT_IP6_ADDRESS_STRING_SIZE]; + otIp6AddressToString(&message_info->mPeerAddr, addr_str, sizeof(addr_str)); + + LOG_INF("Node entdeckt: %s (%s)", name_buf, addr_str); + + /* Zur Liste hinzufügen (einfaches CSV Format für 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; + + /* Liste zurücksetzen */ + 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 gestartet..."); + return 0; +} + +const char* thread_mgmt_get_discovered_list(void) +{ + return discovered_nodes_list; +} + +/* --- UDP & Init --- */ -/** - * @brief Callback für empfangene UDP-Nachrichten. - */ 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; - } + if (length > sizeof(buf) - 1) length = sizeof(buf) - 1; int read = otMessageRead(message, otMessageGetOffset(message), buf, length); buf[read] = '\0'; @@ -34,7 +139,6 @@ static void udp_receive_cb(void *context, otMessage *message, const otMessageInf char addr_str[OT_IP6_ADDRESS_STRING_SIZE]; otIp6AddressToString(&message_info->mPeerAddr, addr_str, sizeof(addr_str)); - /* Deutliche Log-Ausgabe für das Testen */ LOG_INF("------------------------------------------"); LOG_INF("UDP DATA RECEIVED!"); LOG_INF("From: [%s]", addr_str); @@ -45,7 +149,6 @@ static void udp_receive_cb(void *context, otMessage *message, const otMessageInf int thread_mgmt_send_udp(const char *addr_str, uint8_t *payload, uint16_t len) { struct otInstance *instance = openthread_get_default_instance(); - otError error = OT_ERROR_NONE; otMessage *message; otMessageInfo message_info; @@ -58,20 +161,10 @@ int thread_mgmt_send_udp(const char *addr_str, uint8_t *payload, uint16_t len) message = otUdpNewMessage(instance, NULL); if (message == NULL) return -ENOMEM; - error = otMessageAppend(message, payload, len); - if (error != OT_ERROR_NONE) { - otMessageFree(message); - return -EIO; - } + otMessageAppend(message, payload, len); + otUdpSend(instance, &s_udp_socket, message, &message_info); - error = otUdpSend(instance, &s_udp_socket, message, &message_info); - if (error != OT_ERROR_NONE) { - otMessageFree(message); - LOG_ERR("UDP Senden fehlgeschlagen (err %d)", error); - return -EIO; - } - - LOG_INF("UDP gesendet an %s: %d Bytes", addr_str, len); + LOG_INF("UDP gesendet an %s", addr_str); return 0; } @@ -79,73 +172,45 @@ int thread_mgmt_init(void) { struct otInstance *instance = openthread_get_default_instance(); otOperationalDataset dataset; - otError error; - if (!instance) { - LOG_ERR("OpenThread Instanz nicht gefunden"); - return -ENODEV; - } + if (!instance) return -ENODEV; - LOG_INF("Thread stack wird konfiguriert..."); - - /* Dataset-Struktur initialisieren */ + /* Dataset Setup */ memset(&dataset, 0, sizeof(otOperationalDataset)); - - /* 0. Active Timestamp - Wichtig für die Netzwerksynchronisation */ dataset.mActiveTimestamp.mSeconds = 1; dataset.mComponents.mIsActiveTimestampPresent = true; - - /* 1. Netzwerkname */ + const char *net_name = lasertag_get_thread_network_name(); memcpy(dataset.mNetworkName.m8, net_name, strlen(net_name)); dataset.mComponents.mIsNetworkNamePresent = true; - - /* 2. PAN ID */ dataset.mPanId = lasertag_get_thread_pan_id(); dataset.mComponents.mIsPanIdPresent = true; - - /* 3. Kanal */ dataset.mChannel = lasertag_get_thread_channel(); dataset.mComponents.mIsChannelPresent = true; - - /* 4. Extended PAN ID */ memcpy(dataset.mExtendedPanId.m8, lasertag_get_thread_ext_pan_id(), 8); dataset.mComponents.mIsExtendedPanIdPresent = true; - - /* 5. Netzwerk Key */ memcpy(dataset.mNetworkKey.m8, lasertag_get_thread_network_key(), 16); dataset.mComponents.mIsNetworkKeyPresent = true; - - /* 6. Mesh Local Prefix */ uint8_t ml_prefix[] = {0xfd, 0xde, 0xad, 0x00, 0xbe, 0xef, 0x00, 0x00}; memcpy(dataset.mMeshLocalPrefix.m8, ml_prefix, 8); dataset.mComponents.mIsMeshLocalPrefixPresent = true; - /* Dataset aktivieren */ otDatasetSetActive(instance, &dataset); - - /* Interface und Stack starten */ otIp6SetEnabled(instance, true); otThreadSetEnabled(instance, true); - /* UDP Socket initialisieren */ + /* 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); - error = otUdpOpen(instance, &s_udp_socket, udp_receive_cb, NULL); - if (error != OT_ERROR_NONE) { - LOG_ERR("UDP Socket konnte nicht geoeffnet werden (err %d)", error); - return -EIO; - } + /* CoAP Initialisierung */ + otCoapStart(instance, OT_DEFAULT_COAP_PORT); + s_id_resource.mContext = instance; + otCoapAddResource(instance, &s_id_resource); - /* otUdpBind für SDK v3.2.1 mit 4 Argumenten */ - error = otUdpBind(instance, &s_udp_socket, &listen_addr, OT_NETIF_UNSPECIFIED); - if (error != OT_ERROR_NONE) { - LOG_ERR("UDP Bind fehlgeschlagen (err %d)", error); - return -EIO; - } - - LOG_INF("Thread MGMT: Initialisiert, UDP Port %d offen.", UDP_PORT); + LOG_INF("Thread MGMT: Initialisiert, UDP %d & CoAP %d offen.", UDP_PORT, OT_DEFAULT_COAP_PORT); return 0; } \ No newline at end of file diff --git a/software/provisioning/index.html b/software/provisioning/index.html index 41a7c7c..5f08356 100644 --- a/software/provisioning/index.html +++ b/software/provisioning/index.html @@ -3,16 +3,18 @@ - Lasertag Provisioning + Lasertag Provisioning & Mesh Discovery @@ -38,16 +45,16 @@
Bereit zum Scannen...
- +
-
+
- - + +
@@ -106,6 +113,13 @@
+ + +
+ +
Keine Knoten entdeckt.
+ +

Hinweis: Alle Änderungen werden im NVS gespeichert. Ein Reboot ist erforderlich, um Thread neu zu starten.

@@ -117,10 +131,13 @@ const disconnectBtn = document.getElementById('disconnectBtn'); const status = document.getElementById('status'); const deviceInfo = document.getElementById('deviceInfo'); - const saveToCookieBtn = document.getElementById('saveToCookieBtn'); - const loadFromCookieBtn = document.getElementById('loadFromCookieBtn'); - const genParamsBtn = document.getElementById('genParamsBtn'); + const saveToStorageBtn = document.getElementById('saveToStorageBtn'); + const loadFromStorageBtn = document.getElementById('loadFromStorageBtn'); + const genParamsBtn = document.getElementById('genParamsBtn'); + const refreshNodesBtn = document.getElementById('refreshNodesBtn'); + const nodeListContainer = document.getElementById('nodeListContainer'); + const nameInput = document.getElementById('nameInput'); const netNameInput = document.getElementById('netNameInput'); const panInput = document.getElementById('panInput'); @@ -129,9 +146,10 @@ const keyInput = document.getElementById('keyInput'); let bluetoothDevice = null; + let gattServer = null; let provService = null; - // UUIDs passend zur Firmware + // Bluetooth UUIDs from Firmware const SERVICE_UUID = '03afe2cf-6c64-4a22-9289-c3ae820cbc00'; const NAME_CHAR = '03afe2cf-6c64-4a22-9289-c3ae820cbc01'; const PANID_CHAR = '03afe2cf-6c64-4a22-9289-c3ae820cbc02'; @@ -139,13 +157,19 @@ const EXTPAN_CHAR = '03afe2cf-6c64-4a22-9289-c3ae820cbc04'; const NETKEY_CHAR = '03afe2cf-6c64-4a22-9289-c3ae820cbc05'; const NETNAME_CHAR = '03afe2cf-6c64-4a22-9289-c3ae820cbc06'; + const NODES_CHAR = '03afe2cf-6c64-4a22-9289-c3ae820cbc07'; - // Check if we have saved network settings - function updateLoadButtonVisibility() { + // Utility: Visibility of load button + function updateUIState() { const saved = localStorage.getItem('lasertag_net_config'); - loadFromCookieBtn.style.display = (saved && bluetoothDevice && bluetoothDevice.gatt.connected) ? 'inline-block' : 'none'; + const connected = bluetoothDevice && bluetoothDevice.gatt.connected; + loadFromStorageBtn.style.display = (saved && connected) ? 'inline-block' : 'none'; + disconnectBtn.style.display = connected ? 'inline-block' : 'none'; + connectBtn.style.display = connected ? 'none' : 'inline-block'; + deviceInfo.style.display = connected ? 'block' : 'none'; } + // Utility: Conversions function hexToBytes(hex) { let bytes = []; for (let c = 0; c < hex.length; c += 2) @@ -163,9 +187,10 @@ return bytesToHex(arr); } + // BLE Reading async function readAll() { try { - status.textContent = 'Lese Konfiguration...'; + status.textContent = 'Lese Konfiguration vom Node...'; // Name const nameData = await (await provService.getCharacteristic(NAME_CHAR)).readValue(); @@ -192,49 +217,13 @@ keyInput.value = bytesToHex(new Uint8Array(keyData.buffer)); status.textContent = 'Verbunden mit ' + bluetoothDevice.name; - updateLoadButtonVisibility(); + updateUIState(); } catch (e) { status.textContent = 'Fehler beim Lesen: ' + e.message; } } - connectBtn.addEventListener('click', async () => { - try { - bluetoothDevice = await navigator.bluetooth.requestDevice({ - filters: [{ services: [SERVICE_UUID] }] - }); - - status.textContent = 'Verbinde...'; - const server = await bluetoothDevice.gatt.connect(); - provService = await server.getPrimaryService(SERVICE_UUID); - - deviceInfo.style.display = 'block'; - disconnectBtn.style.display = 'inline-block'; - connectBtn.style.display = 'none'; - - await readAll(); - - bluetoothDevice.addEventListener('gattserverdisconnected', onDisconnected); - } catch (error) { - status.textContent = 'Suche abgebrochen oder Fehler: ' + error.message; - } - }); - - disconnectBtn.addEventListener('click', () => { - if (bluetoothDevice && bluetoothDevice.gatt.connected) { - bluetoothDevice.gatt.disconnect(); - } - }); - - function onDisconnected() { - status.textContent = 'Verbindung getrennt.'; - deviceInfo.style.display = 'none'; - disconnectBtn.style.display = 'none'; - connectBtn.style.display = 'inline-block'; - provService = null; - updateLoadButtonVisibility(); - } - + // BLE Writing helper async function writeValue(uuid, data) { if (!provService) return; try { @@ -248,7 +237,37 @@ } } - // Global Action: Generate Random Params + // Event Listeners + connectBtn.addEventListener('click', async () => { + try { + bluetoothDevice = await navigator.bluetooth.requestDevice({ + filters: [{ services: [SERVICE_UUID] }] + }); + + status.textContent = 'Verbinde mit GATT Server...'; + gattServer = await bluetoothDevice.gatt.connect(); + provService = await gattServer.getPrimaryService(SERVICE_UUID); + + await readAll(); + bluetoothDevice.addEventListener('gattserverdisconnected', onDisconnected); + } catch (error) { + status.textContent = 'Suche abgebrochen: ' + error.message; + } + }); + + disconnectBtn.addEventListener('click', () => { + if (bluetoothDevice && bluetoothDevice.gatt.connected) { + bluetoothDevice.gatt.disconnect(); + } + }); + + function onDisconnected() { + status.textContent = 'Verbindung getrennt.'; + provService = null; + updateUIState(); + } + + // Action: Generate Random Params genParamsBtn.addEventListener('click', () => { panInput.value = generateRandomHex(4); extPanInput.value = generateRandomHex(16); @@ -257,8 +276,8 @@ status.textContent = 'Zufallsparameter generiert.'; }); - // Global Action: Save to Local Storage - saveToCookieBtn.addEventListener('click', () => { + // Action: Save to Local Storage + saveToStorageBtn.addEventListener('click', () => { const config = { netName: netNameInput.value, panId: panInput.value, @@ -267,44 +286,43 @@ netKey: keyInput.value }; localStorage.setItem('lasertag_net_config', JSON.stringify(config)); - status.textContent = 'Netzwerk-Einstellungen lokal gemerkt.'; - updateLoadButtonVisibility(); + status.textContent = 'Netzwerk-Einstellungen im Browser gespeichert.'; + updateUIState(); }); - // Global Action: Load and Write to Node - loadFromCookieBtn.addEventListener('click', async () => { + // Action: Load from Local Storage and Batch-Write + loadFromStorageBtn.addEventListener('click', async () => { const saved = localStorage.getItem('lasertag_net_config'); if (!saved || !provService) return; try { const config = JSON.parse(saved); - status.textContent = 'Schreibe gemerktes Netzwerk...'; + status.textContent = 'Batch-Schreibvorgang läuft...'; - // Write each setting const encoder = new TextEncoder(); - // Net Name + // 1. Net Name await (await provService.getCharacteristic(NETNAME_CHAR)).writeValue(encoder.encode(config.netName)); - // PAN ID + // 2. PAN ID const panView = new DataView(new ArrayBuffer(2)); panView.setUint16(0, parseInt(config.panId, 16), true); await (await provService.getCharacteristic(PANID_CHAR)).writeValue(panView.buffer); - // Channel + // 3. Channel const chanView = new DataView(new ArrayBuffer(1)); chanView.setUint8(0, parseInt(config.channel)); await (await provService.getCharacteristic(CHAN_CHAR)).writeValue(chanView.buffer); - // Ext PAN + // 4. Ext PAN await (await provService.getCharacteristic(EXTPAN_CHAR)).writeValue(hexToBytes(config.extPanId)); - // Net Key + // 5. Net Key await (await provService.getCharacteristic(NETKEY_CHAR)).writeValue(hexToBytes(config.netKey)); status.textContent = 'Netzwerk vollständig übertragen!'; - // Update UI fields to show what was written + // Sync UI netNameInput.value = config.netName; panInput.value = config.panId; chanInput.value = config.channel; @@ -316,7 +334,38 @@ } }); - // Manual Saves + // Action: Discovery (CoAP) + refreshNodesBtn.addEventListener('click', async () => { + if (!provService) return; + try { + status.textContent = 'Triggere Mesh-Discovery...'; + const char = await provService.getCharacteristic(NODES_CHAR); + + // Write triggers discovery on the nRF node + await char.writeValue(new Uint8Array([1])); + + status.textContent = 'Warte auf CoAP Antworten...'; + nodeListContainer.innerHTML = "Scanne..."; + + // Wait 3 seconds for CoAP responses to arrive and be processed + setTimeout(async () => { + const data = await char.readValue(); + const listStr = new TextDecoder().decode(data); + + if (listStr.trim() === "") { + nodeListContainer.innerHTML = "Keine anderen Knoten gefunden."; + } else { + const nodes = listStr.split(','); + nodeListContainer.innerHTML = nodes.map(n => `
📡 ${n}
`).join(''); + } + status.textContent = 'Mesh-Scan abgeschlossen.'; + }, 3000); + } catch (e) { + status.textContent = 'Scan fehlgeschlagen: ' + e.message; + } + }); + + // Manual Save Handlers document.getElementById('saveNameBtn').addEventListener('click', () => writeValue(NAME_CHAR, new TextEncoder().encode(nameInput.value))); @@ -341,6 +390,8 @@ document.getElementById('saveKeyBtn').addEventListener('click', () => writeValue(NETKEY_CHAR, hexToBytes(keyInput.value))); + // Initial check for stored config + updateUIState(); \ No newline at end of file