Compare commits

11 Commits

Author SHA1 Message Date
e74437a846 sync 2026-05-26 17:22:30 +02:00
2d3ea34603 sync 2026-05-26 17:19:45 +02:00
87cba0b419 Sync 2026-05-26 14:23:19 +02:00
52bab32309 Battery measurement, basic version 2026-05-19 17:08:00 +02:00
dd51f45084 moved SETTINGS_RUNTIME to lib 2026-05-19 08:51:37 +02:00
980e23e51a Bluetooth funktioniert, programming ohne flash erase tut 2026-05-19 08:12:04 +02:00
5557c78179 sync 2026-05-19 07:58:11 +02:00
32fe244fcf sync 2026-05-11 19:28:06 +02:00
5ae098962d feat: add settings mgmt and split debug transport snippets 2026-05-11 16:19:37 +02:00
3fed249430 fix: add full_name field to board.yml for NCS 3.3.0 schema compliance 2026-05-11 14:26:36 +02:00
7381173026 Merge buzzy_integration into main 2026-05-11 14:02:07 +02:00
53 changed files with 2100 additions and 423 deletions

12
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "nrf-connect",
"request": "launch",
"name": "Launch firmware/build_nrf52840dk_debug",
"config": "${workspaceFolder}/firmware/build_nrf52840dk_debug",
"runToEntryPoint": "main"
}
]
}

View File

@@ -6,5 +6,8 @@
"nrf-connect.boardRoots": [ "nrf-connect.boardRoots": [
"${workspaceFolder}/firmware" "${workspaceFolder}/firmware"
], ],
"cmake.sourceDirectory": "C:/Projekte/buzzer_2/firmware/libs/ble_mgmt" "cmake.sourceDirectory": "C:/Projekte/buzzer_2/firmware/libs/ble_mgmt",
"nrf-connect.debugging.bindings": {
"${workspaceFolder}/firmware/build_nrf52840dk_debug": "Launch firmware/build_nrf52840dk_debug"
}
} }

View File

@@ -1,6 +1,6 @@
VERSION_MAJOR = 0 VERSION_MAJOR = 0
VERSION_MINOR = 0 VERSION_MINOR = 0
PATCHLEVEL = 6 PATCHLEVEL = 77
VERSION_TWEAK = 0 VERSION_TWEAK = 0
#if (IS_ENABLED(CONFIG_LOG)) #if (IS_ENABLED(CONFIG_LOG))
EXTRAVERSION = debug EXTRAVERSION = debug

View File

@@ -1,11 +1,2 @@
### Console / Logging: RTT # Keep QSPI NOR page layout aligned with generated LittleFS block size (4KB).
CONFIG_USE_SEGGER_RTT=y CONFIG_NORDIC_QSPI_NOR_FLASH_LAYOUT_PAGE_SIZE=4096
CONFIG_CONSOLE=y
CONFIG_RTT_CONSOLE=y
CONFIG_UART_CONSOLE=n
CONFIG_LOG_BACKEND_UART=n
CONFIG_LOG_BACKEND_RTT=y
# Keep SPI NOR page layout aligned with generated LittleFS block size (4KB).
CONFIG_SPI_NOR_FLASH_LAYOUT_PAGE_SIZE=4096

View File

@@ -1,5 +1,6 @@
board: board:
name: buzzy name: buzzy
full_name: Buzzy
vendor: iten vendor: iten
socs: socs:
- name: nrf52840 - name: nrf52840

View File

@@ -18,47 +18,47 @@
}; };
}; };
spi3_default: spi3_default { // spi3_default: spi3_default {
group1 { // group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 2)>, // psels = <NRF_PSEL(SPIM_SCK, 0, 2)>,
<NRF_PSEL(SPIM_MOSI, 0, 29)>, // <NRF_PSEL(SPIM_MOSI, 0, 29)>,
<NRF_PSEL(SPIM_MISO, 0, 30)>; // <NRF_PSEL(SPIM_MISO, 0, 30)>;
}; // };
}; // };
spi3_sleep: spi3_sleep { // spi3_sleep: spi3_sleep {
group1 { // group1 {
psels = <NRF_PSEL(SPIM_SCK, 0, 2)>, // psels = <NRF_PSEL(SPIM_SCK, 0, 2)>,
<NRF_PSEL(SPIM_MOSI, 0, 29)>, // <NRF_PSEL(SPIM_MOSI, 0, 29)>,
<NRF_PSEL(SPIM_MISO, 0, 30)>; // <NRF_PSEL(SPIM_MISO, 0, 30)>;
low-power-enable; // low-power-enable;
}; // };
}; // };
/* /*
* Optional future QSPI pinctrl states (keep disabled for now). * Optional future QSPI pinctrl states (keep disabled for now).
* Use these when switching from &spi3 to &qspi in buzzy.dts. * Use these when switching from &spi3 to &qspi in buzzy.dts.
*/ */
// qspi_default: qspi_default { qspi_default: qspi_default {
// group1 { group1 {
// psels = <NRF_PSEL(QSPI_SCK, 0, 2)>, psels = <NRF_PSEL(QSPI_SCK, 0, 2)>,
// <NRF_PSEL(QSPI_CSN, 0, 5)>, <NRF_PSEL(QSPI_CSN, 0, 5)>,
// <NRF_PSEL(QSPI_IO0, 0, 29)>, <NRF_PSEL(QSPI_IO0, 0, 29)>,
// <NRF_PSEL(QSPI_IO1, 0, 30)>, <NRF_PSEL(QSPI_IO1, 0, 30)>,
// <NRF_PSEL(QSPI_IO2, 0, 31)>, <NRF_PSEL(QSPI_IO2, 0, 31)>,
// <NRF_PSEL(QSPI_IO3, 1, 13)>; <NRF_PSEL(QSPI_IO3, 1, 13)>;
// }; };
// }; };
// qspi_sleep: qspi_sleep { qspi_sleep: qspi_sleep {
// group1 { group1 {
// psels = <NRF_PSEL(QSPI_SCK, 0, 2)>, psels = <NRF_PSEL(QSPI_SCK, 0, 2)>,
// <NRF_PSEL(QSPI_CSN, 0, 5)>, <NRF_PSEL(QSPI_CSN, 0, 5)>,
// <NRF_PSEL(QSPI_IO0, 0, 29)>, <NRF_PSEL(QSPI_IO0, 0, 29)>,
// <NRF_PSEL(QSPI_IO1, 0, 30)>, <NRF_PSEL(QSPI_IO1, 0, 30)>,
// <NRF_PSEL(QSPI_IO2, 0, 31)>, <NRF_PSEL(QSPI_IO2, 0, 31)>,
// <NRF_PSEL(QSPI_IO3, 1, 13)>; <NRF_PSEL(QSPI_IO3, 1, 13)>;
// low-power-enable; low-power-enable;
// }; };
// }; };
}; };

View File

@@ -54,7 +54,7 @@
charger_status { charger_status {
compatible = "gpio-keys"; compatible = "gpio-keys";
chg_status: chg_status { chg_status: chg_status {
gpios = <&gpio0 13 GPIO_ACTIVE_HIGH>; gpios = <&gpio0 13 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>;
label = "ETA6003_CHG_STATUS"; label = "ETA6003_CHG_STATUS";
}; };
}; };
@@ -137,17 +137,52 @@
* SO/SIO1-> P0.30 * SO/SIO1-> P0.30
* CS -> P0.05 * CS -> P0.05
*/ */
&spi3 { // &spi3 {
// status = "okay";
// pinctrl-0 = <&spi3_default>;
// pinctrl-1 = <&spi3_sleep>;
// pinctrl-names = "default", "sleep";
// cs-gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
// mx25r64: flash@0 {
// compatible = "jedec,spi-nor";
// reg = <0>;
// spi-max-frequency = <8000000>;
// jedec-id = [c2 28 17];
// size = <DT_SIZE_M(64)>;
// has-dpd;
// t-enter-dpd = <10000>;
// t-exit-dpd = <35000>;
// partitions {
// compatible = "fixed-partitions";
// #address-cells = <1>;
// #size-cells = <1>;
// ext_flash_lfs: partition@0 {
// label = "ext-littlefs";
// reg = <0x00000000 DT_SIZE_M(8)>;
// };
// };
// };
// };
/*
* Optional future QSPI variant (keep disabled for now):
* - Disable &spi3 block above.
* - Enable &qspi block below.
* - Keep the same flash partition layout.
*/
&qspi {
status = "okay"; status = "okay";
pinctrl-0 = <&spi3_default>; pinctrl-0 = <&qspi_default>;
pinctrl-1 = <&spi3_sleep>; pinctrl-1 = <&qspi_sleep>;
pinctrl-names = "default", "sleep"; pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
mx25r64: flash@0 { mx25r64: flash@0 {
compatible = "jedec,spi-nor"; compatible = "nordic,qspi-nor";
reg = <0>; reg = <0>;
spi-max-frequency = <8000000>; sck-frequency = <32000000>;
jedec-id = [c2 28 17]; jedec-id = [c2 28 17];
size = <DT_SIZE_M(64)>; size = <DT_SIZE_M(64)>;
has-dpd; has-dpd;
@@ -167,47 +202,6 @@
}; };
}; };
/*
* Optional future QSPI variant (keep disabled for now):
* - Disable &spi3 block above.
* - Enable &qspi block below.
* - Keep the same flash partition layout.
*/
// &qspi {
// status = "okay";
// pinctrl-0 = <&qspi_default>;
// pinctrl-1 = <&qspi_sleep>;
// pinctrl-names = "default", "sleep";
// mx25r64: flash@0 {
// compatible = "nordic,qspi-nor";
// reg = <0>;
// jedec-id = [c2 28 17];
// size = <DT_SIZE_M(64)>;
// has-dpd;
// t-enter-dpd = <10000>;
// t-exit-dpd = <35000>;
// /* Net mapping from hardware: *
// * SCK=P0.02, CSN=P0.05, IO0=P0.29, IO1=P0.30, IO2=P0.31, IO3=P1.13
// */
// sck-pin = <2>;
// csn-pins = <5>;
// io-pins = <29>, <30>, <31>, <45>;
// partitions {
// compatible = "fixed-partitions";
// #address-cells = <1>;
// #size-cells = <1>;
// ext_flash_lfs: partition@0 {
// label = "ext-littlefs";
// reg = <0x00000000 DT_SIZE_M(8)>;
// };
// };
// };
// };
&gpio0 { &gpio0 {
status = "okay"; status = "okay";

View File

@@ -1,4 +0,0 @@
### Console / Logging: UART
CONFIG_UART_CONSOLE=y
CONFIG_LOG_BACKEND_UART=y
CONFIG_LOG_BACKEND_RTT=n

View File

@@ -34,3 +34,16 @@
pinctrl-1 = <&i2s0_sleep>; pinctrl-1 = <&i2s0_sleep>;
pinctrl-names = "default", "sleep"; pinctrl-names = "default", "sleep";
}; };
&mx25r64 {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
ext_flash_lfs: partition@0 {
label = "ext-littlefs";
reg = <0x00000000 DT_SIZE_M(8)>;
};
};
};

22
firmware/build_output.txt Normal file
View File

@@ -0,0 +1,22 @@
-- west build: making build dir /Users/edi/nrf_playground/buzzer_2/firmware/build pristine
WARNING: This looks like a fresh build and BOARD is unknown; so it probably won't work. To fix, use --board=<your-board>.
Note: to silence the above message, run 'west config build.board_warn false'
-- west build: generating a build system
Loading Zephyr module(s) (Zephyr base): sysbuild_default
-- Found Python3: /opt/nordic/ncs/toolchains/322ac893fe/opt/python@3.12/bin/python3.12 (found suitable version "3.12.4", minimum required is "3.10") found components: Interpreter
-- Cache files will be written to: /Users/edi/Library/Caches/zephyr
-- Found west (found suitable version "1.4.0", minimum required is "0.14.0")
CMake Error at /opt/nordic/ncs/v3.2.1/zephyr/cmake/modules/extensions.cmake:3518 (message):
BOARD is not being defined on the CMake command-line, in the environment or
by the app.
Call Stack (most recent call first):
/opt/nordic/ncs/v3.2.1/zephyr/cmake/modules/boards.cmake:61 (zephyr_check_cache)
cmake/modules/sysbuild_default.cmake:15 (include)
/opt/nordic/ncs/v3.2.1/zephyr/share/zephyr-package/cmake/ZephyrConfig.cmake:75 (include)
/opt/nordic/ncs/v3.2.1/zephyr/share/zephyr-package/cmake/ZephyrConfig.cmake:92 (include_boilerplate)
/opt/nordic/ncs/v3.2.1/zephyr/share/sysbuild-package/cmake/SysbuildConfig.cmake:8 (include)
template/CMakeLists.txt:10 (find_package)
-- Configuring incomplete, errors occurred!
FATAL ERROR: command exited with status 1: /opt/homebrew/bin/cmake -DWEST_PYTHON=/opt/nordic/ncs/toolchains/322ac893fe/opt/python@3.12/bin/python3.12 -B/Users/edi/nrf_playground/buzzer_2/firmware/build -GNinja -S/opt/nordic/ncs/v3.2.1/zephyr/share/sysbuild -DAPP_DIR:PATH=/Users/edi/nrf_playground/buzzer_2/firmware

View File

@@ -1,8 +1,28 @@
### Logging ### Logging
CONFIG_LOG=y CONFIG_LOG=y
CONFIG_AUDIO_LOG_LEVEL_DBG=y #CONFIG_LOG_MODE_IMMEDIATE=y
CONFIG_DEBUG=y
CONFIG_DEBUG_OPTIMIZATIONS=y CONFIG_DEBUG_OPTIMIZATIONS=y
CONFIG_INIT_STACKS=y CONFIG_INIT_STACKS=y
CONFIG_THREAD_STACK_INFO=y CONFIG_THREAD_STACK_INFO=y
### Lib logging levels
CONFIG_BATT_MGMT_LOG_LEVEL_DBG=y
# CONFIG_BATT_MGMT_BLINK_INTERVAL_LOGGING=y
CONFIG_USB_MGMT_LOG_LEVEL_DBG=y
### Bluetooth
CONFIG_BLE_MGMT=y
CONFIG_BT_LOG_LEVEL_WRN=y
### Audio
CONFIG_BUZZ_AUDIO=n
### Shell features shared by all debug variants
CONFIG_SHELL=y
CONFIG_SHELL_LOG_BACKEND=y
CONFIG_FILE_SYSTEM_SHELL=y
CONFIG_SHELL_STACK_SIZE=2048
CONFIG_FILE_SYSTEM_SHELL_LS_SIZE=y

15
firmware/debug_rtt.conf Normal file
View File

@@ -0,0 +1,15 @@
### Debug transport: RTT
CONFIG_CONSOLE=y
CONFIG_USE_SEGGER_RTT=y
CONFIG_RTT_CONSOLE=y
CONFIG_LOG_BACKEND_RTT=y
### Shell over RTT
CONFIG_SHELL_BACKEND_RTT=y
CONFIG_SHELL_BACKEND_RTT_BUFFER=1
CONFIG_UART_CONSOLE=n
CONFIG_LOG_BACKEND_UART=n
### RTT Buffer
CONFIG_SEGGER_RTT_BUFFER_SIZE_UP=8192

10
firmware/debug_uart.conf Normal file
View File

@@ -0,0 +1,10 @@
### Debug transport: UART
CONFIG_CONSOLE=y
CONFIG_UART_CONSOLE=y
CONFIG_LOG_BACKEND_UART=y
### Shell over UART
CONFIG_SHELL_BACKEND_SERIAL=y
CONFIG_RTT_CONSOLE=n
CONFIG_LOG_BACKEND_RTT=n

View File

@@ -244,8 +244,10 @@ def main() -> None:
if staging_root.exists(): if staging_root.exists():
shutil.rmtree(staging_root) shutil.rmtree(staging_root)
out_sys = staging_root / "lfs" / "sys" # The LittleFS partition is mounted at /lfs in firmware, so image root
out_a = staging_root / "lfs" / "a" # must contain /sys and /a directly (not /lfs/sys and /lfs/a).
out_sys = staging_root / "sys"
out_a = staging_root / "a"
out_sys.mkdir(parents=True, exist_ok=True) out_sys.mkdir(parents=True, exist_ok=True)
out_a.mkdir(parents=True, exist_ok=True) out_a.mkdir(parents=True, exist_ok=True)

View File

@@ -1 +1 @@
nrfutil device --x-ext-mem-config-file "%~dp0buzzy.json" program --firmware "%~dp0lfs_external_flash.hex" --options verify=VERIFY_READ,reset=RESET_SYSTEM nrfutil device --x-ext-mem-config-file "%~dp0buzzy.json" program --firmware "%~dp0lfs_external_flash.hex" --options chip_erase_mode=ERASE_NONE,verify=VERIFY_READ,reset=RESET_SYSTEM,ext_mem_erase_mode=ERASE_RANGES_TOUCHED_BY_FIRMWARE

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env sh #!/usr/bin/env sh
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)" SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
nrfutil device --x-ext-mem-config-file "$SCRIPT_DIR/buzzy.json" program --firmware "$SCRIPT_DIR/lfs_external_flash.hex" --options verify=VERIFY_READ,reset=RESET_SYSTEM nrfutil device --x-ext-mem-config-file "$SCRIPT_DIR/buzzy.json" program --firmware "$SCRIPT_DIR/lfs_external_flash.hex" --options chip_erase_mode=ERASE_NONE,verify=VERIFY_READ,reset=RESET_SYSTEM,ext_mem_erase_mode=ERASE_RANGES_TOUCHED_BY_FIRMWARE

View File

@@ -1,6 +1,9 @@
add_subdirectory(settings_mgmt)
add_subdirectory(fw_mgmt) add_subdirectory(fw_mgmt)
add_subdirectory(fs_mgmt) add_subdirectory(fs_mgmt)
add_subdirectory(ble_mgmt) add_subdirectory(ble_mgmt)
add_subdirectory(buzz_proto) add_subdirectory(buzz_proto)
add_subdirectory(audio) add_subdirectory(audio)
add_subdirectory(event_mgmt) add_subdirectory(event_mgmt)
add_subdirectory(batt_mgmt)
add_subdirectory(usb_mgmt)

View File

@@ -1,6 +1,9 @@
rsource "settings_mgmt/Kconfig"
rsource "fw_mgmt/Kconfig" rsource "fw_mgmt/Kconfig"
rsource "fs_mgmt/Kconfig" rsource "fs_mgmt/Kconfig"
rsource "ble_mgmt/Kconfig" rsource "ble_mgmt/Kconfig"
rsource "buzz_proto/Kconfig" rsource "buzz_proto/Kconfig"
rsource "audio/Kconfig" rsource "audio/Kconfig"
rsource "event_mgmt/Kconfig" rsource "event_mgmt/Kconfig"
rsource "batt_mgmt/Kconfig"
rsource "usb_mgmt/Kconfig"

View File

@@ -89,6 +89,15 @@ static void audio_set_state(enum audio_thread_state_t new_state, const char *rea
{ {
enum audio_thread_state_t old_state = atomic_get(&thread_state); enum audio_thread_state_t old_state = atomic_get(&thread_state);
if ((old_state == AUDIO_IDLE) && (new_state != AUDIO_IDLE))
{
event_mgmt_set_event(EVENT_MGMT_AUDIO_ACTIVE);
}
else if ((old_state != AUDIO_IDLE) && (new_state == AUDIO_IDLE))
{
event_mgmt_set_event(EVENT_MGMT_AUDIO_IDLE);
}
if (old_state != new_state) if (old_state != new_state)
{ {
LOG_INF("Audio state %s -> %s (%s)", LOG_INF("Audio state %s -> %s (%s)",

View File

@@ -0,0 +1,5 @@
if(CONFIG_BATT_MGMT)
zephyr_library()
zephyr_library_sources(src/batt_mgmt.c)
zephyr_include_directories(include)
endif()

View File

@@ -0,0 +1,96 @@
menuconfig BATT_MGMT
bool "Battery Management"
default y
select ADC
select EVENT_MGMT
select GPIO
select USB_MGMT
select NRFX_SAADC
help
Library for initializing and managing the battery subsystem.
if BATT_MGMT
config BATT_MGMT_EMPTY_THRESHOLD
int "Battery Empty Voltage (mV)"
default 3400
help
Voltage threshold treated as empty battery level (0/5).
config BATT_MGMT_SHUTDOWN_THRESHOLD
int "Critical shutdown voltage (mV)"
default 3300
help
If measured battery voltage is less than or equal to this threshold,
the device will power off to protect the battery.
config BATT_MGMT_FULL_THRESHOLD
int "Battery Full Voltage (mV)"
default 3980
help
Set the voltage level (in millivolts) that represents a full battery. Default is 3980 mV.
config BATT_MGMT_80P_THRESHOLD
int "Battery 80% Voltage (mV)"
default 3820
help
Set the voltage level (in millivolts) that represents 80% battery. Default is 3820 mV.
config BATT_MGMT_50P_THRESHOLD
int "Battery 50% Voltage (mV)"
default 3720
help
Set the voltage level (in millivolts) that represents 50% battery. Default is 3720 mV.
config BATT_MGMT_20P_THRESHOLD
int "Battery 20% Voltage (mV)"
default 3620
help
Set the voltage level (in millivolts) that represents 20% battery. Default is 3620 mV.
config BATT_MGMT_CHG_BLINKING_WINDOW_MS
int "Charger blinking detection window (ms)"
default 700
help
Time window to detect charger pin blinking (state changes). If the pin state
changes within this window, it's considered blinking (error state).
config BATT_MGMT_STANDBY_MEASURE_INTERVAL_MIN
int "Standby measurement interval (minutes)"
default 360
help
Measurement interval while idle in standby (no charging, no BLE, no audio).
config BATT_MGMT_ACTIVE_MEASURE_INTERVAL_MIN
int "Active measurement interval (minutes)"
default 1
help
Measurement interval while charging or BLE connected.
config BATT_MGMT_AUDIO_COOLDOWN_SEC
int "Audio cooldown before next measure (seconds)"
default 60
help
Delay before restarting periodic battery measurement after audio playback ends.
config BATT_MGMT_MONITOR_THREAD_STACK_SIZE
int "Battery monitor thread stack size"
default 1024
help
Stack size for the USB-triggered charger status monitor thread.
config BATT_MGMT_MONITOR_THREAD_PRIORITY
int "Battery monitor thread priority"
default 7
help
Cooperative priority for the monitor thread (lower number = higher priority).
config BATT_MGMT_BLINK_INTERVAL_LOGGING
bool "Enable logging of charger pin blinking intervals"
default n
help
If enabled, logs the time intervals between charger pin state changes, which can help diagnose charger connection issues.
module = BATT_MGMT
module-str = batt_mgmt
source "subsys/logging/Kconfig.template.log_config"
endif # BATT_MGMT

View File

@@ -0,0 +1,107 @@
#ifndef BATT_MGMT_H
#define BATT_MGMT_H
#include <zephyr/types.h>
#include <stdbool.h>
#define BATT_MGMT_OVERSAMPLING_8X 8U
#define BATT_MGMT_OVERSAMPLING_16X 16U
typedef enum {
BATT_STATE_DISCHARGING = 0x00,
BATT_STATE_FULL= 0x01,
BATT_STATE_CHARGING = 0x02,
BATT_STATE_ERROR = 0x03,
BATT_STATE_UNKNOWN = 0x04,
} batt_mgmt_state_t;
typedef struct {
batt_mgmt_state_t state;
uint8_t level;
uint8_t percent;
int32_t voltage_mv;
} batt_mgmt_info_t;
/**
* @brief Measure battery VDDH voltage.
*
* @param oversampling Oversampling factor (BATT_MGMT_OVERSAMPLING_8X or BATT_MGMT_OVERSAMPLING_16X).
* @param vddh_mv Pointer to store the result in millivolts.
* @return 0 on success, negative errno on failure.
*/
int batt_mgmt_measure_vddh_mv(uint8_t oversampling, int32_t *vddh_mv);
/**
* @brief Get display voltage in millivolts.
*
* Returns the voltage value intended for UI/protocol display. Currently this is
* the latest measured VDDH value cached by batt_mgmt.
*
* @param vddh_mv Pointer receiving display voltage in mV.
* @return 0 on success, negative errno on failure.
*/
int batt_mgmt_get_display_voltage_mv(int32_t *vddh_mv);
/**
* @brief Get battery level bucket (0..4) from configured thresholds.
*
* Level mapping:
* - 0: below 20% threshold
* - 1: 20%..49%
* - 2: 50%..79%
* - 3: 80%..99%
* - 4: full and above
*
* @param level Pointer receiving level in range 0..4.
* @return 0 on success, negative errno on failure.
*/
int batt_mgmt_get_battery_level(uint8_t *level);
/**
* @brief Get battery percentage (0-100) from configured voltage thresholds.
*
* Uses piecewise linear interpolation over:
* - EMPTY..20%
* - 20%..50%
* - 50%..80%
* - 80%..FULL
*
* @param percent Pointer receiving percentage in range 0..100.
* @return 0 on success, negative errno on failure.
*/
int batt_mgmt_get_battery_percent(uint8_t *percent);
/**
* @brief Get raw charger status (GPIO level).
*
* Returns the current logic level of the charger status pin (typically from ETA6003).
* This is the **raw GPIO status**, not a processed state machine result.
*
* @return true if charger pin is high (e.g., battery is "charging"), false if low.
*/
bool batt_mgmt_get_charger_status(void);
/**
* @brief Get processed battery state with blinking detection.
*
* Returns a derived state based on:
* - USB VBUS presence (from usb_mgmt)
* - Charger status GPIO level (from batt_mgmt_get_charger_status)
* - Blinking detection (state changes within CONFIG_BATT_MGMT_CHG_BLINKING_WINDOW_MS)
*
* Note: This differs from batt_mgmt_get_charger_status() which returns the **raw** GPIO level.
*
* @return Battery state enum (DISCHARGING, FULL, CHARGING, or ERROR if blinking detected).
*/
batt_mgmt_state_t batt_mgmt_get_battery_state(void);
/**
* @brief Get all battery information in one call.
*
* @param info Pointer receiving state, level (0..4), percent (0..100), and voltage in mV.
* @return 0 on success, negative errno on failure.
*/
int batt_mgmt_get_info(batt_mgmt_info_t *info);
#endif

View File

@@ -0,0 +1,591 @@
#include <zephyr/device.h>
#include <zephyr/drivers/adc.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/init.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <nrfx_saadc.h>
#include <errno.h>
#include <stdbool.h>
#include "batt_mgmt.h"
#include "usb_mgmt.h"
LOG_MODULE_REGISTER(batt_mgmt, CONFIG_BATT_MGMT_LOG_LEVEL);
#define BATT_VDDH_DIVIDER_FACTOR 5
#define BATT_VDDH_ADC_NODE DT_PATH(zephyr_user)
static const struct adc_dt_spec batt_vddh_adc = ADC_DT_SPEC_GET_BY_NAME(BATT_VDDH_ADC_NODE, vddh);
#define BATT_CHG_STATUS_NODE DT_ALIAS(chg_status)
static const struct gpio_dt_spec batt_chg_status = GPIO_DT_SPEC_GET(BATT_CHG_STATUS_NODE, gpios);
static bool batt_mgmt_ready;
static int64_t batt_mgmt_last_chg_change_ms;
static int64_t batt_mgmt_last_offset_calib_ms;
static int32_t batt_mgmt_last_vddh_mv;
static bool batt_mgmt_chg_interrupt_enabled;
static K_THREAD_STACK_DEFINE(batt_monitor_stack, CONFIG_BATT_MGMT_MONITOR_THREAD_STACK_SIZE);
static struct k_thread batt_monitor_thread_data;
static struct k_sem batt_mgmt_saadc_calib_sem;
static int batt_mgmt_saadc_calib_rc;
static void batt_mgmt_saadc_calib_handler(nrfx_saadc_evt_t const *p_event)
{
if ((p_event == NULL) || (p_event->type != NRFX_SAADC_EVT_CALIBRATEDONE))
{
batt_mgmt_saadc_calib_rc = -EIO;
}
else
{
batt_mgmt_saadc_calib_rc = 0;
}
k_sem_give(&batt_mgmt_saadc_calib_sem);
}
static int batt_mgmt_calibrate_offset_if_needed(bool force)
{
int rc;
int64_t now_ms = k_uptime_get();
int64_t calib_interval_ms = (int64_t)CONFIG_BATT_MGMT_STANDBY_MEASURE_INTERVAL_MIN * 60 * 1000;
if (!force && (now_ms - batt_mgmt_last_offset_calib_ms < calib_interval_ms))
{
return 0;
}
while (k_sem_take(&batt_mgmt_saadc_calib_sem, K_NO_WAIT) == 0)
{
}
batt_mgmt_saadc_calib_rc = -EIO;
rc = nrfx_saadc_offset_calibrate(batt_mgmt_saadc_calib_handler);
if (rc < 0)
{
return rc;
}
rc = k_sem_take(&batt_mgmt_saadc_calib_sem, K_MSEC(100));
if (rc < 0)
{
return rc;
}
if (batt_mgmt_saadc_calib_rc < 0)
{
return batt_mgmt_saadc_calib_rc;
}
batt_mgmt_last_offset_calib_ms = k_uptime_get();
return 0;
}
static int batt_mgmt_to_adc_oversampling(uint8_t oversampling, uint8_t *adc_oversampling)
{
if (adc_oversampling == NULL)
{
return -EINVAL;
}
switch (oversampling)
{
case BATT_MGMT_OVERSAMPLING_8X:
*adc_oversampling = 3U;
return 0;
case BATT_MGMT_OVERSAMPLING_16X:
*adc_oversampling = 4U;
return 0;
default:
return -EINVAL;
}
}
int batt_mgmt_measure_vddh_mv(uint8_t oversampling, int32_t *vddh_mv)
{
int rc;
uint8_t adc_oversampling;
int16_t raw_sample;
int32_t input_mv;
struct adc_sequence sequence = {
.buffer = &raw_sample,
.buffer_size = sizeof(raw_sample),
};
if (vddh_mv == NULL)
{
return -EINVAL;
}
if (!batt_mgmt_ready)
{
return -EAGAIN;
}
rc = batt_mgmt_calibrate_offset_if_needed(false);
if (rc < 0)
{
LOG_WRN("ADC offset calibration failed before measurement: %d", rc);
}
rc = batt_mgmt_to_adc_oversampling(oversampling, &adc_oversampling);
if (rc < 0)
{
return rc;
}
rc = adc_sequence_init_dt(&batt_vddh_adc, &sequence);
if (rc < 0)
{
return rc;
}
sequence.oversampling = adc_oversampling;
rc = adc_read_dt(&batt_vddh_adc, &sequence);
if (rc < 0)
{
return rc;
}
input_mv = raw_sample;
rc = adc_raw_to_millivolts_dt(&batt_vddh_adc, &input_mv);
if (rc < 0)
{
return rc;
}
*vddh_mv = input_mv * BATT_VDDH_DIVIDER_FACTOR;
batt_mgmt_last_vddh_mv = *vddh_mv;
LOG_DBG("Battery VDDH: %d mV (raw=%d, os=%ux)", *vddh_mv, raw_sample, oversampling);
return 0;
}
static int batt_mgmt_linear_percent(int32_t mv, int32_t lo_mv, int32_t hi_mv,
uint8_t lo_pct, uint8_t hi_pct, uint8_t *out_pct)
{
int32_t num;
int32_t den;
int32_t pct;
if (out_pct == NULL)
{
return -EINVAL;
}
if (hi_mv <= lo_mv)
{
return -EINVAL;
}
if (mv <= lo_mv)
{
*out_pct = lo_pct;
return 0;
}
if (mv >= hi_mv)
{
*out_pct = hi_pct;
return 0;
}
num = (mv - lo_mv) * (int32_t)(hi_pct - lo_pct);
den = hi_mv - lo_mv;
pct = (int32_t)lo_pct + (num / den);
if (pct < 0)
{
pct = 0;
}
else if (pct > 100)
{
pct = 100;
}
*out_pct = (uint8_t)pct;
return 0;
}
int batt_mgmt_get_display_voltage_mv(int32_t *vddh_mv)
{
int rc;
if (vddh_mv == NULL)
{
return -EINVAL;
}
if (!batt_mgmt_ready)
{
return -EAGAIN;
}
if (batt_mgmt_last_vddh_mv <= 0)
{
rc = batt_mgmt_measure_vddh_mv(BATT_MGMT_OVERSAMPLING_16X, &batt_mgmt_last_vddh_mv);
if (rc < 0)
{
return rc;
}
}
*vddh_mv = batt_mgmt_last_vddh_mv;
return 0;
}
int batt_mgmt_get_battery_level(uint8_t *level)
{
int32_t mv;
int rc;
if (level == NULL)
{
return -EINVAL;
}
rc = batt_mgmt_get_display_voltage_mv(&mv);
if (rc < 0)
{
return rc;
}
if (mv >= CONFIG_BATT_MGMT_FULL_THRESHOLD)
{
*level = 4U;
}
else if (mv >= CONFIG_BATT_MGMT_80P_THRESHOLD)
{
*level = 3U;
}
else if (mv >= CONFIG_BATT_MGMT_50P_THRESHOLD)
{
*level = 2U;
}
else if (mv >= CONFIG_BATT_MGMT_20P_THRESHOLD)
{
*level = 1U;
}
else
{
*level = 0U;
}
return 0;
}
int batt_mgmt_get_battery_percent(uint8_t *percent)
{
int32_t mv;
int rc;
if (percent == NULL)
{
return -EINVAL;
}
rc = batt_mgmt_get_display_voltage_mv(&mv);
if (rc < 0)
{
return rc;
}
if (mv <= CONFIG_BATT_MGMT_EMPTY_THRESHOLD)
{
*percent = 0;
return 0;
}
if (mv >= CONFIG_BATT_MGMT_FULL_THRESHOLD)
{
*percent = 100;
return 0;
}
if (mv < CONFIG_BATT_MGMT_20P_THRESHOLD)
{
return batt_mgmt_linear_percent(mv,
CONFIG_BATT_MGMT_EMPTY_THRESHOLD,
CONFIG_BATT_MGMT_20P_THRESHOLD,
0, 20, percent);
}
if (mv < CONFIG_BATT_MGMT_50P_THRESHOLD)
{
return batt_mgmt_linear_percent(mv,
CONFIG_BATT_MGMT_20P_THRESHOLD,
CONFIG_BATT_MGMT_50P_THRESHOLD,
20, 50, percent);
}
if (mv < CONFIG_BATT_MGMT_80P_THRESHOLD)
{
return batt_mgmt_linear_percent(mv,
CONFIG_BATT_MGMT_50P_THRESHOLD,
CONFIG_BATT_MGMT_80P_THRESHOLD,
50, 80, percent);
}
return batt_mgmt_linear_percent(mv,
CONFIG_BATT_MGMT_80P_THRESHOLD,
CONFIG_BATT_MGMT_FULL_THRESHOLD,
80, 100, percent);
}
static void batt_mgmt_chg_interrupt_handler(const struct device *dev,
struct gpio_callback *cb,
uint32_t pins)
{
int64_t now_ms = k_uptime_get();
ARG_UNUSED(dev);
ARG_UNUSED(cb);
ARG_UNUSED(pins);
#ifdef CONFIG_BATT_MGMT_BLINK_INTERVAL_LOGGING
int64_t delta_ms = now_ms - batt_mgmt_last_chg_change_ms;
LOG_DBG("CHG interrupt: delta=%lld ms", delta_ms);
#endif
batt_mgmt_last_chg_change_ms = now_ms;
}
static int batt_mgmt_enable_chg_interrupt(void)
{
int rc;
static struct gpio_callback batt_chg_cb;
if (batt_mgmt_chg_interrupt_enabled)
{
return 0;
}
if (!gpio_is_ready_dt(&batt_chg_status))
{
LOG_ERR("CHG GPIO device not ready");
return -ENODEV;
}
rc = gpio_pin_configure_dt(&batt_chg_status, GPIO_INPUT);
if (rc < 0)
{
LOG_ERR("Failed to configure CHG GPIO: %d", rc);
return rc;
}
gpio_init_callback(&batt_chg_cb, batt_mgmt_chg_interrupt_handler, BIT(batt_chg_status.pin));
rc = gpio_add_callback(batt_chg_status.port, &batt_chg_cb);
if (rc < 0)
{
LOG_ERR("Failed to add CHG callback: %d", rc);
return rc;
}
rc = gpio_pin_interrupt_configure_dt(&batt_chg_status, GPIO_INT_EDGE_BOTH);
if (rc < 0)
{
LOG_ERR("Failed to configure CHG interrupt: %d", rc);
return rc;
}
batt_mgmt_chg_interrupt_enabled = true;
batt_mgmt_last_chg_change_ms = k_uptime_get();
LOG_INF("CHG interrupt enabled");
return 0;
}
static int batt_mgmt_disable_chg_interrupt(void)
{
int rc;
if (!batt_mgmt_chg_interrupt_enabled)
{
return 0;
}
rc = gpio_pin_interrupt_configure_dt(&batt_chg_status, GPIO_INT_DISABLE);
if (rc < 0)
{
LOG_WRN("Failed to disable CHG interrupt: %d", rc);
return rc;
}
rc = gpio_pin_configure(batt_chg_status.port, batt_chg_status.pin, GPIO_DISCONNECTED);
if (rc < 0)
{
LOG_WRN("Failed to disconnect CHG GPIO: %d", rc);
return rc;
}
batt_mgmt_chg_interrupt_enabled = false;
LOG_DBG("CHG interrupt disabled");
return 0;
}
bool batt_mgmt_get_charger_status(void)
{
if (!batt_mgmt_chg_interrupt_enabled)
{
return false;
}
return gpio_pin_get_dt(&batt_chg_status);
}
batt_mgmt_state_t batt_mgmt_get_battery_state(void)
{
int64_t now_ms = k_uptime_get();
int64_t delta_ms = now_ms - batt_mgmt_last_chg_change_ms;
bool chg_high = batt_mgmt_get_charger_status();
bool usb_present = usb_mgmt_is_vbus_present();
/* Blinking detection: state change within window */
if (delta_ms < CONFIG_BATT_MGMT_CHG_BLINKING_WINDOW_MS)
{
LOG_DBG("Blinking detected (delta=%lld ms)", delta_ms);
return BATT_STATE_ERROR;
}
/* Stable states */
if (chg_high)
{
return BATT_STATE_CHARGING;
}
if (usb_present)
{
return BATT_STATE_FULL;
}
return BATT_STATE_DISCHARGING;
}
int batt_mgmt_get_info(batt_mgmt_info_t *info)
{
int rc;
if (info == NULL)
{
return -EINVAL;
}
rc = batt_mgmt_get_display_voltage_mv(&info->voltage_mv);
if (rc < 0)
{
return rc;
}
rc = batt_mgmt_get_battery_level(&info->level);
if (rc < 0)
{
return rc;
}
rc = batt_mgmt_get_battery_percent(&info->percent);
if (rc < 0)
{
return rc;
}
info->state = batt_mgmt_get_battery_state();
return 0;
}
static void batt_mgmt_monitor_thread(void *unused1, void *unused2, void *unused3)
{
(void)unused1;
(void)unused2;
(void)unused3;
if (!usb_mgmt_wait_ready(K_SECONDS(2)))
{
LOG_WRN("usb_mgmt not ready after timeout; using current VBUS state");
}
/* Sync initial state in case VBUS was already present before this thread started. */
bool vbus_present = usb_mgmt_is_vbus_present();
if (vbus_present)
{
(void)batt_mgmt_enable_chg_interrupt();
}
else
{
(void)batt_mgmt_disable_chg_interrupt();
}
while (1)
{
uint32_t events = k_event_wait(&usb_mgmt_events,
USB_MGMT_VBUS_CONNECTED | USB_MGMT_VBUS_DISCONNECTED,
false, K_FOREVER);
if (events & USB_MGMT_VBUS_CONNECTED)
{
(void)batt_mgmt_enable_chg_interrupt();
}
if (events & USB_MGMT_VBUS_DISCONNECTED)
{
(void)batt_mgmt_disable_chg_interrupt();
}
k_event_clear(&usb_mgmt_events, USB_MGMT_VBUS_CONNECTED | USB_MGMT_VBUS_DISCONNECTED);
}
}
static int batt_mgmt_init(void)
{
int32_t boot_vddh_mv;
int rc;
if (!adc_is_ready_dt(&batt_vddh_adc))
{
LOG_ERR("VDDH ADC device not ready");
return -ENODEV;
}
rc = adc_channel_setup_dt(&batt_vddh_adc);
if (rc < 0)
{
LOG_ERR("VDDH ADC channel setup failed: %d", rc);
return rc;
}
k_sem_init(&batt_mgmt_saadc_calib_sem, 0, 1);
batt_mgmt_last_offset_calib_ms = 0;
batt_mgmt_ready = true;
rc = batt_mgmt_calibrate_offset_if_needed(true);
if (rc < 0)
{
LOG_WRN("Initial ADC offset calibration failed: %d", rc);
}
rc = batt_mgmt_measure_vddh_mv(BATT_MGMT_OVERSAMPLING_16X, &boot_vddh_mv);
if (rc < 0)
{
LOG_WRN("Initial battery measurement failed: %d", rc);
return 0;
}
batt_mgmt_last_vddh_mv = boot_vddh_mv;
LOG_DBG("Initial battery VDDH: %d mV", boot_vddh_mv);
k_thread_create(&batt_monitor_thread_data,
batt_monitor_stack,
K_THREAD_STACK_SIZEOF(batt_monitor_stack),
batt_mgmt_monitor_thread,
NULL, NULL, NULL,
K_PRIO_COOP(CONFIG_BATT_MGMT_MONITOR_THREAD_PRIORITY),
0, K_NO_WAIT);
return 0;
}
SYS_INIT(batt_mgmt_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY);

View File

@@ -3,88 +3,39 @@ menuconfig BLE_MGMT
default n default n
select BT select BT
select BT_PERIPHERAL select BT_PERIPHERAL
select BT_LOG_LEVEL_WARN
select BT_DEVICE_NAME_DYNAMIC select BT_DEVICE_NAME_DYNAMIC
help help
Library for initializing and managing Bluetooth functionality. Minimal BLE transport for the buzzer firmware.
if BLE_MGMT if BLE_MGMT
config BLE_MGMT_TX_QUEUE_DEPTH
int "BLE TX queue depth"
default 32
help
Number of notification payloads that can be queued in the BLE transport.
config BLE_MGMT_DEFAULT_DEVICE_NAME config BLE_MGMT_DEFAULT_DEVICE_NAME
string "Default Bluetooth Device Name" string "Default Bluetooth device name"
default "Edis Buzzer" default "Edis Buzzer 2.0"
config BLE_MGMT_ADV_INT_MIN
int "Minimum Advertising Interval (in 0.625 ms units)"
default 160
help help
Minimal advertising interval. 160 equals to 100ms. Device name used when ble_mgmt_init() is called with a NULL name.
config BLE_MGMT_ADV_INT_MAX
int "Maximum Advertising Interval (ms)"
default 160
help
Maximal advertising interval. 160 equals to 100ms.
# Airtime
config BT_CTLR_SDC_MAX_CONN_EVENT_LEN_DEFAULT
default 4000000
# MTU Setup
config BT_BUF_ACL_RX_SIZE
default 502
config BT_BUF_ACL_TX_SIZE
default 502
config BT_L2CAP_TX_MTU
default 498
config BT_CTLR_DATA_LENGTH_MAX
default 251
# Buffers
config BT_BUF_ACL_TX_COUNT
default 15
config BT_L2CAP_TX_BUF_COUNT
default 15
config BT_CONN_TX_MAX
default 15
config BT_CTLR_SDC_TX_PACKET_COUNT
default 15
config BT_CTLR_SDC_RX_PACKET_COUNT
default 15
config BT_BUF_EVT_RX_COUNT
default 16
# Callbacks
config BT_USER_PHY_UPDATE
default y
config BT_USER_DATA_LEN_UPDATE
default y
# Automatic updates
config BT_AUTO_PHY_UPDATE
default y
config BT_AUTO_DATA_LEN_UPDATE
default y
config BT_GAP_AUTO_UPDATE_CONN_PARAMS
default y
# Preferred defaults
config BT_PERIPHERAL_PREF_MIN_INT
default 6
config BT_PERIPHERAL_PREF_MAX_INT
default 40
config BT_PERIPHERAL_PREF_LATENCY
default 0
config BT_PERIPHERAL_PREF_TIMEOUT
default 400
# Connections
config BT_MAX_CONN config BT_MAX_CONN
default 2 default 2
# BLE control/data handling runs in BT RX workqueue. Enforce a larger
# stack while BLE_MGMT is enabled to avoid overflows on connect/file ops.
config BT_RX_STACK_SIZE
range 3072 8192
default 3072
# Use larger BLE data path buffers for file transfer use-cases.
config BT_L2CAP_TX_MTU
default 498
config BT_BUF_ACL_RX_SIZE
default 502
config BT_BUF_ACL_TX_SIZE
default 502
config BT_CTLR_DATA_LENGTH_MAX
default 251
module = BLE_MGMT module = BLE_MGMT
module-str = ble_mgmt module-str = ble_mgmt
source "subsys/logging/Kconfig.template.log_config" source "subsys/logging/Kconfig.template.log_config"

View File

@@ -17,7 +17,8 @@ int ble_mgmt_init(ble_mgmt_rx_cb_t rx_cb, const char *device_name);
* Sends data to the connected central device via a GATT characteristic. * Sends data to the connected central device via a GATT characteristic.
* @param data Pointer to the data buffer to send. * @param data Pointer to the data buffer to send.
* @param len Length of the data in bytes. * @param len Length of the data in bytes.
* @return 0 on success, -EACCES if notifications are not enabled, or a negative error code on failure. * @return 0 on success, -EAGAIN if no ATT link is ready yet, -EACCES if notifications are not enabled,
* or a negative error code on failure.
*/ */
int ble_mgmt_send(const uint8_t *data, uint16_t len); int ble_mgmt_send(const uint8_t *data, uint16_t len);

View File

@@ -1,15 +1,16 @@
#include <zephyr/bluetooth/bluetooth.h> #include <errno.h>
#include <zephyr/bluetooth/hci.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/logging/log.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/bluetooth/conn.h>
#include <string.h> #include <string.h>
#include <zephyr/kernel.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/bluetooth/hci.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/logging/log.h>
#include "ble_mgmt.h" #include "ble_mgmt.h"
#include "event_mgmt.h"
LOG_MODULE_REGISTER(ble_mgmt, CONFIG_BLE_MGMT_LOG_LEVEL); LOG_MODULE_REGISTER(ble_mgmt, CONFIG_BLE_MGMT_LOG_LEVEL);
@@ -20,18 +21,24 @@ LOG_MODULE_REGISTER(ble_mgmt, CONFIG_BLE_MGMT_LOG_LEVEL);
#define BUZZ_TX_UUID_VAL \ #define BUZZ_TX_UUID_VAL \
BT_UUID_128_ENCODE(0xe517d988, 0xbab5, 0x4574, 0x8479, 0x97c6cb115ca2) BT_UUID_128_ENCODE(0xe517d988, 0xbab5, 0x4574, 0x8479, 0x97c6cb115ca2)
#define DEFAULT_ATT_MTU 23U
#define GATT_NOTIFY_OVERHEAD 3U
#define MAX_ADV_NAME_LEN 29
struct ble_peer {
struct bt_conn *conn;
};
static struct bt_uuid_128 buzz_service_uuid = BT_UUID_INIT_128(BUZZ_SERVICE_UUID_VAL); static struct bt_uuid_128 buzz_service_uuid = BT_UUID_INIT_128(BUZZ_SERVICE_UUID_VAL);
static struct bt_uuid_128 buzz_rx_uuid = BT_UUID_INIT_128(BUZZ_RX_UUID_VAL); static struct bt_uuid_128 buzz_rx_uuid = BT_UUID_INIT_128(BUZZ_RX_UUID_VAL);
static struct bt_uuid_128 buzz_tx_uuid = BT_UUID_INIT_128(BUZZ_TX_UUID_VAL); static struct bt_uuid_128 buzz_tx_uuid = BT_UUID_INIT_128(BUZZ_TX_UUID_VAL);
static ble_mgmt_rx_cb_t app_rx_cb = NULL; static ble_mgmt_rx_cb_t app_rx_cb;
static bool notify_enabled = false; static bool ble_ready;
static bool advertising_active;
static uint16_t current_tx_mtu = 23; static uint16_t current_att_mtu = DEFAULT_ATT_MTU;
static uint16_t current_rx_mtu = 23;
#define MAX_ADV_NAME_LEN 29
static char current_device_name[MAX_ADV_NAME_LEN + 1]; static char current_device_name[MAX_ADV_NAME_LEN + 1];
static struct ble_peer peers[CONFIG_BT_MAX_CONN];
static const struct bt_data ad[] = { 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_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
@@ -44,38 +51,155 @@ static struct bt_data sd[] = {
static struct bt_le_adv_param adv_param = { static struct bt_le_adv_param adv_param = {
.id = BT_ID_DEFAULT, .id = BT_ID_DEFAULT,
.sid = 0,
.secondary_max_skip = 0,
.options = BT_LE_ADV_OPT_CONN, .options = BT_LE_ADV_OPT_CONN,
.interval_min = CONFIG_BLE_MGMT_ADV_INT_MIN, .interval_min = BT_GAP_ADV_FAST_INT_MIN_2,
.interval_max = CONFIG_BLE_MGMT_ADV_INT_MAX, .interval_max = BT_GAP_ADV_FAST_INT_MAX_2,
.peer = NULL,
}; };
static void att_mtu_updated(struct bt_conn *conn, uint16_t tx, uint16_t rx) static void att_mtu_updated(struct bt_conn *conn, uint16_t tx, uint16_t rx)
{ {
LOG_INF("MTU exchanged: TX %u bytes, RX %u bytes", tx, rx); ARG_UNUSED(conn);
current_tx_mtu = tx;
current_rx_mtu = rx; current_att_mtu = MIN(tx, rx);
LOG_INF("ATT MTU updated to %u", current_att_mtu);
}
static size_t peer_count(void)
{
size_t count = 0;
for (size_t index = 0; index < ARRAY_SIZE(peers); ++index) {
if (peers[index].conn != NULL) {
++count;
}
}
return count;
}
static struct ble_peer *find_peer(struct bt_conn *conn)
{
for (size_t index = 0; index < ARRAY_SIZE(peers); ++index) {
if (peers[index].conn == conn) {
return &peers[index];
}
}
return NULL;
}
static struct ble_peer *reserve_peer(struct bt_conn *conn)
{
struct ble_peer *peer = find_peer(conn);
if (peer != NULL) {
return peer;
}
for (size_t index = 0; index < ARRAY_SIZE(peers); ++index) {
if (peers[index].conn == NULL) {
peers[index].conn = bt_conn_ref(conn);
return &peers[index];
}
}
return NULL;
}
static void release_peer(struct bt_conn *conn)
{
struct ble_peer *peer = find_peer(conn);
if (peer == NULL) {
return;
}
bt_conn_unref(peer->conn);
peer->conn = NULL;
}
static void set_device_name(const char *name)
{
const char *name_to_use = name != NULL ? name : CONFIG_BLE_MGMT_DEFAULT_DEVICE_NAME;
strncpy(current_device_name, name_to_use, MAX_ADV_NAME_LEN);
current_device_name[MAX_ADV_NAME_LEN] = '\0';
sd[0].data_len = strlen(current_device_name);
#ifdef CONFIG_BT_DEVICE_NAME_DYNAMIC
if (ble_ready) {
(void)bt_set_name(current_device_name);
}
#endif
}
static int start_advertising(void)
{
int rc;
if (!ble_ready || peer_count() >= CONFIG_BT_MAX_CONN || advertising_active) {
return 0;
}
rc = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
if (rc == -EALREADY) {
advertising_active = true;
return 0;
}
if (rc != 0) {
LOG_ERR("Failed to start advertising (err %d)", rc);
return rc;
}
advertising_active = true;
LOG_INF("Advertising as %s", current_device_name);
return 0;
}
static int restart_advertising(void)
{
int rc;
if (!ble_ready) {
return -EAGAIN;
}
if (advertising_active) {
rc = bt_le_adv_stop();
if ((rc != 0) && (rc != -EALREADY)) {
LOG_ERR("Failed to stop advertising (err %d)", rc);
return rc;
}
advertising_active = false;
}
return start_advertising();
} }
static ssize_t rx_cb(struct bt_conn *conn, const struct bt_gatt_attr *attr, static ssize_t rx_cb(struct bt_conn *conn, const struct bt_gatt_attr *attr,
const void *buf, uint16_t len, uint16_t offset, uint8_t flags) const void *buf, uint16_t len, uint16_t offset, uint8_t flags)
{ {
LOG_DBG("Received %u bytes", len); ARG_UNUSED(conn);
LOG_HEXDUMP_DBG(buf, len, "Data:"); ARG_UNUSED(attr);
ARG_UNUSED(flags);
if (app_rx_cb) if (offset != 0U) {
{ return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
}
if (app_rx_cb != NULL) {
app_rx_cb((const uint8_t *)buf, len); app_rx_cb((const uint8_t *)buf, len);
} }
return len; return len;
} }
static void tx_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) static void tx_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value)
{ {
notify_enabled = (value == BT_GATT_CCC_NOTIFY); ARG_UNUSED(attr);
LOG_INF("Notifications %s", notify_enabled ? "enabled" : "disabled"); LOG_INF("Notification state changed: 0x%04x", value);
} }
BT_GATT_SERVICE_DEFINE(ble_mgmt_svc, BT_GATT_SERVICE_DEFINE(ble_mgmt_svc,
@@ -86,177 +210,167 @@ BT_GATT_SERVICE_DEFINE(ble_mgmt_svc,
BT_GATT_PERM_NONE, NULL, NULL, NULL), BT_GATT_PERM_NONE, NULL, NULL, NULL),
BT_GATT_CCC(tx_ccc_cfg_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE)); BT_GATT_CCC(tx_ccc_cfg_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE));
uint16_t ble_mgmt_get_max_payload(void)
{
/* Kappe die verhandelte MTU auf die hart konfigurierte Zephyr-Puffergrenze */
uint16_t effective_mtu = MIN(current_tx_mtu, current_rx_mtu);
#ifdef CONFIG_BT_L2CAP_TX_MTU
if (effective_mtu > CONFIG_BT_L2CAP_TX_MTU)
{
effective_mtu = CONFIG_BT_L2CAP_TX_MTU;
}
#endif
/* 3 Bytes abziehen für den GATT Notification Overhead */
return (effective_mtu > 3) ? (effective_mtu - 3) : 20;
}
int ble_mgmt_send(const uint8_t *data, uint16_t len)
{
if (!notify_enabled)
{
return -EACCES;
}
int rc;
do
{
rc = bt_gatt_notify(NULL, &ble_mgmt_svc.attrs[4], data, len);
if (rc == -ENOMEM)
{
k_sleep(K_MSEC(5)); // Thread pausieren, bis TX-Buffer frei wird
}
} while (rc == -ENOMEM);
if (rc)
{
LOG_ERR("Failed to send notification (err %d)", rc);
return rc;
}
return rc;
}
/* Interne Hilfsfunktion zur Zuweisung des Namens */
static void set_device_name(const char *name)
{
if (!name)
{
return;
}
strncpy(current_device_name, name, MAX_ADV_NAME_LEN);
current_device_name[MAX_ADV_NAME_LEN] = '\0';
/* Längen-Update im Scan-Response Array */
sd[0].data_len = strlen(current_device_name);
#ifdef CONFIG_BT_DEVICE_NAME_DYNAMIC
/* Setzt den Namen parallel im Zephyr GAP-Service (wichtig für macOS) */
bt_set_name(current_device_name);
#endif
}
int ble_mgmt_update_adv_name(const char *new_name)
{
int rc;
bt_le_adv_stop();
set_device_name(new_name);
rc = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
if (rc)
{
LOG_ERR("Advertising failed to restart after name update (err %d)", rc);
return rc;
}
LOG_INF("Advertising updated. New Name: %s", current_device_name);
return 0;
}
static void connected(struct bt_conn *conn, uint8_t err) static void connected(struct bt_conn *conn, uint8_t err)
{ {
if (err) char addr_str[BT_ADDR_LE_STR_LEN];
{ struct bt_conn_info info;
int rc;
if (err != 0U) {
LOG_ERR("Connection failed (err 0x%02x)", err); LOG_ERR("Connection failed (err 0x%02x)", err);
return; return;
} }
char addr_str[BT_ADDR_LE_STR_LEN]; advertising_active = false;
struct bt_conn_info info;
int rc = bt_conn_get_info(conn, &info); if (reserve_peer(conn) == NULL) {
if (rc == 0) LOG_ERR("No free BLE peer slot available");
{ return;
}
rc = bt_conn_get_info(conn, &info);
if ((rc == 0) && (info.type == BT_CONN_TYPE_LE)) {
bt_addr_le_to_str(info.le.dst, addr_str, sizeof(addr_str)); bt_addr_le_to_str(info.le.dst, addr_str, sizeof(addr_str));
LOG_INF("Connected to %s", addr_str); LOG_INF("Connected to %s (%u/%u)", addr_str, (unsigned int)peer_count(),
LOG_INF("Role: %s", info.role == BT_CONN_ROLE_CENTRAL ? "Central" : "Peripheral"); CONFIG_BT_MAX_CONN);
} else {
LOG_INF("Connected (%u/%u)", (unsigned int)peer_count(), CONFIG_BT_MAX_CONN);
} }
else
{ if (peer_count() < CONFIG_BT_MAX_CONN) {
LOG_INF("Connected (info retrieval failed)"); (void)start_advertising();
} }
event_mgmt_set_event(EVENT_MGMT_BLE_CONNECTED);
} }
static void disconnected(struct bt_conn *conn, uint8_t reason) static void disconnected(struct bt_conn *conn, uint8_t reason)
{ {
LOG_INF("Disconnected (reason 0x%02x)", reason); release_peer(conn);
advertising_active = false;
/* Startet Advertising mit dem global definierten Setup neu */ LOG_INF("Disconnected (reason 0x%02x, %u/%u active)", reason,
int rc = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd)); (unsigned int)peer_count(), CONFIG_BT_MAX_CONN);
if (rc)
{ if (peer_count() < CONFIG_BT_MAX_CONN) {
LOG_ERR("Advertising failed to restart (err %d)", rc); (void)start_advertising();
}
else
{
LOG_INF("Advertising successfully restarted");
}
} }
static void le_phy_updated(struct bt_conn *conn, struct bt_conn_le_phy_info *param) event_mgmt_set_event(EVENT_MGMT_BLE_DISCONNECTED);
{
const char *tx_phy_str = (param->tx_phy == BT_GAP_LE_PHY_2M) ? "2M" : (param->tx_phy == BT_GAP_LE_PHY_1M) ? "1M" : "Coded/Unknown";
const char *rx_phy_str = (param->rx_phy == BT_GAP_LE_PHY_2M) ? "2M" : (param->rx_phy == BT_GAP_LE_PHY_1M) ? "1M" : "Coded/Unknown";
LOG_INF("LE PHY updated: TX PHY %s, RX PHY %s", tx_phy_str, rx_phy_str);
}
static void le_param_updated(struct bt_conn *conn, uint16_t interval,
uint16_t latency, uint16_t timeout)
{
LOG_INF("Connection parameters updated: Interval: %u, Latency: %u, Timeout: %u",
interval, latency, timeout);
} }
BT_CONN_CB_DEFINE(conn_callbacks) = { BT_CONN_CB_DEFINE(conn_callbacks) = {
.connected = connected, .connected = connected,
.disconnected = disconnected, .disconnected = disconnected,
.le_param_updated = le_param_updated,
.le_phy_updated = le_phy_updated,
}; };
uint16_t ble_mgmt_get_max_payload(void)
{
uint16_t mtu = current_att_mtu;
#ifdef CONFIG_BT_L2CAP_TX_MTU
if (mtu > CONFIG_BT_L2CAP_TX_MTU) {
mtu = CONFIG_BT_L2CAP_TX_MTU;
}
#endif
return (mtu > GATT_NOTIFY_OVERHEAD) ? (mtu - GATT_NOTIFY_OVERHEAD) : 20U;
}
int ble_mgmt_send(const uint8_t *data, uint16_t len)
{
bool any_subscriber = false;
bool any_link_present = false;
int last_error = 0;
ARG_UNUSED(data);
ARG_UNUSED(len);
for (size_t index = 0; index < ARRAY_SIZE(peers); ++index) {
int rc;
if (peers[index].conn == NULL) {
continue;
}
any_link_present = true;
if (!bt_gatt_is_subscribed(peers[index].conn, &ble_mgmt_svc.attrs[4],
BT_GATT_CCC_NOTIFY)) {
continue;
}
any_subscriber = true;
do {
rc = bt_gatt_notify(peers[index].conn, &ble_mgmt_svc.attrs[4], data, len);
if (rc == -ENOMEM) {
k_sleep(K_MSEC(5));
}
} while (rc == -ENOMEM);
if (rc != 0) {
LOG_ERR("Failed to send notification (err %d)", rc);
last_error = rc;
}
}
if (!any_link_present || current_att_mtu <= DEFAULT_ATT_MTU) {
LOG_DBG("TX deferred: ATT not ready (links=%u, mtu=%u)",
(unsigned int)peer_count(), current_att_mtu);
return -EAGAIN;
}
if (!any_subscriber) {
LOG_DBG("TX blocked: no peer subscribed for notifications");
return -EACCES;
}
return last_error;
}
int ble_mgmt_update_adv_name(const char *new_name)
{
set_device_name(new_name);
if (peer_count() >= CONFIG_BT_MAX_CONN && !advertising_active) {
return 0;
}
return restart_advertising();
}
int ble_mgmt_init(ble_mgmt_rx_cb_t rx_cb, const char *device_name) int ble_mgmt_init(ble_mgmt_rx_cb_t rx_cb, const char *device_name)
{ {
int rc; int rc;
app_rx_cb = rx_cb;
static struct bt_gatt_cb gatt_callbacks = { static struct bt_gatt_cb gatt_callbacks = {
.att_mtu_updated = att_mtu_updated, .att_mtu_updated = att_mtu_updated,
}; };
bt_gatt_cb_register(&gatt_callbacks); LOG_INF("ble_mgmt_init: starting");
app_rx_cb = rx_cb;
current_att_mtu = DEFAULT_ATT_MTU;
LOG_INF("ble_mgmt_init: calling bt_enable");
rc = bt_enable(NULL); rc = bt_enable(NULL);
if (rc) if (rc != 0) {
{
LOG_ERR("Bluetooth init failed (err %d)", rc); LOG_ERR("Bluetooth init failed (err %d)", rc);
return rc; return rc;
} }
const char *name_to_use = (device_name != NULL) ? device_name : CONFIG_BLE_MGMT_DEFAULT_DEVICE_NAME; LOG_INF("ble_mgmt_init: bt_enable done, marking ready");
LOG_INF("BLE init: set_device_name"); ble_ready = true;
set_device_name(name_to_use); bt_gatt_cb_register(&gatt_callbacks);
advertising_active = false;
set_device_name(device_name);
LOG_INF("BLE init: bt_le_adv_start"); LOG_INF("ble_mgmt_init: calling start_advertising");
rc = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd)); rc = start_advertising();
if (rc) if (rc != 0) {
{ LOG_ERR("start_advertising failed (err %d)", rc);
LOG_ERR("Advertising failed to start (err %d)", rc);
return rc; return rc;
} }
LOG_INF("Bluetooth initialized. Adv-Name: %s", current_device_name); LOG_INF("ble_mgmt_init: complete");
return 0; return 0;
} }

View File

@@ -36,6 +36,7 @@ enum buzz_data_type
BUZZ_DATA_DEVICE_INFO = 0x02, BUZZ_DATA_DEVICE_INFO = 0x02,
BUZZ_DATA_FS_INFO = 0x03, BUZZ_DATA_FS_INFO = 0x03,
BUZZ_DATA_FW_INFO = 0x04, BUZZ_DATA_FW_INFO = 0x04,
BUZZ_DATA_BATT_INFO = 0x05,
BUZZ_DATA_FILE_GET = 0x20, BUZZ_DATA_FILE_GET = 0x20,
BUZZ_DATA_FILE_PUT = 0x21, BUZZ_DATA_FILE_PUT = 0x21,
@@ -122,6 +123,17 @@ struct __attribute__((packed)) buzz_resp_fw_info
uint8_t kernel_version_length; /* Länge der Kernel-Versionszeichenkette */ uint8_t kernel_version_length; /* Länge der Kernel-Versionszeichenkette */
char data[]; /* Variabler String ohne Null-Terminierung: [fw_version][kernel_version] */ char data[]; /* Variabler String ohne Null-Terminierung: [fw_version][kernel_version] */
}; };
/* Payload für die Batterie-Infos */
struct __attribute__((packed)) buzz_resp_batt_info
{
uint8_t data_type; /* BUZZ_DATA_BATT_INFO */
uint8_t batt_status; /* batt_mgmt_state_t */
uint8_t batt_level; /* 0..4 */
uint8_t batt_percent; /* 0..100 */
uint16_t batt_voltage_mv; /* Little Endian */
};
/* Payload für das Entfernen einer Datei */ /* Payload für das Entfernen einer Datei */
struct __attribute__((packed)) buzz_rm_file_payload struct __attribute__((packed)) buzz_rm_file_payload
{ {

View File

@@ -7,6 +7,7 @@
#include <stdlib.h> #include <stdlib.h>
#include "buzz_proto.h" #include "buzz_proto.h"
#include "batt_mgmt.h"
#include "fs_mgmt.h" #include "fs_mgmt.h"
#include "fw_mgmt.h" #include "fw_mgmt.h"
@@ -293,6 +294,41 @@ static void handle_fw_info_request(struct buzz_frame_msg *msg)
} }
} }
static void handle_batt_info_request(struct buzz_frame_msg *msg)
{
struct buzz_proto_header *hdr = (struct buzz_proto_header *)msg->data_ptr;
struct buzz_resp_batt_info *resp_data = (struct buzz_resp_batt_info *)(msg->data_ptr + sizeof(*hdr));
batt_mgmt_info_t batt_info;
uint16_t voltage_mv = 0;
int rc = batt_mgmt_get_info(&batt_info);
if (rc < 0)
{
LOG_WRN("Failed to get battery info: %d", rc);
send_error_frame(msg, abs(rc));
return;
}
if (batt_info.voltage_mv > 0)
{
voltage_mv = (batt_info.voltage_mv > UINT16_MAX) ? UINT16_MAX : (uint16_t)batt_info.voltage_mv;
}
hdr->frame_type = BUZZ_FRAME_RESPONSE;
hdr->payload_length = sys_cpu_to_le16(sizeof(struct buzz_resp_batt_info));
resp_data->data_type = BUZZ_DATA_BATT_INFO;
resp_data->batt_status = (uint8_t)batt_info.state;
resp_data->batt_level = batt_info.level;
resp_data->batt_percent = batt_info.percent;
resp_data->batt_voltage_mv = sys_cpu_to_le16(voltage_mv);
if (msg->reply_cb)
{
msg->reply_cb(msg->data_ptr, sizeof(struct buzz_proto_header) + sizeof(struct buzz_resp_batt_info));
}
}
static void handle_ls_request(struct buzz_frame_msg *msg) static void handle_ls_request(struct buzz_frame_msg *msg)
{ {
struct buzz_proto_header *hdr = (struct buzz_proto_header *)msg->data_ptr; struct buzz_proto_header *hdr = (struct buzz_proto_header *)msg->data_ptr;
@@ -698,6 +734,11 @@ static void handle_request(struct buzz_frame_msg *msg)
handle_fw_info_request(msg); handle_fw_info_request(msg);
break; break;
case BUZZ_DATA_BATT_INFO:
LOG_DBG("Received BATT Info Request");
handle_batt_info_request(msg);
break;
case BUZZ_DATA_FILE_GET: case BUZZ_DATA_FILE_GET:
LOG_DBG("Received FILE_GET Request"); LOG_DBG("Received FILE_GET Request");
handle_file_get_request(msg, false); handle_file_get_request(msg, false);

View File

@@ -7,6 +7,8 @@
#define EVENT_MGMT_AUDIO_READY BIT(1) #define EVENT_MGMT_AUDIO_READY BIT(1)
#define EVENT_MGMT_BLE_CONNECTED BIT(2) #define EVENT_MGMT_BLE_CONNECTED BIT(2)
#define EVENT_MGMT_BLE_DISCONNECTED BIT(3) #define EVENT_MGMT_BLE_DISCONNECTED BIT(3)
#define EVENT_MGMT_AUDIO_ACTIVE BIT(4)
#define EVENT_MGMT_AUDIO_IDLE BIT(5)
extern struct k_event event_mgmt_events; extern struct k_event event_mgmt_events;

View File

@@ -7,7 +7,7 @@ menuconfig FS_MGMT
select FILE_SYSTEM_LITTLEFS select FILE_SYSTEM_LITTLEFS
select FILE_SYSTEM_MKFS select FILE_SYSTEM_MKFS
select FLASH_PAGE_LAYOUT select FLASH_PAGE_LAYOUT
select SPI_NOR if BOARD_BUZZY select NORDIC_QSPI_NOR if BOARD_BUZZY
select PM_OVERRIDE_EXTERNAL_DRIVER_CHECK if BOARD_BUZZY select PM_OVERRIDE_EXTERNAL_DRIVER_CHECK if BOARD_BUZZY
select NORDIC_QSPI_NOR if BOARD_NRF52840DK_NRF52840 select NORDIC_QSPI_NOR if BOARD_NRF52840DK_NRF52840
help help

View File

@@ -12,13 +12,16 @@
LOG_MODULE_REGISTER(fs_mgmt, CONFIG_FS_MGMT_LOG_LEVEL); LOG_MODULE_REGISTER(fs_mgmt, CONFIG_FS_MGMT_LOG_LEVEL);
/* Prefer external LittleFS partition when present, otherwise internal storage partition. */ /*
#if DT_NODE_EXISTS(DT_NODELABEL(ext_flash_lfs)) * Under sysbuild, Partition Manager generates PM_<name>_ID symbols.
* Without PM, we fall back to the DTS node label.
*/
#if defined(PM_littlefs_storage_ID)
#define FS_PARTITION_ID PM_littlefs_storage_ID
#elif DT_NODE_EXISTS(DT_NODELABEL(ext_flash_lfs))
#define FS_PARTITION_ID FIXED_PARTITION_ID(ext_flash_lfs) #define FS_PARTITION_ID FIXED_PARTITION_ID(ext_flash_lfs)
#elif DT_NODE_EXISTS(DT_NODELABEL(storage_partition))
#define FS_PARTITION_ID FIXED_PARTITION_ID(storage_partition)
#else #else
#error "No compatible LittleFS partition node label found (expected ext_flash_lfs or storage_partition)" #error "No compatible LittleFS partition found (expected PM littlefs_storage or DTS ext_flash_lfs)"
#endif #endif
FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(fs_storage_data); FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(fs_storage_data);
@@ -35,8 +38,10 @@ static struct fs_mount_t fs_storage_mnt = {
.mnt_point = CONFIG_FS_MGMT_MOUNT_POINT, .mnt_point = CONFIG_FS_MGMT_MOUNT_POINT,
}; };
#if defined(CONFIG_PM_DEVICE) && !defined(CONFIG_FILE_SYSTEM_SHELL)
static int open_count = 0; static int open_count = 0;
static struct k_mutex flash_pm_lock; static struct k_mutex flash_pm_lock;
#endif
// #define ACK_WATERMARK (CONFIG_BUZZ_PROTO_SLAB_COUNT / 4) // #define ACK_WATERMARK (CONFIG_BUZZ_PROTO_SLAB_COUNT / 4)
#define INITIAL_CREDITS CONFIG_BUZZ_PROTO_SLAB_COUNT #define INITIAL_CREDITS CONFIG_BUZZ_PROTO_SLAB_COUNT
@@ -76,7 +81,9 @@ static struct
*/ */
static int fs_mgmt_pm_flash_suspend(void) static int fs_mgmt_pm_flash_suspend(void)
{ {
#if IS_ENABLED(CONFIG_PM_DEVICE) #if defined(CONFIG_FILE_SYSTEM_SHELL)
return 0;
#elif defined(CONFIG_PM_DEVICE)
if (!device_is_ready(flash_dev)) if (!device_is_ready(flash_dev))
{ {
return -ENODEV; return -ENODEV;
@@ -102,7 +109,7 @@ static int fs_mgmt_pm_flash_suspend(void)
} }
k_mutex_unlock(&flash_pm_lock); k_mutex_unlock(&flash_pm_lock);
#endif /* CONFIG_PM_DEVICE */ #endif /* CONFIG_FILE_SYSTEM_SHELL / CONFIG_PM_DEVICE */
return 0; return 0;
} }
@@ -113,7 +120,9 @@ static int fs_mgmt_pm_flash_suspend(void)
*/ */
static int fs_mgmt_pm_flash_resume(void) static int fs_mgmt_pm_flash_resume(void)
{ {
#if IS_ENABLED(CONFIG_PM_DEVICE) #if defined(CONFIG_FILE_SYSTEM_SHELL)
return 0;
#elif defined(CONFIG_PM_DEVICE)
if (!device_is_ready(flash_dev)) if (!device_is_ready(flash_dev))
return -ENODEV; return -ENODEV;
@@ -131,7 +140,7 @@ static int fs_mgmt_pm_flash_resume(void)
open_count++; open_count++;
k_mutex_unlock(&flash_pm_lock); k_mutex_unlock(&flash_pm_lock);
#endif /* CONFIG_PM_DEVICE */ #endif /* CONFIG_FILE_SYSTEM_SHELL / CONFIG_PM_DEVICE */
return 0; return 0;
} }
@@ -179,6 +188,20 @@ int fs_mgmt_pm_unlink(const char *path)
{ {
LOG_DBG("PM Unlinking file '%s'", path); LOG_DBG("PM Unlinking file '%s'", path);
fs_mgmt_pm_flash_resume(); fs_mgmt_pm_flash_resume();
struct fs_dirent entry;
int stat_rc = fs_stat(path, &entry);
if (stat_rc == -ENOENT)
{
fs_mgmt_pm_flash_suspend();
return 0;
}
if (stat_rc < 0)
{
fs_mgmt_pm_flash_suspend();
return stat_rc;
}
int rc = fs_unlink(path); int rc = fs_unlink(path);
fs_mgmt_pm_flash_suspend(); fs_mgmt_pm_flash_suspend();
return rc; return rc;
@@ -397,7 +420,9 @@ int fs_mgmt_pm_mkdir_recursive(char *path)
static int fs_mgmt_init(void) static int fs_mgmt_init(void)
{ {
#if defined(CONFIG_PM_DEVICE) && !defined(CONFIG_FILE_SYSTEM_SHELL)
k_mutex_init(&flash_pm_lock); k_mutex_init(&flash_pm_lock);
#endif
if (!device_is_ready(flash_dev)) if (!device_is_ready(flash_dev))
{ {

View File

@@ -0,0 +1,5 @@
if(CONFIG_SETTINGS_MGMT)
zephyr_library()
zephyr_library_sources(src/settings_mgmt.c)
zephyr_include_directories(include)
endif()

View File

@@ -0,0 +1,15 @@
menuconfig SETTINGS_MGMT
bool "Settings Management"
default y
select SETTINGS
select SETTINGS_RUNTIME
select ZMS
help
Library for initializing and managing the settings subsystem.
ZMS (Zephyr Memory Storage) backend is automatically selected when ZMS is enabled.
if SETTINGS_MGMT
module = SETTINGS_MGMT
module-str = settings_mgmt
source "subsys/logging/Kconfig.template.log_config"
endif # SETTINGS_MGMT

View File

@@ -0,0 +1,34 @@
#ifndef SETTINGS_MGMT_H
#define SETTINGS_MGMT_H
#include <zephyr/types.h>
struct app_settings_t {
/* System */
char dev_name[33];
/* Audio */
uint8_t vol;
uint8_t shuffle_mode; /* 0: Rnd, 1: No-Rep */
/* BLE */
uint32_t ble_timeout; /* Seconds, 0xFFFFFFFF = forever */
uint16_t ble_interval; /* Milliseconds */
/* Power & Calibration */
uint8_t chg_mode; /* 0: 500mA, 1: 1A, 2: Auto */
uint16_t off_threshold;/* mV */
int16_t adc_gain;
int16_t adc_offset;
};
/* Global access to the current runtime configuration */
extern struct app_settings_t app_cfg;
/* Initializes the settings subsystem and loads values from flash */
int settings_mgmt_init(void);
/* Sets a setting via string path (for your protocol) */
int settings_mgmt_set_by_path(const char *path, const void *value, size_t len);
#endif

View File

@@ -0,0 +1,243 @@
#include <zephyr/settings/settings.h>
#include <zephyr/logging/log.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include "settings_mgmt.h"
LOG_MODULE_REGISTER(settings_mgmt, LOG_LEVEL_INF);
/* Sanity check limits */
#define VOL_MIN 0
#define VOL_MAX 100
#define SHUFFLE_MODE_MIN 0
#define SHUFFLE_MODE_MAX 1
#define BLE_TIMEOUT_MIN 1000
#define BLE_TIMEOUT_MAX 0xFFFFFFFF
#define BLE_INTERVAL_MIN 10
#define BLE_INTERVAL_MAX 10000
#define CHG_MODE_MIN 0
#define CHG_MODE_MAX 2
#define OFF_THRESHOLD_MIN 2900 /* mV, LiPo cut-off */
#define OFF_THRESHOLD_MAX 3500 /* mV, LiPo nominal */
#define ADC_GAIN_MIN -100
#define ADC_GAIN_MAX 100
#define ADC_OFFSET_MIN -1000
#define ADC_OFFSET_MAX 1000
/* The "Source of Truth" in RAM */
struct app_settings_t app_cfg = {
#ifdef CONFIG_BLE_MGMT_DEFAULT_DEVICE_NAME
.dev_name = CONFIG_BLE_MGMT_DEFAULT_DEVICE_NAME,
#else
.dev_name = CONFIG_BOARD, /* Default to board name if no explicit default set */
#endif
.vol = 100, /* 0-100 % */
.shuffle_mode = 0,
.ble_timeout = 0xFFFFFFFF,
.ble_interval = 100,
.chg_mode = 2, /* Auto */
.off_threshold = 3000, /* mV */
.adc_gain = 0,
.adc_offset = 0,
};
static int read_exact(settings_read_cb read_cb, void *cb_arg,
void *dst, size_t expected_len)
{
int rc = read_cb(cb_arg, dst, expected_len);
if (rc < 0) {
return rc;
}
if ((size_t)rc != expected_len) {
return -EINVAL;
}
return 0;
}
/* Sanity check helpers */
static int check_uint8_range(uint8_t val, uint8_t min, uint8_t max,
const char *name)
{
if (val < min || val > max) {
LOG_WRN("%s out of range: %u (valid: %u-%u)", name, val, min, max);
return -EINVAL;
}
return 0;
}
static int check_uint16_range(uint16_t val, uint16_t min, uint16_t max,
const char *name)
{
if (val < min || val > max) {
LOG_WRN("%s out of range: %u (valid: %u-%u)", name, val, min, max);
return -EINVAL;
}
return 0;
}
static int check_uint32_range(uint32_t val, uint32_t min, uint32_t max,
const char *name)
{
if (val < min || val > max) {
LOG_WRN("%s out of range: %u (valid: %u-%u)", name, val, min, max);
return -EINVAL;
}
return 0;
}
static int check_int16_range(int16_t val, int16_t min, int16_t max,
const char *name)
{
if (val < min || val > max) {
LOG_WRN("%s out of range: %d (valid: %d-%d)", name, val, min, max);
return -EINVAL;
}
return 0;
}
/* Callback: Called by settings_load() or settings_runtime_set() */
static int buzzy_settings_set(const char *name, size_t len,
settings_read_cb read_cb, void *cb_arg)
{
const char *next;
int rc;
/* Path matching: "buzzy/sys/name" -> name is "sys/name" here */
if (settings_name_steq(name, "sys/name", &next) && !next) {
rc = read_cb(cb_arg, app_cfg.dev_name, sizeof(app_cfg.dev_name) - 1);
if (rc >= 0) app_cfg.dev_name[rc] = '\0';
return 0;
}
if (settings_name_steq(name, "audio/vol", &next) && !next) {
uint8_t vol;
rc = read_exact(read_cb, cb_arg, &vol, sizeof(vol));
if (rc == 0) {
rc = check_uint8_range(vol, VOL_MIN, VOL_MAX, "audio/vol");
if (rc == 0) {
app_cfg.vol = vol;
}
}
return rc;
}
if ((settings_name_steq(name, "audio/shuffle", &next) ||
settings_name_steq(name, "audio/shuffle_mode", &next)) && !next) {
uint8_t shuffle;
rc = read_exact(read_cb, cb_arg, &shuffle, sizeof(shuffle));
if (rc == 0) {
rc = check_uint8_range(shuffle, SHUFFLE_MODE_MIN, SHUFFLE_MODE_MAX, "shuffle_mode");
if (rc == 0) {
app_cfg.shuffle_mode = shuffle;
}
}
return rc;
}
if (settings_name_steq(name, "ble/to", &next) && !next) {
uint32_t timeout;
rc = read_exact(read_cb, cb_arg, &timeout, sizeof(timeout));
if (rc == 0) {
rc = check_uint32_range(timeout, BLE_TIMEOUT_MIN, BLE_TIMEOUT_MAX, "ble/to");
if (rc == 0) {
app_cfg.ble_timeout = timeout;
}
}
return rc;
}
if ((settings_name_steq(name, "ble/interval", &next) ||
settings_name_steq(name, "ble/int", &next)) && !next) {
uint16_t interval;
rc = read_exact(read_cb, cb_arg, &interval, sizeof(interval));
if (rc == 0) {
rc = check_uint16_range(interval, BLE_INTERVAL_MIN, BLE_INTERVAL_MAX, "ble/interval");
if (rc == 0) {
app_cfg.ble_interval = interval;
}
}
return rc;
}
if ((settings_name_steq(name, "power/chg_mode", &next) ||
settings_name_steq(name, "sys/chg_mode", &next)) && !next) {
uint8_t chg_mode;
rc = read_exact(read_cb, cb_arg, &chg_mode, sizeof(chg_mode));
if (rc == 0) {
rc = check_uint8_range(chg_mode, CHG_MODE_MIN, CHG_MODE_MAX, "chg_mode");
if (rc == 0) {
app_cfg.chg_mode = chg_mode;
}
}
return rc;
}
if ((settings_name_steq(name, "power/off_threshold", &next) ||
settings_name_steq(name, "sys/off_threshold", &next)) && !next) {
uint16_t threshold;
rc = read_exact(read_cb, cb_arg, &threshold, sizeof(threshold));
if (rc == 0) {
rc = check_uint16_range(threshold, OFF_THRESHOLD_MIN, OFF_THRESHOLD_MAX, "off_threshold [mV]");
if (rc == 0) {
app_cfg.off_threshold = threshold;
}
}
return rc;
}
if (settings_name_steq(name, "adc/gain", &next) && !next) {
int16_t gain;
rc = read_exact(read_cb, cb_arg, &gain, sizeof(gain));
if (rc == 0) {
rc = check_int16_range(gain, ADC_GAIN_MIN, ADC_GAIN_MAX, "adc/gain");
if (rc == 0) {
app_cfg.adc_gain = gain;
}
}
return rc;
}
if (settings_name_steq(name, "adc/offset", &next) && !next) {
int16_t offset;
rc = read_exact(read_cb, cb_arg, &offset, sizeof(offset));
if (rc == 0) {
rc = check_int16_range(offset, ADC_OFFSET_MIN, ADC_OFFSET_MAX, "adc/offset");
if (rc == 0) {
app_cfg.adc_offset = offset;
}
}
return rc;
}
return -ENOENT;
}
struct settings_handler buzzy_handler = {
.name = "buzzy",
.h_set = buzzy_settings_set
};
int settings_mgmt_set_by_path(const char *path, const void *value, size_t len)
{
char full_path[64];
snprintf(full_path, sizeof(full_path), "buzzy/%s", path);
/* Schreibt in ZMS UND triggert buzzy_settings_set zur RAM-Aktualisierung */
return settings_runtime_set(full_path, value, len);
}
int settings_mgmt_init(void)
{
int rc = settings_subsys_init();
if (rc) return rc;
rc = settings_register(&buzzy_handler);
if (rc) return rc;
/* Lädt alle gespeicherten Werte aus dem ZMS in die app_cfg Struktur */
return settings_load();
}

View File

@@ -0,0 +1,5 @@
if(CONFIG_USB_MGMT)
zephyr_library()
zephyr_library_sources(src/usb_mgmt.c)
zephyr_include_directories(include)
endif()

View File

@@ -0,0 +1,13 @@
menuconfig USB_MGMT
bool "USB Management"
default y
select NRFX_POWER
help
Library for USB-related queries, e.g. VBUS presence detection
via the nRF52840 POWER peripheral.
if USB_MGMT
module = USB_MGMT
module-str = usb_mgmt
source "subsys/logging/Kconfig.template.log_config"
endif # USB_MGMT

View File

@@ -0,0 +1,30 @@
#ifndef USB_MGMT_H
#define USB_MGMT_H
#include <stdbool.h>
#include <zephyr/kernel.h>
#define USB_MGMT_VBUS_CONNECTED BIT(0)
#define USB_MGMT_VBUS_DISCONNECTED BIT(1)
extern struct k_event usb_mgmt_events;
/**
* @brief Wait until usb_mgmt has completed initialization.
*
* @param timeout Timeout for waiting.
*
* @retval true usb_mgmt is ready.
* @retval false timeout occurred.
*/
bool usb_mgmt_wait_ready(k_timeout_t timeout);
/**
* @brief Returns true when VBUS (USB 5 V) is present.
*
* Reads the USBREGSTATUS.VBUSDETECT bit in the nRF52840 POWER peripheral.
* No USB stack needs to be enabled.
*/
bool usb_mgmt_is_vbus_present(void);
#endif /* USB_MGMT_H */

View File

@@ -0,0 +1,65 @@
#include <hal/nrf_power.h>
#include <nrfx_power.h>
#include <errno.h>
#include <zephyr/logging/log.h>
#include "usb_mgmt.h"
LOG_MODULE_REGISTER(usb_mgmt, CONFIG_USB_MGMT_LOG_LEVEL);
K_EVENT_DEFINE(usb_mgmt_events);
K_SEM_DEFINE(usb_mgmt_ready_sem, 0, 1);
static bool usb_mgmt_last_vbus_state;
static bool usb_mgmt_ready;
static void usb_mgmt_power_handler(nrfx_power_usb_evt_t event)
{
bool is_present = (event == NRFX_POWER_USB_EVT_DETECTED);
LOG_INF("VBUS %s (event=%u)", is_present ? "connected" : "disconnected", event);
if (is_present != usb_mgmt_last_vbus_state) {
usb_mgmt_last_vbus_state = is_present;
k_event_post(&usb_mgmt_events,
is_present ? USB_MGMT_VBUS_CONNECTED : USB_MGMT_VBUS_DISCONNECTED);
}
}
static int usb_mgmt_init(void)
{
usb_mgmt_last_vbus_state = nrf_power_usbregstatus_vbusdet_get(NRF_POWER);
LOG_DBG("Boot VBUS state: %s", usb_mgmt_last_vbus_state ? "present" : "absent");
k_event_post(&usb_mgmt_events,
usb_mgmt_last_vbus_state ? USB_MGMT_VBUS_CONNECTED : USB_MGMT_VBUS_DISCONNECTED);
nrfx_power_config_t config = { 0 };
nrfx_power_usbevt_config_t usb_config = {
.handler = usb_mgmt_power_handler,
};
nrfx_power_init(&config);
nrfx_power_usbevt_init(&usb_config);
nrfx_power_usbevt_enable();
usb_mgmt_ready = true;
k_sem_give(&usb_mgmt_ready_sem);
return 0;
}
SYS_INIT(usb_mgmt_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY);
bool usb_mgmt_is_vbus_present(void)
{
return usb_mgmt_last_vbus_state;
}
bool usb_mgmt_wait_ready(k_timeout_t timeout)
{
if (usb_mgmt_ready) {
return true;
}
return k_sem_take(&usb_mgmt_ready_sem, timeout) == 0;
}

2
firmware/output.txt Normal file
View File

@@ -0,0 +1,2 @@
ERROR: Build directory /Users/edi/nrf_playground/buzzer_2/firmware/build targets board buzzy/nrf52840, but board {self.args.board} was specified. (Clean the directory, use --pristine, or use --build-dir to specify a different one.)
FATAL ERROR: refusing to proceed without --force due to above error

View File

@@ -1,4 +1,39 @@
# External Flash # Static Partition Manager layout for sysbuild builds.
# Keep this aligned with boards/iten/buzzy/buzzy.dts internal flash partitions.
mcuboot:
address: 0x0
size: 0xC000
region: flash_primary
mcuboot_pad:
address: 0xC000
size: 0x200
region: flash_primary
app:
address: 0xC200
size: 0x75E00
region: flash_primary
mcuboot_primary:
address: 0xC000
size: 0x76000
region: flash_primary
span: [mcuboot_pad, app]
mcuboot_secondary:
address: 0x82000
size: 0x76000
region: flash_primary
# Internal flash tail partition (8 x 4 KiB pages) for ZMS/settings-style usage.
storage:
address: 0xF8000
size: 0x8000
region: flash_primary
# External flash LittleFS image area.
littlefs_storage: littlefs_storage:
address: 0x0 address: 0x0
size: 0x800000 size: 0x800000

View File

@@ -1,39 +0,0 @@
# mcuboot:
# address: 0x0
# size: 0xC000
# region: flash_primary
# # Primary Slot: Start bleibt 0xC000, Größe 200KB (0x32000)
# mcuboot_primary:
# address: 0xC000
# size: 0x32000
# region: flash_primary
# mcuboot_pad:
# address: 0xC000
# size: 0x200
# region: flash_primary
# # Die App startet nach dem Padding des Primary Slots
# app:
# address: 0xC200
# size: 0x31E00 # (0x32000 - 0x200)
# region: flash_primary
# # Secondary Slot: Startet jetzt bei 0xC000 + 0x32000 = 0x3E000
# mcuboot_secondary:
# address: 0x3E000
# size: 0x32000
# region: flash_primary
# # NVS storage am Ende des Flashs, 16KB (0x4000)
# settings_storage:
# address: 0xFC000
# size: 0x4000
# region: flash_primary
# External Flash
littlefs_storage:
address: 0x0
size: 0x800000 # 8MB
region: external_flash

View File

@@ -1,6 +1,3 @@
### Bluetooth
CONFIG_BLE_MGMT=y
### Error handling ### Error handling
CONFIG_HW_STACK_PROTECTION=y CONFIG_HW_STACK_PROTECTION=y
CONFIG_RESET_ON_FATAL_ERROR=y CONFIG_RESET_ON_FATAL_ERROR=y
@@ -8,5 +5,9 @@ CONFIG_RESET_ON_FATAL_ERROR=y
### Power management ### Power management
CONFIG_PM_DEVICE=y CONFIG_PM_DEVICE=y
### Boot banner
CONFIG_NCS_APPLICATION_BOOT_BANNER_STRING="Edis Buzzer"
### Stack ### Stack
CONFIG_MAIN_STACK_SIZE=2048 CONFIG_MAIN_STACK_SIZE=2048
CONFIG_BT_RX_STACK_SIZE=4096

View File

@@ -0,0 +1,5 @@
connect
rtt start
sleep 3000
rtt stop
exit

View File

@@ -2,13 +2,30 @@
#include <zephyr/logging/log.h> #include <zephyr/logging/log.h>
#include <string.h> #include <string.h>
#include "fs_mgmt.h"
#include "buzz_proto.h" #include "buzz_proto.h"
#include "fw_mgmt.h" #include "settings_mgmt.h"
#include "batt_mgmt.h"
// #include "fw_mgmt.h"
// #include "audio.h" // #include "audio.h"
LOG_MODULE_REGISTER(main); LOG_MODULE_REGISTER(main);
static const char *battery_state_to_str(batt_mgmt_state_t state)
{
switch (state) {
case BATT_STATE_DISCHARGING:
return "discharging";
case BATT_STATE_FULL:
return "full";
case BATT_STATE_CHARGING:
return "charging";
case BATT_STATE_ERROR:
return "error";
default:
return "unknown";
}
}
#if IS_ENABLED(CONFIG_BLE_MGMT) #if IS_ENABLED(CONFIG_BLE_MGMT)
#include "ble_mgmt.h" #include "ble_mgmt.h"
void ble_rx_cb(const uint8_t *data, uint16_t len) void ble_rx_cb(const uint8_t *data, uint16_t len)
@@ -49,24 +66,50 @@ void ble_rx_cb(const uint8_t *data, uint16_t len)
int main(void) int main(void)
{ {
#if IS_ENABLED(CONFIG_BLE_MGMT) #if IS_ENABLED(CONFIG_BLE_MGMT)
/* BLE-Subsystem initialisieren und RX-Callback registrieren */ /* BLE-Subsystem initialisieren und RX-Callback registrieren */
int rc = ble_mgmt_init(ble_rx_cb, CONFIG_BLE_MGMT_DEFAULT_DEVICE_NAME); int rc = ble_mgmt_init(ble_rx_cb, app_cfg.dev_name);
if (rc < 0) { if (rc < 0) {
LOG_ERR("Failed to initialize BLE management: %d", rc); LOG_ERR("Failed to initialize BLE management: %d", rc);
return rc; return rc;
} }
LOG_WRN("After BLE init");
#else #else
LOG_WRN("BLE not enabled"); LOG_WRN("BLE not enabled");
#endif #endif
LOG_WRN("Main park loop active"); #if IS_ENABLED(CONFIG_LOG)
#if IS_ENABLED(CONFIG_BATT_MGMT)
k_sleep(K_SECONDS(1));
batt_mgmt_info_t batt_info;
int batt_rc = batt_mgmt_get_info(&batt_info);
if (batt_rc == 0) {
LOG_INF("Battery after 1s: %d mV, %u%%, level=%u, state=%s (%d)",
batt_info.voltage_mv,
batt_info.percent,
batt_info.level,
battery_state_to_str(batt_info.state),
batt_info.state);
} else {
LOG_WRN("Battery info read failed: %d", batt_rc);
}
for (;;) {
k_sleep(K_SECONDS(5));
batt_rc = batt_mgmt_get_info(&batt_info);
if (batt_rc == 0) {
LOG_INF("Battery: %d mV, %u%%, level=%u, state=%s (%d)",
batt_info.voltage_mv,
batt_info.percent,
batt_info.level,
battery_state_to_str(batt_info.state),
batt_info.state);
} else {
LOG_WRN("Battery info read failed: %d", batt_rc);
}
}
#endif // CONFIG_BATT_MGMT
#endif // CONFIG_LOG
// k_sleep(K_MSEC(500));
// LOG_INF("Playing test audio files...");
// audio_queue_play("/lfs/sys/update", false);
// audio_queue_play("/lfs/sys/confirm", false);
for (;;) { for (;;) {
int32_t rem_ms = k_sleep(K_FOREVER); int32_t rem_ms = k_sleep(K_FOREVER);
LOG_WRN("main woke unexpectedly (remaining=%d ms)", rem_ms); LOG_WRN("main woke unexpectedly (remaining=%d ms)", rem_ms);

View File

@@ -92,6 +92,7 @@ Das Protokoll ist so ausgelegt, dass es mit mindestens 100 Bytes auskommen sollt
| `0x02` | `DEVICE_INFO` | aktiv | Device-Infos (Board, Revision, SOC, ID) | | `0x02` | `DEVICE_INFO` | aktiv | Device-Infos (Board, Revision, SOC, ID) |
| `0x03` | `FS_INFO` | aktiv | Dateisystem- und Pfadinfos | | `0x03` | `FS_INFO` | aktiv | Dateisystem- und Pfadinfos |
| `0x04` | `FW_INFO` | aktiv | Info über Firmware-Status und -Version sowie Kernelversion | | `0x04` | `FW_INFO` | aktiv | Info über Firmware-Status und -Version sowie Kernelversion |
| `0x05` | `BATT_INFO` | aktiv | Info über die Batterie |
| `0x20` | `FILE_GET` | aktiv | Datei vom Device streamen | | `0x20` | `FILE_GET` | aktiv | Datei vom Device streamen |
| `0x21` | `FILE_PUT` | aktiv | Datei zum Device hochladen | | `0x21` | `FILE_PUT` | aktiv | Datei zum Device hochladen |
| `0x22` | `TAGS_GET` | aktiv | nur Tag-Bereich streamen | | `0x22` | `TAGS_GET` | aktiv | nur Tag-Bereich streamen |
@@ -238,6 +239,7 @@ Request: keine Zusatzdaten
Response: Response:
```c ```c
uint8_t data_type; /* 0x04 */
uint8_t fw_status; /* 0x00: Confirmed, 0x01: Pending, 0x02: Testing, 0xFF: Unbekannt */ uint8_t fw_status; /* 0x00: Confirmed, 0x01: Pending, 0x02: Testing, 0xFF: Unbekannt */
uint32_t slot1_size; /* (LE) Grösse des Firmware Update Slots */ uint32_t slot1_size; /* (LE) Grösse des Firmware Update Slots */
uint8_t fw_version_len; /* Länge des Firmware-Versionsstring */ uint8_t fw_version_len; /* Länge des Firmware-Versionsstring */
@@ -247,6 +249,20 @@ uint8_t data[]; /* FW-Version und Kernelversion, ohne Nullterminier
***Hinweis:*** in der Aktuellen implementierung werden die Versionen auf 32 Zeichen limitiert. ***Hinweis:*** in der Aktuellen implementierung werden die Versionen auf 32 Zeichen limitiert.
### `BATT_INFO` (`0x05`)
Request: keine Zusatzdaten
Response:
```c
uint8_t data_type; /* 0x05 */
uint8_t batt_status; /* 0x00: Discharging, 0x01: Full, 0x02: Charging, 0x03: Error, 0x04: Unknown */
uint8_t batt_level; /* 0-4, Anzahl Striche für den Akku */
uint8_t batt_percent; /* Akku-Füllstand in Prozent */
uint16_t batt_voltage_mv; /* (LE) Batteriespannung in mV */
```
### `LS` (`0x40`) ### `LS` (`0x40`)
Request-Payload: Request-Payload:

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import FlashUsage from "./FlashUsage.svelte"; import FlashUsage from "./FlashUsage.svelte";
import { deviceInfo, fwInfo } from "../lib/store"; import { battInfo, deviceInfo, fwInfo } from "../lib/store";
import { FW_STATUS } from "../lib/protocol/constants"; import { BATT_STATUS, FW_STATUS } from "../lib/protocol/constants";
import { tooltip } from "../lib/actions/tooltip"; import { tooltip } from "../lib/actions/tooltip";
import { import {
CheckCircleIcon, CheckCircleIcon,
@@ -13,6 +13,52 @@
BatteryFullIcon, BatteryFullIcon,
BatteryChargingIcon, BatteryChargingIcon,
} from "phosphor-svelte"; } from "phosphor-svelte";
function clampBatteryLevel(level: number): number {
if (Number.isNaN(level)) return 0;
return Math.max(0, Math.min(4, Math.trunc(level)));
}
$: resolvedBattIcon = (() => {
if (!$battInfo) return BatteryEmptyIcon;
if ($battInfo.battStatus === BATT_STATUS.CHARGING) return BatteryChargingIcon;
switch (clampBatteryLevel($battInfo.battLevel)) {
case 0:
return BatteryEmptyIcon;
case 1:
return BatteryLowIcon;
case 2:
return BatteryMediumIcon;
case 3:
return BatteryHighIcon;
default:
return BatteryFullIcon;
}
})();
$: battStatusText = (() => {
if (!$battInfo) return "unbekannt";
switch ($battInfo.battStatus) {
case BATT_STATUS.DISCHARGING:
return "Entladen";
case BATT_STATUS.FULL:
return "Voll";
case BATT_STATUS.CHARGING:
return "Laden";
case BATT_STATUS.ERROR:
return "Fehler";
default:
return "Unbekannt";
}
})();
$: battIconClass =
$battInfo?.battStatus === BATT_STATUS.ERROR
? "w-5 h-5 text-red-500"
: $battInfo?.battStatus === BATT_STATUS.CHARGING
? "w-5 h-5 text-emerald-600"
: "w-5 h-5";
</script> </script>
<div class="text-sm"> <div class="text-sm">
@@ -127,7 +173,14 @@
<tr> <tr>
<td class="key">Batterie</td> <td class="key">Batterie</td>
<td class="value flex items-center gap-2"> <td class="value flex items-center gap-2">
85% <BatteryChargingIcon weight="bold" class="w-5 h-5" /> 1200mAh {#if $battInfo}
<span>{$battInfo.battPercent}%</span>
<svelte:component this={resolvedBattIcon} weight="bold" class={battIconClass} />
<span class="text-text-muted">{$battInfo.battVoltageMv} mV</span>
<span class="text-text-muted">({battStatusText})</span>
{:else}
unbekannt
{/if}
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@@ -27,6 +27,7 @@ export const DATA = {
DEVICE_INFO: 0x02, DEVICE_INFO: 0x02,
FS_INFO: 0x03, FS_INFO: 0x03,
FW_INFO: 0x04, FW_INFO: 0x04,
BATT_INFO: 0x05,
FILE_GET: 0x20, FILE_GET: 0x20,
FILE_PUT: 0x21, FILE_PUT: 0x21,
@@ -66,3 +67,11 @@ export const FW_STATUS = {
TESTING: 0x02, TESTING: 0x02,
UNKNOWN: 0xFF, UNKNOWN: 0xFF,
} }
export const BATT_STATUS = {
DISCHARGING: 0x00,
FULL: 0x01,
CHARGING: 0x02,
ERROR: 0x03,
UNKNOWN: 0x04,
};

View File

@@ -1,5 +1,5 @@
import { FRAME, DATA, ZEPHYR_ERRORS } from './constants'; import { FRAME, DATA, ZEPHYR_ERRORS } from './constants';
import { protocolInfo, deviceInfo, fsInfo, transferStats, fwInfo, resetTransferStats, transferDetails } from '../store'; import { protocolInfo, deviceInfo, fsInfo, transferStats, fwInfo, battInfo, resetTransferStats, transferDetails } from '../store';
import { addToast } from '../toast'; import { addToast } from '../toast';
import { SETTINGS } from '../settings'; import { SETTINGS } from '../settings';
import { crc32 } from './crc32'; import { crc32 } from './crc32';
@@ -84,6 +84,18 @@ export function parseIncomingFrame(view: DataView, sender: FrameSender) {
const fwVersion = new TextDecoder().decode(new Uint8Array(view.buffer, 11, fw_version_length)); const fwVersion = new TextDecoder().decode(new Uint8Array(view.buffer, 11, fw_version_length));
const kernelVersion = new TextDecoder().decode(new Uint8Array(view.buffer, 11 + fw_version_length, kernel_version_length)); const kernelVersion = new TextDecoder().decode(new Uint8Array(view.buffer, 11 + fw_version_length, kernel_version_length));
fwInfo.set({ fwStatus, slot1Size, fwVersion, kernelVersion }); fwInfo.set({ fwStatus, slot1Size, fwVersion, kernelVersion });
break;
case DATA.BATT_INFO:
if (payloadLength < 6) {
console.warn(`Invalid BATT_INFO payload length: ${payloadLength}`);
break;
}
const battStatus = view.getUint8(4);
const battLevel = view.getUint8(5);
const battPercent = view.getUint8(6);
const battVoltageMv = view.getUint16(7, true);
battInfo.set({ battStatus, battLevel, battPercent, battVoltageMv });
break;
} }
break; break;
@@ -287,7 +299,14 @@ export function parseIncomingFrame(view: DataView, sender: FrameSender) {
case FRAME.ERROR: case FRAME.ERROR:
const errorCode = view.getUint16(3, true); const errorCode = view.getUint16(3, true);
console.error(`Received error frame with code: 0x${errorCode.toString(16)}`); const errorInfo = ZEPHYR_ERRORS[errorCode];
if (errorInfo) {
console.error(
`Received error frame: 0x${errorCode.toString(16).padStart(2, '0')} (${errorInfo.zephyr}) - ${errorInfo.text}`
);
} else {
console.error(`Received error frame with code: 0x${errorCode.toString(16).padStart(2, '0')}`);
}
showErrorToast(errorCode); showErrorToast(errorCode);
if (lsReject) { if (lsReject) {
const currentReject = lsReject; const currentReject = lsReject;
@@ -358,9 +377,21 @@ export function buildFWInfoRequest(): ArrayBuffer {
return buffer; return buffer;
} }
export function buildBattInfoRequest(): ArrayBuffer {
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint8(0, FRAME.REQUEST);
view.setUint16(1, 1, true);
view.setUint8(3, DATA.BATT_INFO);
return buffer;
}
export function buildLSRequest(path: string): ArrayBuffer { export function buildLSRequest(path: string): ArrayBuffer {
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const pathBytes = encoder.encode(path); const pathBytes = encoder.encode(path);
console.debug(`[Protocol] LS request for path: ${path}`);
const buffer = new ArrayBuffer(4 + pathBytes.length); const buffer = new ArrayBuffer(4 + pathBytes.length);
const view = new DataView(buffer); const view = new DataView(buffer);
@@ -442,6 +473,7 @@ export function setFileGetResolver(
export function buildFileGetRequest(path: string): ArrayBuffer { export function buildFileGetRequest(path: string): ArrayBuffer {
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const pathBytes = encoder.encode(path); const pathBytes = encoder.encode(path);
console.debug(`[Protocol] FILE_GET request for path: ${path}`);
const buffer = new ArrayBuffer(4 + pathBytes.length); const buffer = new ArrayBuffer(4 + pathBytes.length);
const view = new DataView(buffer); const view = new DataView(buffer);
@@ -458,6 +490,7 @@ export function buildFileGetRequest(path: string): ArrayBuffer {
export function buildTagsGetRequest(path: string): ArrayBuffer { export function buildTagsGetRequest(path: string): ArrayBuffer {
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const pathBytes = encoder.encode(path); const pathBytes = encoder.encode(path);
console.debug(`[Protocol] TAGS_GET request for path: ${path}`);
const buffer = new ArrayBuffer(4 + pathBytes.length); const buffer = new ArrayBuffer(4 + pathBytes.length);
const view = new DataView(buffer); const view = new DataView(buffer);

View File

@@ -7,6 +7,7 @@ export const SETTINGS = {
bluetooth: { bluetooth: {
connectionTimeoutMs: 3000, // Timeout für den Verbindungsaufbau connectionTimeoutMs: 3000, // Timeout für den Verbindungsaufbau
appleMaxInflight: 15, // iOS erlaubt nur wenige unbestätigte Nachrichten, daher begrenzen wir die Anzahl der gleichzeitig gesendeten Frames appleMaxInflight: 15, // iOS erlaubt nur wenige unbestätigte Nachrichten, daher begrenzen wir die Anzahl der gleichzeitig gesendeten Frames
batteryPollIntervalMs: 60_000, // Intervall für periodische BATT_INFO-Abfragen
}, },
ui: { ui: {
toastDurationMs: 5000, toastDurationMs: 5000,

View File

@@ -53,6 +53,13 @@ export interface FwInfo {
kernelVersion: string; kernelVersion: string;
} }
export interface BattInfo {
battStatus: number;
battLevel: number;
battPercent: number;
battVoltageMv: number;
}
export interface StorageUsage { export interface StorageUsage {
totalBytes: number; totalBytes: number;
freeBytes: number; freeBytes: number;
@@ -84,6 +91,7 @@ export const protocolInfo = writable<ProtocolInfo | null>(null);
export const deviceInfo = writable<DeviceInfo | null>(null); export const deviceInfo = writable<DeviceInfo | null>(null);
export const fsInfo = writable<FsInfo | null>(null); export const fsInfo = writable<FsInfo | null>(null);
export const fwInfo = writable<FwInfo | null>(null); export const fwInfo = writable<FwInfo | null>(null);
export const battInfo = writable<BattInfo | null>(null);
// Dateilisten // Dateilisten
export const buzzerAudioFiles = writable<BuzzerFile[]>([]); export const buzzerAudioFiles = writable<BuzzerFile[]>([]);
@@ -277,6 +285,7 @@ export function resetRemote(): void {
deviceInfo.set(null); deviceInfo.set(null);
fsInfo.set(null); fsInfo.set(null);
fwInfo.set(null); fwInfo.set(null);
battInfo.set(null);
activeDeviceId.set(null); activeDeviceId.set(null);
buzzerAudioFiles.set([]); buzzerAudioFiles.set([]);
buzzerSysFiles.set([]); buzzerSysFiles.set([]);

View File

@@ -1,6 +1,6 @@
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { isConnected, deviceInfo, fsInfo, fwInfo, buzzerAudioFiles, buzzerSysFiles, isTransferingRemote, isFetchingLocal, storageUsage, localAudioFiles, transferStats } from './store'; import { isConnected, deviceInfo, fsInfo, fwInfo, buzzerAudioFiles, buzzerSysFiles, isTransferingRemote, isFetchingLocal, storageUsage, localAudioFiles, transferStats } from './store';
import { requestProtocolInfo, requestFSInfo, fetchDirectory, getFile, putFile, deleteRemoteFile, requestDeviceInfo, requestFWInfo } from './transport'; import { requestProtocolInfo, requestFSInfo, fetchDirectory, getFile, putFile, deleteRemoteFile, requestDeviceInfo, requestFWInfo, requestBattInfo } from './transport';
import type { BuzzerFile } from './types'; import type { BuzzerFile } from './types';
import { addToast } from './toast'; import { addToast } from './toast';
import { getLocalFiles, deleteLocalFile, getLocalFile } from './db'; import { getLocalFiles, deleteLocalFile, getLocalFile } from './db';
@@ -28,18 +28,25 @@ export async function refreshRemote() {
await requestProtocolInfo(); await requestProtocolInfo();
await requestFSInfo(); await requestFSInfo();
await requestFWInfo(); await requestFWInfo();
await requestBattInfo();
await requestDeviceInfo(); await requestDeviceInfo();
// Kurze Verzögerung für Store-Propagation // Kurze Verzögerung für Store-Propagation
await new Promise(r => setTimeout(r, 100)); await new Promise(r => setTimeout(r, 100));
const currentFsInfo = get(fsInfo); const currentFsInfo = get(fsInfo);
console.debug("[Sync] Remote FS info:", currentFsInfo);
// Sequenzielle Abfrage via Transport-Layer // Sequenzielle Abfrage via Transport-Layer
const sysFiles = await fetchDirectory(currentFsInfo?.sysPath || "/lfs/sys"); const sysPath = currentFsInfo?.sysPath || "/lfs/sys";
const audioPath = currentFsInfo?.audioPath || "/lfs/a";
console.debug(`[Sync] Listing system directory: ${sysPath}`);
const sysFiles = await fetchDirectory(sysPath);
buzzerSysFiles.set(sysFiles.map(mapToBuzzerFile)); buzzerSysFiles.set(sysFiles.map(mapToBuzzerFile));
const audioFiles = await fetchDirectory(currentFsInfo?.audioPath || "/lfs/a"); console.debug(`[Sync] Listing audio directory: ${audioPath}`);
const audioFiles = await fetchDirectory(audioPath);
let mappedAudio = audioFiles.map(mapToBuzzerFile); let mappedAudio = audioFiles.map(mapToBuzzerFile);
// Dateien sofort im UI anzeigen, bevor die Tags geladen sind // Dateien sofort im UI anzeigen, bevor die Tags geladen sind
@@ -101,6 +108,7 @@ export async function refreshLocal() {
export async function downloadSelectedFiles() { export async function downloadSelectedFiles() {
const files = get(buzzerAudioFiles).filter(f => f.selected); const files = get(buzzerAudioFiles).filter(f => f.selected);
const pathPrefix = get(fsInfo)?.audioPath || "/lfs/a"; const pathPrefix = get(fsInfo)?.audioPath || "/lfs/a";
console.debug(`[Sync] Download prefix: ${pathPrefix}`);
if (files.length === 0) { if (files.length === 0) {
addToast("Keine Dateien zum Herunterladen ausgewählt.", "warning"); addToast("Keine Dateien zum Herunterladen ausgewählt.", "warning");
@@ -181,6 +189,7 @@ export async function deleteSelectedLocalFiles() {
export async function deleteSelectedRemoteFiles() { export async function deleteSelectedRemoteFiles() {
const files = get(buzzerAudioFiles).filter(f => f.selected); const files = get(buzzerAudioFiles).filter(f => f.selected);
const pathPrefix = get(fsInfo)?.audioPath || "/lfs/a"; const pathPrefix = get(fsInfo)?.audioPath || "/lfs/a";
console.debug(`[Sync] Delete prefix: ${pathPrefix}`);
if (files.length === 0) return; if (files.length === 0) return;
@@ -208,6 +217,7 @@ export async function deleteSelectedRemoteFiles() {
export async function uploadSelectedFiles() { export async function uploadSelectedFiles() {
const files = get(localAudioFiles).filter(f => f.selected); const files = get(localAudioFiles).filter(f => f.selected);
const pathPrefix = get(fsInfo)?.audioPath || "/lfs/a"; const pathPrefix = get(fsInfo)?.audioPath || "/lfs/a";
console.debug(`[Sync] Upload prefix: ${pathPrefix}`);
if (files.length === 0) { if (files.length === 0) {
addToast("Keine Dateien zum Hochladen ausgewählt.", "warning"); addToast("Keine Dateien zum Hochladen ausgewählt.", "warning");

View File

@@ -1,4 +1,4 @@
import { buildLSRequest, buildProtocolInfoRequest, buildDeviceInfoRequest, buildFSInfoRequest, buildFWInfoRequest, setLsResolver, buildFileGetRequest, setFileGetResolver, buildTagsGetRequest, uploadState } from './protocol/parser'; import { buildLSRequest, buildProtocolInfoRequest, buildDeviceInfoRequest, buildFSInfoRequest, buildFWInfoRequest, buildBattInfoRequest, setLsResolver, buildFileGetRequest, setFileGetResolver, buildTagsGetRequest, uploadState } from './protocol/parser';
import { crc32 } from './protocol/crc32'; import { crc32 } from './protocol/crc32';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { protocolInfo, transferStats, } from './store'; import { protocolInfo, transferStats, } from './store';
@@ -10,9 +10,45 @@ const isMac = navigator.userAgent.includes('Macintosh') || navigator.userAgent.i
const MAX_INFLIGHT = isMac ? SETTINGS.bluetooth.appleMaxInflight : Infinity; // iOS erlaubt nur wenige unbestätigte Nachrichten const MAX_INFLIGHT = isMac ? SETTINGS.bluetooth.appleMaxInflight : Infinity; // iOS erlaubt nur wenige unbestätigte Nachrichten
console.log("Transport: Max Inflight Frames =", MAX_INFLIGHT); console.log("Transport: Max Inflight Frames =", MAX_INFLIGHT);
const BATT_POLL_INTERVAL_MS = Math.max(1_000, SETTINGS.bluetooth.batteryPollIntervalMs);
export type FrameSender = (buffer: ArrayBuffer) => Promise<void>; export type FrameSender = (buffer: ArrayBuffer) => Promise<void>;
let currentSender: FrameSender | null = null; let currentSender: FrameSender | null = null;
let battPollTimer: ReturnType<typeof setInterval> | null = null;
let isBattPollInFlight = false;
function stopBattPolling() {
if (battPollTimer) {
clearInterval(battPollTimer);
battPollTimer = null;
}
}
function shouldSkipBattPoll(): boolean {
return isListing || isFileTransferring || uploadState.active;
}
async function pollBatteryInfo() {
if (!currentSender || isBattPollInFlight || shouldSkipBattPoll()) {
return;
}
isBattPollInFlight = true;
try {
await requestBattInfo();
} catch (error) {
console.debug("Periodic BATT_INFO request failed:", error);
} finally {
isBattPollInFlight = false;
}
}
function startBattPolling() {
stopBattPolling();
battPollTimer = setInterval(() => {
void pollBatteryInfo();
}, BATT_POLL_INTERVAL_MS);
}
export function registerTransport(sender: FrameSender | null) { export function registerTransport(sender: FrameSender | null) {
currentSender = sender; currentSender = sender;
@@ -21,6 +57,7 @@ export function registerTransport(sender: FrameSender | null) {
// NEU: Wird von bluetooth.ts oder serial.ts nach dem physischen Connect gerufen // NEU: Wird von bluetooth.ts oder serial.ts nach dem physischen Connect gerufen
export async function handleTransportConnect(sender: FrameSender) { export async function handleTransportConnect(sender: FrameSender) {
registerTransport(sender); registerTransport(sender);
stopBattPolling();
try { try {
// Basis-Informationen zwingend vorab laden // Basis-Informationen zwingend vorab laden
@@ -28,9 +65,11 @@ export async function handleTransportConnect(sender: FrameSender) {
await requestFSInfo(); await requestFSInfo();
await requestDeviceInfo(); await requestDeviceInfo();
await requestFWInfo(); await requestFWInfo();
await requestBattInfo();
// Erst wenn diese Basisdaten da sind, wird die UI freigeschaltet // Erst wenn diese Basisdaten da sind, wird die UI freigeschaltet
isConnected.set(true); isConnected.set(true);
startBattPolling();
} catch (error) { } catch (error) {
console.error("Transport-Initialisierung fehlgeschlagen:", error); console.error("Transport-Initialisierung fehlgeschlagen:", error);
handleTransportDisconnect(); handleTransportDisconnect();
@@ -58,6 +97,10 @@ export async function requestFWInfo() {
await sendFrame(buildFWInfoRequest()); await sendFrame(buildFWInfoRequest());
} }
export async function requestBattInfo() {
await sendFrame(buildBattInfoRequest());
}
let isListing = false; let isListing = false;
export async function fetchDirectory(path: string): Promise<any[]> { export async function fetchDirectory(path: string): Promise<any[]> {
@@ -66,6 +109,8 @@ export async function fetchDirectory(path: string): Promise<any[]> {
} }
isListing = true; isListing = true;
console.debug(`[Transport] fetchDirectory(${path})`);
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
// Dem Parser sagen, wen er bei Erfolg/Fehler anrufen soll // Dem Parser sagen, wen er bei Erfolg/Fehler anrufen soll
setLsResolver( setLsResolver(
@@ -83,6 +128,7 @@ export async function fetchDirectory(path: string): Promise<any[]> {
} }
export function handleTransportDisconnect() { export function handleTransportDisconnect() {
stopBattPolling();
registerTransport(null); registerTransport(null);
resetRemote(); resetRemote();
} }
@@ -95,6 +141,8 @@ export async function getFile(path: string): Promise<boolean> {
} }
isFileTransferring = true; isFileTransferring = true;
console.debug(`[Transport] getFile(${path})`);
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
setFileGetResolver( setFileGetResolver(
(result: any) => { isFileTransferring = false; resolve(result.success); }, (result: any) => { isFileTransferring = false; resolve(result.success); },
@@ -116,6 +164,8 @@ export async function getTags(path: string): Promise<Blob> {
} }
isFileTransferring = true; isFileTransferring = true;
console.debug(`[Transport] getTags(${path})`);
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
setFileGetResolver( setFileGetResolver(
(result: any) => { (result: any) => {