Umbau Thread-Config an/vom leader in einem paket
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
All checks were successful
Deploy Docs / build-and-deploy (push) Successful in 12s
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
@@ -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 <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <zephyr/bluetooth/bluetooth.h>
|
||||
@@ -9,47 +20,104 @@
|
||||
#include <lasertag_utils.h>
|
||||
#include <thread_mgmt.h>
|
||||
#include <ble_mgmt.h>
|
||||
#include <string.h>
|
||||
|
||||
LOG_MODULE_REGISTER(ble_mgmt, CONFIG_BLE_MGMT_LOG_LEVEL);
|
||||
|
||||
/**
|
||||
* Base UUID: 03afe2cf-6c64-4a22-9289-c3ae820cXXXX
|
||||
* Alias positions: Byte 12 & 13
|
||||
*/
|
||||
/* ============================================================================
|
||||
UUID Definitions
|
||||
============================================================================
|
||||
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,
|
||||
|
||||
3
software/app/devtools_options.yaml
Normal file
3
software/app/devtools_options.yaml
Normal file
@@ -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:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
87
software/app/lib/models/device_config_model.dart
Normal file
87
software/app/lib/models/device_config_model.dart
Normal file
@@ -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<int> 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<int> 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<int> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<LasertagDevice> _discoveredLeaders = [];
|
||||
@@ -16,172 +18,129 @@ class DeviceProvider extends ChangeNotifier {
|
||||
List<LasertagDevice> get leaders => _discoveredLeaders;
|
||||
List<LasertagDevice> 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<String> 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<BluetoothService> srv = await ltDevice.btDevice.discoverServices();
|
||||
var c = srv.firstWhere((s) => s.uuid == Guid(LasertagUUIDs.provService))
|
||||
.characteristics.firstWhere((c) => c.uuid == Guid(LasertagUUIDs.provNameChar));
|
||||
List<int> value = await c.read();
|
||||
String hardwareName = utf8.decode(value);
|
||||
updateDeviceName(ltDevice.id, hardwareName, verified: true);
|
||||
return hardwareName;
|
||||
} finally { await ltDevice.btDevice.disconnect(); }
|
||||
}
|
||||
|
||||
Future<void> updateDeviceNameOnHardware(LasertagDevice ltDevice, String newName) async {
|
||||
try {
|
||||
await ltDevice.btDevice.connect(license: License.free, timeout: const Duration(seconds: 10));
|
||||
List<BluetoothService> 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<DeviceConfig> readDeviceConfig(LasertagDevice device) async {
|
||||
try {
|
||||
if (!device.btDevice.isConnected) {
|
||||
await device.btDevice.connect(license: License.free, timeout: const Duration(seconds: 10));
|
||||
}
|
||||
List<BluetoothService> 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<void> provisionDevice(LasertagDevice device, DeviceConfig config) async {
|
||||
try {
|
||||
if (!device.btDevice.isConnected) {
|
||||
await device.btDevice.connect(license: License.free, timeout: const Duration(seconds: 10));
|
||||
}
|
||||
|
||||
List<BluetoothService> 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<int> 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<BluetoothService> 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<void> 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<BluetoothService> 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<LasertagDevice> list = device.isLeader
|
||||
? _discoveredLeaders
|
||||
: _discoveredPeripherals;
|
||||
|
||||
List<LasertagDevice> 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<void> _verifyNameInBackground(LasertagDevice device) async {
|
||||
if (device.isNameVerified) return;
|
||||
|
||||
try {
|
||||
await device.btDevice.connect(
|
||||
license: License.free,
|
||||
timeout: const Duration(seconds: 5),
|
||||
);
|
||||
|
||||
List<BluetoothService> 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<BluetoothService> services = await device.btDevice.discoverServices();
|
||||
var characteristic = services.firstWhere((s) => s.uuid == Guid(LasertagUUIDs.provService))
|
||||
.characteristics.firstWhere((c) => c.uuid == Guid(LasertagUUIDs.provNameChar));
|
||||
List<int> 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<void> provisionLeader(
|
||||
LasertagDevice device,
|
||||
Map<String, dynamic> config,
|
||||
) async {
|
||||
try {
|
||||
if (!device.btDevice.isConnected) {
|
||||
await device.btDevice.connect(
|
||||
license: License.free,
|
||||
timeout: const Duration(seconds: 10),
|
||||
);
|
||||
}
|
||||
|
||||
List<BluetoothService> 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<int> _hexToBytes(String hex) {
|
||||
List<int> bytes = [];
|
||||
for (int i = 0; i < hex.length; i += 2) {
|
||||
bytes.add(int.parse(hex.substring(i, i + 2), radix: 16));
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> readFullLeaderConfig(
|
||||
LasertagDevice ltDevice,
|
||||
) async {
|
||||
try {
|
||||
if (!ltDevice.btDevice.isConnected) {
|
||||
await ltDevice.btDevice.connect(
|
||||
license: License.free,
|
||||
timeout: const Duration(seconds: 10),
|
||||
);
|
||||
}
|
||||
|
||||
List<BluetoothService> 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<int> bytes) {
|
||||
return bytes
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join()
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
void _sortList(List<LasertagDevice> list) {
|
||||
list.sort((a, b) {
|
||||
int typeComp = a.type.compareTo(b.type);
|
||||
@@ -330,4 +165,4 @@ class DeviceProvider extends ChangeNotifier {
|
||||
_scanSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,9 +86,9 @@ class _DeviceSelectionScreenState extends State<DeviceSelectionScreen> {
|
||||
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(
|
||||
|
||||
@@ -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<LeaderConfigScreen> {
|
||||
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<void> _loadCurrentConfig() async {
|
||||
try {
|
||||
final config = await context.read<DeviceProvider>().readFullLeaderConfig(widget.device);
|
||||
final config = await context.read<DeviceProvider>().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<LeaderConfigScreen> {
|
||||
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<LeaderConfigScreen> {
|
||||
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<int>(
|
||||
// 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<LeaderConfigScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
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<LeaderConfigScreen> {
|
||||
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<void> _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<DeviceProvider>().provisionLeader(widget.device, config);
|
||||
if (mounted) Navigator.pop(context);
|
||||
// Optimierte Methode im Provider aufrufen
|
||||
await context.read<DeviceProvider>().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<LeaderConfigScreen> {
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user