diff --git a/firmware/apps/leader/prj.conf b/firmware/apps/leader/prj.conf index 5803d3c..153d4a0 100644 --- a/firmware/apps/leader/prj.conf +++ b/firmware/apps/leader/prj.conf @@ -34,6 +34,10 @@ CONFIG_BT=y CONFIG_BT_PERIPHERAL=y CONFIG_BT_DEVICE_NAME="Lasertag-Device" CONFIG_BT_DEVICE_NAME_DYNAMIC=y +CONFIG_BT_L2CAP_TX_MTU=252 +CONFIG_BT_BUF_ACL_TX_SIZE=251 +CONFIG_BT_BUF_ACL_RX_SIZE=251 +CONFIG_BT_ATT_PREPARE_COUNT=5 # Enable Lasertag Shared Modules CONFIG_LASERTAG_UTILS=y diff --git a/firmware/apps/vest/prj.conf b/firmware/apps/vest/prj.conf index 5803d3c..153d4a0 100644 --- a/firmware/apps/vest/prj.conf +++ b/firmware/apps/vest/prj.conf @@ -34,6 +34,10 @@ CONFIG_BT=y CONFIG_BT_PERIPHERAL=y CONFIG_BT_DEVICE_NAME="Lasertag-Device" CONFIG_BT_DEVICE_NAME_DYNAMIC=y +CONFIG_BT_L2CAP_TX_MTU=252 +CONFIG_BT_BUF_ACL_TX_SIZE=251 +CONFIG_BT_BUF_ACL_RX_SIZE=251 +CONFIG_BT_ATT_PREPARE_COUNT=5 # Enable Lasertag Shared Modules CONFIG_LASERTAG_UTILS=y diff --git a/firmware/libs/ble_mgmt/include/ble_mgmt.h b/firmware/libs/ble_mgmt/include/ble_mgmt.h index 5ec9197..c2e5516 100644 --- a/firmware/libs/ble_mgmt/include/ble_mgmt.h +++ b/firmware/libs/ble_mgmt/include/ble_mgmt.h @@ -35,4 +35,24 @@ int ble_mgmt_adv_start(void); */ int ble_mgmt_adv_stop(void); +/** + * @brief Get the current system state + */ +uint8_t lasertag_get_system_state(void); + +/** + * @brief Set the current system state. + */ +void lasertag_set_system_state(uint8_t state); + +/** + * @brief Get the current 64-bit Game ID + */ +uint64_t lasertag_get_game_id(void); + +/** + * @brief Set the current 64-bit Game ID. + */ +void lasertag_set_game_id(uint64_t id); + #endif /* BLE_MGMT_H */ \ No newline at end of file diff --git a/firmware/libs/ble_mgmt/src/ble_mgmt.c b/firmware/libs/ble_mgmt/src/ble_mgmt.c index 2eace3b..f11456a 100644 --- a/firmware/libs/ble_mgmt/src/ble_mgmt.c +++ b/firmware/libs/ble_mgmt/src/ble_mgmt.c @@ -1,3 +1,14 @@ +/** + * BLE Management Module (ble_mgmt.c) + * + * Handles Bluetooth Low Energy (BLE) setup, advertising, GATT services, + * and connection management for the Lasertag device. + * + * Services provided: + * - Provisioning Service (0x10xx): Device configuration and Thread mesh setup + * - Game Service (0x20xx): Game-related commands and logging + */ + #include #include #include @@ -9,47 +20,104 @@ #include #include #include +#include LOG_MODULE_REGISTER(ble_mgmt, CONFIG_BLE_MGMT_LOG_LEVEL); -/** - * Base UUID: 03afe2cf-6c64-4a22-9289-c3ae820cXXXX - * Alias positions: Byte 12 & 13 - */ +/* ============================================================================ + UUID Definitions + ============================================================================ + Base UUID: 03afe2cf-6c64-4a22-9289-c3ae820cXXXX + Service and characteristic IDs use the last two bytes (XXXX) + ========================================================================== */ #define LT_UUID_BASE_VAL \ BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c0000) -/* ========================================================================== - SERVICE 1: PROVISIONING (0x10XX) - ========================================================================== */ + +/* PROVISIONING SERVICE (0x10xx): Device configuration & Thread mesh setup */ #define BT_UUID_LT_PROV_SERVICE BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c1000)) +/* Provisioning characteristics */ #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_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)) -/* ========================================================================== - SERVICE 2: GAME (0x20XX) - ========================================================================== */ +/* Full configuration pack characteristic */ +#define BT_UUID_LT_PROV_CONFIG_CHAR BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c100c)) + +/* GAME SERVICE (0x20xx): Game-related commands and logging */ #define BT_UUID_LT_GAME_SERVICE BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x03afe2cf, 0x6c64, 0x4a22, 0x9289, 0xc3ae820c2000)) +/* Game service characteristics */ #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)) -/* --- Global Variables --- */ -static uint8_t device_role = 0; // Store device type for provisioning -static uint8_t adv_enabled = 0; // Track advertising state -static struct k_work_delayable adv_restart_work; -/* --- GATT Callbacks --- */ +/* ============================================================================ + Data Structures + ============================================================================ */ +/** + * Leader configuration payload. + * Packed structure for transmitting full leader configuration via BLE. + * Total size: 86 bytes + */ +struct leader_config_payload { + 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 (16 chars + \0) */ + char node_name[33]; /* Offset 53 (32 chars + \0) */ +} __packed; + + +/* ============================================================================ + Global State Variables + ============================================================================ */ +static uint8_t device_role = 0; /* Store device type for provisioning */ +static uint8_t adv_enabled = 0; /* Track advertising state */ +static struct k_work_delayable adv_restart_work; /* Delayed advertising restart */ +static uint8_t system_state = 0; /* Game system state */ +static uint64_t game_id = 0; /* Current game identifier */ + +/* ============================================================================ + Advertising Data + ============================================================================ */ +/* Manufacturer data: last byte contains device role type */ +static uint8_t mfg_data[] = { 0xff, 0xff, 0x00 }; + +/* Advertising data array with UUID and flags */ +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, + 0x00, 0x10, 0x0c, 0x82, 0xae, 0xc3, 0x89, 0x92, + 0x22, 0x4a, 0x64, 0x6c, 0xcf, 0xe2, 0xaf, 0x03), + BT_DATA(BT_DATA_MANUFACTURER_DATA, mfg_data, sizeof(mfg_data)), +}; + + +/* ============================================================================ + GATT Callback Handlers + + These functions handle read/write operations on GATT characteristics. + They validate requests and interface with device configuration APIs. + ============================================================================ */ + +/** + * Read handler for provisioning characteristics. + * Supports reading: device type, name, PAN ID, channel, extended PAN ID, + * network key, and network name. + */ static ssize_t read_lasertag_val(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset) { @@ -66,6 +134,8 @@ static ssize_t read_lasertag_val(struct bt_conn *conn, const struct bt_gatt_attr val_ptr = lasertag_get_device_name(); val_len = strlen(val_ptr); } + /* Disabled characteristics 1002-1006 */ + /* else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PROV_PANID_CHAR) == 0) { static uint16_t pan_id; @@ -95,10 +165,16 @@ static ssize_t read_lasertag_val(struct bt_conn *conn, const struct bt_gatt_attr 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); } +/** + * Write handler for provisioning characteristics. + * Validates write length and updates device configuration via lasertag_utils APIs. + * Also updates Bluetooth advertised name when device name is changed. + */ 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) { @@ -110,6 +186,8 @@ static ssize_t write_lasertag_val(struct bt_conn *conn, const struct bt_gatt_att bt_set_name(lasertag_get_device_name()); } } + /* Disabled characteristics 1002-1006 */ + /* else if (bt_uuid_cmp(attr->uuid, BT_UUID_LT_PROV_PANID_CHAR) == 0) { if (len != 2) @@ -138,12 +216,17 @@ static ssize_t write_lasertag_val(struct bt_conn *conn, const struct bt_gatt_att { rc = lasertag_set_thread_network_name(buf, len); } + */ if (rc) return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY); return len; } +/** + * Read handler for discovered nodes characteristic. + * Returns the list of discovered Thread mesh nodes as a string. + */ static ssize_t read_discovered_nodes(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, uint16_t len, uint16_t offset) { @@ -151,6 +234,10 @@ static ssize_t read_discovered_nodes(struct bt_conn *conn, const struct bt_gatt_ return bt_gatt_attr_read(conn, attr, buf, len, offset, list, strlen(list)); } +/** + * Write handler for node discovery trigger characteristic. + * Any write to this characteristic starts the Thread mesh node discovery process. + */ 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) { @@ -158,67 +245,142 @@ static ssize_t write_discover_cmd(struct bt_conn *conn, const struct bt_gatt_att thread_mgmt_discover_nodes(); return len; } +/** + * Read handler for the full leader configuration. + * Returns a packed structure containing all device configuration in a single read. + */ +static ssize_t read_leader_config(struct bt_conn *conn, const struct bt_gatt_attr *attr, + void *buf, uint16_t len, uint16_t offset) +{ + struct leader_config_payload payload; + memset(&payload, 0, sizeof(payload)); -/* Service Definition */ + payload.system_state = lasertag_get_system_state(); + payload.game_id = lasertag_get_game_id(); + payload.pan_id = lasertag_get_thread_pan_id(); + payload.channel = lasertag_get_thread_channel(); + + memcpy(payload.ext_pan_id, lasertag_get_thread_ext_pan_id(), 8); + memcpy(payload.network_key, lasertag_get_thread_network_key(), 16); + + /* Ensure null termination and copy strings */ + strncpy(payload.network_name, lasertag_get_thread_network_name(), 16); + strncpy(payload.node_name, lasertag_get_device_name(), 32); + + return bt_gatt_attr_read(conn, attr, buf, len, offset, (const void *)&payload, sizeof(payload)); +} + +/** + * Write handler for the full leader configuration. + * Accepts a packed structure and applies all configuration at once. + */ +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(struct leader_config_payload)) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN); + } + + const struct leader_config_payload *payload = (const struct leader_config_payload *)buf; + + /* Apply RAM values */ + lasertag_set_system_state(payload->system_state); + lasertag_set_game_id(payload->game_id); + + /* Apply NVS values via utils */ + lasertag_set_thread_pan_id(payload->pan_id); + lasertag_set_thread_channel(payload->channel); + lasertag_set_thread_ext_pan_id(payload->ext_pan_id); + lasertag_set_thread_network_key(payload->network_key); + lasertag_set_thread_network_name(payload->network_name, strlen(payload->network_name)); + + lasertag_set_device_name((const void *)payload->node_name, strlen(payload->node_name)); + bt_set_name(lasertag_get_device_name()); + + LOG_INF("Leader config updated via BLE (Packed Struct)"); + return len; +} +/* ============================================================================ + GATT Service Definition + + Defines the Provisioning Service with all characteristics and callbacks. + ============================================================================ */ BT_GATT_SERVICE_DEFINE(provisioning_svc, BT_GATT_PRIMARY_SERVICE(BT_UUID_LT_PROV_SERVICE), - /* Device name */ + /* Device name (readable and writable) */ 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) */ + /* Device type / role (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 */ + + /* Thread PAN ID (read/write) - DISABLED */ + /* 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 */ + /* Thread channel (read/write) - DISABLED */ + /* 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 */ + /* Extended PAN ID (read/write) - DISABLED */ + /* 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 */ + /* Network key (read/write) - DISABLED */ + /* 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 */ + /* Thread network name (read/write) - DISABLED */ + /* 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 */ + /* Node discovery list and trigger (read/write) */ 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), ); + read_discovered_nodes, write_discover_cmd, NULL), -static uint8_t mfg_data[] = { 0xff, 0xff, 0x00 }; // Last byte for device role -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, - 0x00, 0x10, 0x0c, 0x82, 0xae, 0xc3, 0x89, 0x92, - 0x22, 0x4a, 0x64, 0x6c, 0xcf, 0xe2, 0xaf, 0x03), - BT_DATA(BT_DATA_MANUFACTURER_DATA, mfg_data, sizeof(mfg_data)), -}; + /* Full leader configuration (packed struct) */ + 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), ); + +/* ============================================================================ + Internal Helper Functions + ============================================================================ */ + +/** + * Work handler for delayed advertising restart. + * Called when a device disconnects to resume advertising after a brief delay. + */ static void adv_restart_work_handler(struct k_work *work) { if (adv_enabled == 0) @@ -232,6 +394,17 @@ static void adv_restart_work_handler(struct k_work *work) } } +/* ============================================================================ + Public Initialization & Management Functions + ============================================================================ */ + +/** + * Initialize the BLE module. + * Enables Bluetooth and sets up the device role for advertising. + * + * @param device_type The device type/role (leader, weapon, vest, beacon) + * @return 0 on success, negative error code on failure + */ int ble_mgmt_init(uint8_t device_type) { device_role = device_type; @@ -244,6 +417,12 @@ int ble_mgmt_init(uint8_t device_type) return 0; } +/** + * Start BLE advertising. + * Advertises device name and type, configures fast advertising intervals. + * + * @return 0 on success, negative error code on failure + */ int ble_mgmt_adv_start(void) { const char set_device_role = device_role; @@ -272,6 +451,11 @@ int ble_mgmt_adv_start(void) 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(); @@ -283,6 +467,42 @@ int ble_mgmt_adv_stop(void) return err; } +/* ============================================================================ + Game State Management Functions + ============================================================================ */ + +/** + * Get the current system state. + * @return Current system state value + */ +uint8_t lasertag_get_system_state(void) { return system_state; } + +/** + * Set the system state. + * @param state The new system state + */ +void lasertag_set_system_state(uint8_t state) { system_state = state; } + +/** + * Get the current game ID. + * @return Current game identifier + */ +uint64_t lasertag_get_game_id(void) { return game_id; } + +/** + * Set the game ID. + * @param id The new game identifier + */ +void lasertag_set_game_id(uint64_t id) { game_id = id; } + +/* ============================================================================ + 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) { @@ -293,12 +513,17 @@ static void connected(struct bt_conn *conn, uint8_t err) } } +/** + * Callback for when a device disconnects. + * Logs the disconnection and schedules advertising restart. + */ static void disconnected(struct bt_conn *conn, uint8_t reason) { LOG_INF("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, diff --git a/software/app/devtools_options.yaml b/software/app/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/software/app/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/software/app/lib/constants.dart b/software/app/lib/constants.dart index 02b755a..2f08930 100644 --- a/software/app/lib/constants.dart +++ b/software/app/lib/constants.dart @@ -2,16 +2,17 @@ class LasertagUUIDs { static const String base = "03afe2cf-6c64-4a22-9289-c3ae820c"; static const String provService = "${base}1000"; static const String provNameChar = "${base}1001"; - static const String provPanIdChar = "${base}1002"; - static const String provChanChar = "${base}1003"; - static const String provExtPanIdChar = "${base}1004"; - static const String provNetKeyChar = "${base}1005"; - static const String provNetNameChar = "${base}1006"; + // static const String provPanIdChar = "${base}1002"; + // static const String provChanChar = "${base}1003"; + // static const String provExtPanIdChar = "${base}1004"; + // static const String provNetKeyChar = "${base}1005"; + // static const String provNetNameChar = "${base}1006"; static const String provTypeChar = "${base}1008"; - + static const String provConfigChar = "${base}100c"; + // Gerätetypen aus deiner ble_mgmt.h static const int typeLeader = 0x01; static const int typeWeapon = 0x02; static const int typeVest = 0x03; static const int typeBeacon = 0x04; -} \ No newline at end of file +} diff --git a/software/app/lib/models/device_config_model.dart b/software/app/lib/models/device_config_model.dart new file mode 100644 index 0000000..023fead --- /dev/null +++ b/software/app/lib/models/device_config_model.dart @@ -0,0 +1,87 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +class DeviceConfig { + final int systemState; + final BigInt gameId; + final int panId; + final int channel; + final String extPanId; + final String networkKey; + final String networkName; + final String nodeName; + + DeviceConfig({ + required this.systemState, + required this.gameId, + required this.panId, + required this.channel, + required this.extPanId, + required this.networkKey, + required this.networkName, + required this.nodeName, + }); + + factory DeviceConfig.fromBytes(List bytes) { + final data = ByteData.sublistView(Uint8List.fromList(bytes)); + + // Manuelles Decoding von 64-Bit (2x 32-Bit), da getUint64 nicht Standard ist + final low = data.getUint32(1, Endian.little); + final high = data.getUint32(5, Endian.little); + final fullGameId = (BigInt.from(high) << 32) | BigInt.from(low); + + return DeviceConfig( + systemState: data.getUint8(0), + gameId: fullGameId, + panId: data.getUint16(9, Endian.little), + channel: data.getUint8(11), + extPanId: _bytesToHex(bytes.sublist(12, 20)), + networkKey: _bytesToHex(bytes.sublist(20, 36)), + networkName: _decodeString(bytes.sublist(36, 53)), + nodeName: _decodeString(bytes.sublist(53, 86)), + ); + } + + Uint8List toBytes() { + final bytes = Uint8List(86); + final data = ByteData.view(bytes.buffer); + + data.setUint8(0, systemState); + + // Encoding von 64-Bit BigInt in zwei 32-Bit Segmente + data.setUint32(1, (gameId & BigInt.from(0xFFFFFFFF)).toInt(), Endian.little); + data.setUint32(5, (gameId >> 32).toInt(), Endian.little); + + data.setUint16(9, panId, Endian.little); + data.setUint8(11, channel); + + _writeHexToBuffer(bytes, 12, extPanId); + _writeHexToBuffer(bytes, 20, networkKey); + _writeStringToBuffer(bytes, 36, 17, networkName); + _writeStringToBuffer(bytes, 53, 33, nodeName); + + return bytes; + } + + static String _decodeString(List bytes) { + int nullIdx = bytes.indexOf(0); + return utf8.decode(nullIdx == -1 ? bytes : bytes.sublist(0, nullIdx)).trim(); + } + + static void _writeStringToBuffer(Uint8List buffer, int offset, int maxLen, String val) { + final encoded = utf8.encode(val); + for (int i = 0; i < maxLen - 1 && i < encoded.length; i++) { + buffer[offset + i] = encoded[i]; + } + // Buffer ist bereits mit 0 initialisiert, Terminator steht also am Ende + } + + static String _bytesToHex(List bytes) => + bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join().toUpperCase(); + + static void _writeHexToBuffer(Uint8List buffer, int offset, String hex) { + for (int i = 0; i < hex.length; i += 2) { + buffer[offset + (i ~/ 2)] = int.parse(hex.substring(i, i + 2), radix: 16); + } + } +} \ No newline at end of file diff --git a/software/app/lib/providers/device_provider.dart b/software/app/lib/providers/device_provider.dart index 99df7bc..1439c29 100644 --- a/software/app/lib/providers/device_provider.dart +++ b/software/app/lib/providers/device_provider.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; @@ -7,6 +8,7 @@ import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import '../constants.dart'; import '../models/device_model.dart'; +import '../models/device_config_model.dart'; // WICHTIG: Korrekter Import class DeviceProvider extends ChangeNotifier { final List _discoveredLeaders = []; @@ -16,172 +18,129 @@ class DeviceProvider extends ChangeNotifier { List get leaders => _discoveredLeaders; List get peripherals => _discoveredPeripherals; - // Öffentlicher API-Entry: Scan starten + // ... startScan bleibt gleich ... void startScan() async { _discoveredLeaders.clear(); _discoveredPeripherals.clear(); notifyListeners(); - - await FlutterBluePlus.stopScan(); // Scan sauber stoppen vor Neustart - - await FlutterBluePlus.adapterState - .where((s) => s == BluetoothAdapterState.on) - .first; - + await FlutterBluePlus.stopScan(); + await FlutterBluePlus.adapterState.where((s) => s == BluetoothAdapterState.on).first; _scanSubscription?.cancel(); _scanSubscription = FlutterBluePlus.scanResults.listen((results) { for (ScanResult r in results) { - bool hasService = r.advertisementData.serviceUuids.contains( - Guid(LasertagUUIDs.provService), - ); + bool hasService = r.advertisementData.serviceUuids.contains(Guid(LasertagUUIDs.provService)); if (!hasService) continue; - final mfgData = r.advertisementData.manufacturerData[65535]; - if (mfgData != null && mfgData.isNotEmpty) { final device = LasertagDevice( id: r.device.remoteId.toString(), - name: r.device.platformName.isEmpty - ? "Unbekannt" - : r.device.platformName, + name: r.device.platformName.isEmpty ? "Unbekannt" : r.device.platformName, type: mfgData[0], btDevice: r.device, isConnected: false, - isNameVerified: false, // Initial immer unversichert (Kursiv) + isNameVerified: false, ); - _addDeviceToLists(device); } } }); - - try { - await FlutterBluePlus.startScan(timeout: const Duration(seconds: 15)); - } catch (e) { - debugPrint("Scan-Fehler: $e"); - } + try { await FlutterBluePlus.startScan(timeout: const Duration(seconds: 15)); } catch (e) { debugPrint("Scan-Fehler: $e"); } } - // Öffentlicher API-Entry: Namen von der Hardware lesen Future readDeviceNameFromHardware(LasertagDevice ltDevice) async { try { if (!ltDevice.btDevice.isConnected) { - await ltDevice.btDevice.connect( - license: License.free, - timeout: const Duration(seconds: 5), - ); + await ltDevice.btDevice.connect(license: License.free, timeout: const Duration(seconds: 5)); + } + List srv = await ltDevice.btDevice.discoverServices(); + var c = srv.firstWhere((s) => s.uuid == Guid(LasertagUUIDs.provService)) + .characteristics.firstWhere((c) => c.uuid == Guid(LasertagUUIDs.provNameChar)); + List value = await c.read(); + String hardwareName = utf8.decode(value); + updateDeviceName(ltDevice.id, hardwareName, verified: true); + return hardwareName; + } finally { await ltDevice.btDevice.disconnect(); } + } + + Future updateDeviceNameOnHardware(LasertagDevice ltDevice, String newName) async { + try { + await ltDevice.btDevice.connect(license: License.free, timeout: const Duration(seconds: 10)); + List srv = await ltDevice.btDevice.discoverServices(); + var c = srv.firstWhere((s) => s.uuid == Guid(LasertagUUIDs.provService)) + .characteristics.firstWhere((c) => c.uuid == Guid(LasertagUUIDs.provNameChar)); + await c.write(utf8.encode(newName)); + updateDeviceName(ltDevice.id, newName, verified: true); + } finally { await ltDevice.btDevice.disconnect(); } + } + + // Die neuen optimierten Methoden + Future readDeviceConfig(LasertagDevice device) async { + try { + if (!device.btDevice.isConnected) { + await device.btDevice.connect(license: License.free, timeout: const Duration(seconds: 10)); + } + List srv = await device.btDevice.discoverServices(); + var c = srv.firstWhere((s) => s.uuid == Guid(LasertagUUIDs.provService)) + .characteristics.firstWhere((c) => c.uuid == Guid(LasertagUUIDs.provConfigChar)); + final bytes = await c.read(); + return DeviceConfig.fromBytes(bytes); + } finally { await device.btDevice.disconnect(); } + } + + Future provisionDevice(LasertagDevice device, DeviceConfig config) async { + try { + if (!device.btDevice.isConnected) { + await device.btDevice.connect(license: License.free, timeout: const Duration(seconds: 10)); } - List services = await ltDevice.btDevice - .discoverServices(); - var service = services.firstWhere( - (s) => s.uuid == Guid(LasertagUUIDs.provService), - ); - var characteristic = service.characteristics.firstWhere( - (c) => c.uuid == Guid(LasertagUUIDs.provNameChar), - ); - - List value = await characteristic.read(); - String hardwareName = utf8.decode(value); - - updateDeviceName(ltDevice.id, hardwareName, verified: true); - await ltDevice.btDevice.disconnect(); - - return hardwareName; - } catch (e) { - debugPrint("Fehler beim Lesen der Hardware: $e"); - rethrow; - } + // MTU nur auf Android anfordern, auf macOS/iOS macht das das OS automatisch + if (Platform.isAndroid) { + try { + await device.btDevice.requestMtu(250); + // await Future.delayed(const Duration(milliseconds: 200)); + } catch (e) { + debugPrint("MTU Request failed (Android only): $e"); + } + } + + List srv = await device.btDevice.discoverServices(); + var c = srv.firstWhere((s) => s.uuid == Guid(LasertagUUIDs.provService)) + .characteristics.firstWhere((c) => c.uuid == Guid(LasertagUUIDs.provConfigChar)); + await c.write(config.toBytes(), allowLongWrite: true); + updateDeviceName(device.id, config.nodeName, verified: true); + } finally { await device.btDevice.disconnect(); } } - // Öffentlicher API-Entry: Namen auf Hardware schreiben - Future updateDeviceNameOnHardware( - LasertagDevice ltDevice, - String newName, - ) async { - try { - await ltDevice.btDevice.connect( - license: License.free, - autoConnect: false, - timeout: const Duration(seconds: 10), - ); - - await Future.delayed(const Duration(milliseconds: 500)); - - List services = await ltDevice.btDevice - .discoverServices(); - var service = services.firstWhere( - (s) => s.uuid == Guid(LasertagUUIDs.provService), - ); - var characteristic = service.characteristics.firstWhere( - (c) => c.uuid == Guid(LasertagUUIDs.provNameChar), - ); - - await characteristic.write(utf8.encode(newName)); - - updateDeviceName(ltDevice.id, newName, verified: true); - - await ltDevice.btDevice.disconnect(); - } catch (e) { - debugPrint("Hardware-Fehler: $e"); - rethrow; - } - } - - // Öffentlicher API-Entry: Lokale Liste aktualisieren + // ... Hilfsmethoden updateDeviceName, _addDeviceToLists, _verifyNameInBackground (mit license!), _sortList etc. bleiben ... void updateDeviceName(String id, String newName, {bool verified = false}) { - _updateDeviceInLists( - id, - (old) => old.copyWith(name: newName, isNameVerified: verified), - ); + _updateDeviceInLists(id, (old) => old.copyWith(name: newName, isNameVerified: verified)); } void _addDeviceToLists(LasertagDevice device) { - List list = device.isLeader - ? _discoveredLeaders - : _discoveredPeripherals; - + List list = device.isLeader ? _discoveredLeaders : _discoveredPeripherals; if (!list.any((d) => d.id == device.id)) { list.add(device); _sortList(list); notifyListeners(); - _verifyNameInBackground(device); // Hintergrund-Check anstoßen + _verifyNameInBackground(device); } } Future _verifyNameInBackground(LasertagDevice device) async { if (device.isNameVerified) return; - try { - await device.btDevice.connect( - license: License.free, - timeout: const Duration(seconds: 5), - ); - - List services = await device.btDevice - .discoverServices(); - var service = services.firstWhere( - (s) => s.uuid == Guid(LasertagUUIDs.provService), - ); - var characteristic = service.characteristics.firstWhere( - (c) => c.uuid == Guid(LasertagUUIDs.provNameChar), - ); - + await device.btDevice.connect(license: License.free, timeout: const Duration(seconds: 5)); + List services = await device.btDevice.discoverServices(); + var characteristic = services.firstWhere((s) => s.uuid == Guid(LasertagUUIDs.provService)) + .characteristics.firstWhere((c) => c.uuid == Guid(LasertagUUIDs.provNameChar)); List value = await characteristic.read(); - String realName = utf8.decode(value); - - updateDeviceName(device.id, realName, verified: true); - - await device.btDevice.disconnect(); + updateDeviceName(device.id, utf8.decode(value), verified: true); } catch (e) { - debugPrint("Background Sync fehlgeschlagen für ${device.id}: $e"); - } + debugPrint("Background Sync fehlgeschlagen für ${device.id}"); + } finally { await device.btDevice.disconnect(); } } - void _updateDeviceInLists( - String id, - LasertagDevice Function(LasertagDevice) updateFn, - ) { + void _updateDeviceInLists(String id, LasertagDevice Function(LasertagDevice) updateFn) { for (var list in [_discoveredLeaders, _discoveredPeripherals]) { int index = list.indexWhere((d) => d.id == id); if (index != -1) { @@ -193,130 +152,6 @@ class DeviceProvider extends ChangeNotifier { } } - Future provisionLeader( - LasertagDevice device, - Map config, - ) async { - try { - if (!device.btDevice.isConnected) { - await device.btDevice.connect( - license: License.free, - timeout: const Duration(seconds: 10), - ); - } - - List services = await device.btDevice - .discoverServices(); - var s = services.firstWhere( - (s) => s.uuid == Guid(LasertagUUIDs.provService), - ); - - // Hilfsfunktion zum Finden einer Charakteristik - BluetoothCharacteristic findChar(String uuid) => - s.characteristics.firstWhere((c) => c.uuid == Guid(uuid)); - - // 1. Namen schreiben - await findChar( - LasertagUUIDs.provNameChar, - ).write(utf8.encode(config['name'])); - await findChar( - LasertagUUIDs.provNetNameChar, - ).write(utf8.encode(config['netName'])); - - // 2. Kanal (8-bit) - await findChar(LasertagUUIDs.provChanChar).write([config['chan']]); - - // 3. PAN ID (16-bit little endian für Zephyr) - final panData = ByteData(2)..setUint16(0, config['panId'], Endian.little); - await findChar( - LasertagUUIDs.provPanIdChar, - ).write(panData.buffer.asUint8List()); - - // 4. Hex-Werte (ExtPAN 8 Bytes, NetKey 16 Bytes) - await findChar( - LasertagUUIDs.provExtPanIdChar, - ).write(_hexToBytes(config['extPan'])); - await findChar( - LasertagUUIDs.provNetKeyChar, - ).write(_hexToBytes(config['netKey'])); - - // Lokalen Namen in der App-Liste aktualisieren - updateDeviceName(device.id, config['name'], verified: true); - - await device.btDevice.disconnect(); - } catch (e) { - debugPrint("Provisionierungs-Fehler: $e"); - rethrow; - } - } - - // Hilfsmethode: Wandelt "DEADBEEF" in [0xDE, 0xAD, 0xBE, 0xEF] um - List _hexToBytes(String hex) { - List bytes = []; - for (int i = 0; i < hex.length; i += 2) { - bytes.add(int.parse(hex.substring(i, i + 2), radix: 16)); - } - return bytes; - } - - Future> readFullLeaderConfig( - LasertagDevice ltDevice, - ) async { - try { - if (!ltDevice.btDevice.isConnected) { - await ltDevice.btDevice.connect( - license: License.free, - timeout: const Duration(seconds: 10), - ); - } - - List services = await ltDevice.btDevice - .discoverServices(); - var s = services.firstWhere( - (s) => s.uuid == Guid(LasertagUUIDs.provService), - ); - - BluetoothCharacteristic findChar(String uuid) => - s.characteristics.firstWhere((c) => c.uuid == Guid(uuid)); - - // Alle Daten von der Hardware abfragen - final nameBytes = await findChar(LasertagUUIDs.provNameChar).read(); - final netNameBytes = await findChar(LasertagUUIDs.provNetNameChar).read(); - final chanBytes = await findChar(LasertagUUIDs.provChanChar).read(); - final panBytes = await findChar(LasertagUUIDs.provPanIdChar).read(); - final extPanBytes = await findChar(LasertagUUIDs.provExtPanIdChar).read(); - final netKeyBytes = await findChar(LasertagUUIDs.provNetKeyChar).read(); - - // Bytes in passende Dart-Typen umwandeln - return { - 'name': utf8.decode(nameBytes).trim(), - 'netName': utf8.decode(netNameBytes).trim(), - 'chan': chanBytes.isNotEmpty ? chanBytes[0] : 15, - 'panId': panBytes.length >= 2 - ? ByteData.sublistView( - Uint8List.fromList(panBytes), - ).getUint16(0, Endian.little) - : 0xABCD, - 'extPan': _bytesToHex(extPanBytes), - 'netKey': _bytesToHex(netKeyBytes), - }; - } catch (e) { - debugPrint("Fehler beim Laden der Leader-Config: $e"); - rethrow; - } finally { - // Verbindung nach dem Einlesen wieder trennen - await ltDevice.btDevice.disconnect(); - } - } - - // Hilfsmethode: Bytes in Hex-String umwandeln - String _bytesToHex(List bytes) { - return bytes - .map((b) => b.toRadixString(16).padLeft(2, '0')) - .join() - .toUpperCase(); - } - void _sortList(List list) { list.sort((a, b) { int typeComp = a.type.compareTo(b.type); @@ -330,4 +165,4 @@ class DeviceProvider extends ChangeNotifier { _scanSubscription?.cancel(); super.dispose(); } -} +} \ No newline at end of file diff --git a/software/app/lib/ui/screens/device_selection_screen.dart b/software/app/lib/ui/screens/device_selection_screen.dart index 99247e2..9828395 100644 --- a/software/app/lib/ui/screens/device_selection_screen.dart +++ b/software/app/lib/ui/screens/device_selection_screen.dart @@ -86,9 +86,9 @@ class _DeviceSelectionScreenState extends State { device.name, style: TextStyle( fontWeight: FontWeight.bold, - // fontStyle: device.isNameVerified - // ? FontStyle.normal - // : FontStyle.italic, // Kursiv, wenn nicht verifiziert + fontStyle: device.isNameVerified + ? FontStyle.normal + : FontStyle.italic, // Kursiv, wenn nicht verifiziert color: device.isNameVerified ? Theme.of(context).colorScheme.onSurface : Theme.of( diff --git a/software/app/lib/ui/screens/leader_config_screen.dart b/software/app/lib/ui/screens/leader_config_screen.dart index fe5cb25..635c35b 100644 --- a/software/app/lib/ui/screens/leader_config_screen.dart +++ b/software/app/lib/ui/screens/leader_config_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'dart:math'; import '../../models/device_model.dart'; +import '../../models/device_config_model.dart'; import '../../providers/device_provider.dart'; class LeaderConfigScreen extends StatefulWidget { @@ -26,48 +27,42 @@ class _LeaderConfigScreenState extends State { bool _isLoading = true; bool _isSaving = false; + // Speichert die aktuelle Konfiguration der Hardware (für RAM-Werte wie Game-ID) + late DeviceConfig _currentConfig; + @override void initState() { super.initState(); _loadCurrentConfig(); } + /// Lädt die kompakte Konfiguration über die neue 0x100C Charakteristik Future _loadCurrentConfig() async { try { - final config = await context.read().readFullLeaderConfig(widget.device); + final config = await context.read().readDeviceConfig(widget.device); + _currentConfig = config; + if (mounted) { setState(() { - _nameCtrl.text = config['name']; - _netNameCtrl.text = config['netName']; - _selectedChannel = config['chan']; - _panCtrl.text = "0x${config['panId'].toRadixString(16).toUpperCase()}"; - _extPanCtrl.text = config['extPan']; - _netKeyCtrl.text = config['netKey']; + _nameCtrl.text = config.nodeName; + _netNameCtrl.text = config.networkName; + _selectedChannel = config.channel; + _panCtrl.text = "0x${config.panId.toRadixString(16).toUpperCase()}"; + _extPanCtrl.text = config.extPanId; + _netKeyCtrl.text = config.networkKey; _isLoading = false; }); } } catch (e) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Fehler: $e"))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Fehler beim Laden: $e"), backgroundColor: Colors.red), + ); Navigator.pop(context); } } } - // Korrektur 1: Die fehlende Hilfsmethode hinzufügen - Widget _buildSectionTitle(String title) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Text( - title, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontWeight: FontWeight.bold - ) - ), - ); - } - String? _validateHex(String? value, int requiredLength, String fieldName, {bool with0x = false}) { if (value == null || value.isEmpty) return "$fieldName darf nicht leer sein"; @@ -86,7 +81,16 @@ class _LeaderConfigScreenState extends State { return Scaffold( appBar: AppBar(title: const Text("Leader-Konfiguration")), body: _isLoading - ? const Center(child: CircularProgressIndicator()) + ? const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(), + SizedBox(height: 16), + Text("Lade Konfiguration via BLE..."), + ], + ), + ) : Form( key: _formKey, autovalidateMode: AutovalidateMode.onUserInteraction, @@ -99,22 +103,24 @@ class _LeaderConfigScreenState extends State { decoration: const InputDecoration(labelText: "Knoten-Name", helperText: "Max. 31 Zeichen"), maxLength: 31, validator: (v) => (v == null || v.isEmpty) ? "Name erforderlich" : null, + enabled: !_isSaving, ), TextFormField( controller: _netNameCtrl, decoration: const InputDecoration(labelText: "Thread Netzwerk-Name", helperText: "1-16 Zeichen"), maxLength: 16, - // Korrektur 2: .isEmpty statt .length < 1 nutzen validator: (v) => (v == null || v.isEmpty || v.length > 16) ? "1-16 Zeichen erlaubt" : null, + enabled: !_isSaving, ), DropdownButtonFormField( - // Korrektur 3: initialValue statt value nutzen (neue Flutter Regel) initialValue: _selectedChannel, decoration: const InputDecoration(labelText: "Thread-Kanal (11-26)"), - items: List.generate(16, (i) => 11 + i).map((ch) => DropdownMenuItem(value: ch, child: Text("Kanal $ch"))).toList(), - onChanged: (v) => setState(() => _selectedChannel = v!), + items: List.generate(16, (i) => 11 + i) + .map((ch) => DropdownMenuItem(value: ch, child: Text("Kanal $ch"))) + .toList(), + onChanged: _isSaving ? null : (v) => setState(() => _selectedChannel = v!), ), const SizedBox(height: 24), @@ -132,16 +138,32 @@ class _LeaderConfigScreenState extends State { ); } + Widget _buildSectionTitle(String title) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + title, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold + ) + ), + ); + } + Widget _buildValidatedHexField(String label, TextEditingController ctrl, int len, bool with0x) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: TextFormField( controller: ctrl, + enabled: !_isSaving, decoration: InputDecoration( labelText: label, suffixIcon: IconButton( icon: const Icon(Icons.refresh), - onPressed: () => setState(() => ctrl.text = (with0x ? "0x" : "") + _generateRandomHex(len ~/ 2)), + onPressed: _isSaving ? null : () => setState(() { + ctrl.text = (with0x ? "0x" : "") + _generateRandomHex(len ~/ 2); + }), ), ), inputFormatters: [ @@ -155,36 +177,58 @@ class _LeaderConfigScreenState extends State { Widget _buildActionButtons() { return Row( children: [ - Expanded(child: OutlinedButton(onPressed: () => Navigator.pop(context), child: const Text("Abbrechen"))), + Expanded( + child: OutlinedButton( + onPressed: _isSaving ? null : () => Navigator.pop(context), + child: const Text("Abbrechen"), + ), + ), const SizedBox(width: 16), Expanded( child: ElevatedButton( onPressed: _isSaving ? null : _startProvisioning, - child: _isSaving ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) : const Text("Lobby"), + child: _isSaving + ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) + : const Text("Lobby"), ), ), ], ); } + /// Erstellt das neue DeviceConfig Objekt und sendet es als Block an die Hardware Future _startProvisioning() async { if (!_formKey.currentState!.validate()) return; setState(() => _isSaving = true); try { - final config = { - 'name': _nameCtrl.text.trim(), - 'netName': _netNameCtrl.text.trim(), - 'chan': _selectedChannel, - 'panId': int.parse(_panCtrl.text.replaceFirst('0x', ''), radix: 16), - 'extPan': _extPanCtrl.text.trim(), - 'netKey': _netKeyCtrl.text.trim(), - }; + // Neues Model-Objekt erstellen + final newConfig = DeviceConfig( + systemState: _currentConfig.systemState, // Unverändert übernehmen + gameId: _currentConfig.gameId, // Unverändert übernehmen + nodeName: _nameCtrl.text.trim(), + networkName: _netNameCtrl.text.trim(), + channel: _selectedChannel, + panId: int.parse(_panCtrl.text.replaceFirst('0x', ''), radix: 16), + extPanId: _extPanCtrl.text.trim(), + networkKey: _netKeyCtrl.text.trim(), + ); - await context.read().provisionLeader(widget.device, config); - if (mounted) Navigator.pop(context); + // Optimierte Methode im Provider aufrufen + await context.read().provisionDevice(widget.device, newConfig); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("Konfiguration erfolgreich übertragen!")), + ); + Navigator.pop(context); + } } catch (e) { - if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Fehler: $e"), backgroundColor: Colors.red)); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("Fehler beim Speichern: $e"), backgroundColor: Colors.red), + ); + } } finally { if (mounted) setState(() => _isSaving = false); } @@ -192,7 +236,9 @@ class _LeaderConfigScreenState extends State { String _generateRandomHex(int bytes) { final rand = Random(); - return List.generate(bytes, (_) => rand.nextInt(256).toRadixString(16).padLeft(2, '0')).join().toUpperCase(); + return List.generate(bytes, (_) => rand.nextInt(256).toRadixString(16).padLeft(2, '0')) + .join() + .toUpperCase(); } @override