Networkscan working! :)
Some checks failed
Deploy Docs / build-and-deploy (push) Has been cancelled

This commit is contained in:
2026-01-02 18:42:10 +01:00
parent b40e44c991
commit d7f004ee72
5 changed files with 301 additions and 144 deletions

View File

@@ -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

View File

@@ -7,12 +7,13 @@
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/settings/settings.h>
#include <lasertag_utils.h>
#include <thread_mgmt.h>
#include <ble_mgmt.h>
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;
}

View File

@@ -1,17 +1,26 @@
#ifndef THREAD_MGMT_H
#define THREAD_MGMT_H
/**
* @file thread_mgmt.h
* @brief Thread network management and stack initialization.
*/
#include <stdint.h>
/**
* @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 */
/**
* @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

View File

@@ -5,28 +5,133 @@
#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 <string.h>
#include <stdio.h>
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;
}

View File

@@ -3,16 +3,18 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lasertag Provisioning</title>
<title>Lasertag Provisioning & Mesh Discovery</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; display: flex; flex-direction: column; align-items: center; padding: 2rem; background: #f0f2f5; color: #333; }
.card { background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); width: 100%; max-width: 550px; }
h1 { font-size: 1.5rem; margin-bottom: 1rem; color: #1a73e8; }
.status-box { padding: 0.75rem; border-radius: 6px; margin-bottom: 1.5rem; font-size: 0.9rem; background: #e8f0fe; color: #1967d2; border: 1px solid #d2e3fc; min-height: 20px; }
.button-group { display: flex; gap: 10px; width: 100%; flex-wrap: wrap; }
button { background: #1a73e8; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 1rem; width: 100%; font-weight: 600; transition: background 0.2s; margin-top: 10px; }
.button-group { display: flex; gap: 10px; width: 100%; flex-wrap: wrap; margin-bottom: 10px; }
button { background: #1a73e8; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; font-size: 1rem; width: 100%; font-weight: 600; transition: background 0.2s; margin-top: 5px; }
button:hover { background: #1557b0; }
button:disabled { background: #ccc; cursor: not-allowed; }
.btn-danger { background: #d93025; }
.btn-danger:hover { background: #a50e0e; }
.btn-secondary { background: #5f6368; }
@@ -24,12 +26,17 @@
.config-section { margin-top: 1.5rem; }
.config-group { margin-bottom: 1rem; border-bottom: 1px solid #eee; padding-bottom: 1rem; }
.config-group:last-child { border-bottom: none; }
label { display: block; font-size: 0.85rem; color: #666; margin-bottom: 4px; font-weight: bold; }
.input-row { display: flex; gap: 8px; }
input { flex-grow: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 0.9rem; font-family: monospace; }
.btn-small { width: auto; font-size: 0.85rem; padding: 8px 16px; margin-top: 0; }
.node-list { margin-top: 1rem; background: #f8f9fa; border-radius: 6px; padding: 10px; border: 1px solid #ddd; min-height: 40px; }
.node-item { padding: 5px 0; border-bottom: 1px solid #eee; font-size: 0.9rem; display: flex; align-items: center; }
.node-item:last-child { border-bottom: none; }
.hint { font-size: 0.8rem; color: #888; margin-top: 1rem; font-style: italic; }
.action-bar { display: flex; gap: 10px; margin-bottom: 15px; }
</style>
</head>
<body>
@@ -38,16 +45,16 @@
<div class="status-box" id="status">Bereit zum Scannen...</div>
<div class="button-group">
<button id="connectBtn">Gerät suchen</button>
<button id="connectBtn">Gerät suchen & verbinden</button>
<button id="disconnectBtn" class="btn-danger" style="display: none;">Trennen</button>
</div>
<div class="config-section" id="globalActions" style="margin-top: 20px;">
<div class="config-section" id="globalActions">
<label>Globale Aktionen</label>
<div class="button-group">
<button id="genParamsBtn" class="btn-secondary">Zufallsparameter generieren</button>
<button id="saveToCookieBtn" class="btn-secondary">Netzwerk im Browser merken</button>
<button id="loadFromCookieBtn" class="btn-success" style="display: none;">Gemerktes Netzwerk auf Node schreiben</button>
<button id="saveToStorageBtn" class="btn-secondary">Netzwerk im Browser merken</button>
<button id="loadFromStorageBtn" class="btn-success" style="display: none;">Gemerktes Netzwerk auf Node schreiben</button>
</div>
</div>
@@ -106,6 +113,13 @@
<button class="btn-small" id="saveKeyBtn">Setzen</button>
</div>
</div>
<!-- Mesh Discovery Section -->
<div class="config-group">
<label>Entdeckte Mesh-Knoten (via CoAP)</label>
<div id="nodeListContainer" class="node-list">Keine Knoten entdeckt.</div>
<button id="refreshNodesBtn" class="btn-secondary" style="margin-top: 8px;">Mesh scannen</button>
</div>
</div>
<p class="hint">Hinweis: Alle Änderungen werden im NVS gespeichert. Ein Reboot ist erforderlich, um Thread neu zu starten.</p>
@@ -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 => `<div class="node-item">📡 ${n}</div>`).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();
</script>
</body>
</html>