8 Commits

Author SHA1 Message Date
78f0bce5dd Sync 2026-06-04 14:52:37 +02:00
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
49 changed files with 1769 additions and 381 deletions

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

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

View File

@@ -6,5 +6,9 @@
"nrf-connect.boardRoots": [
"${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",
"${workspaceFolder}/firmware/build_buzzy/firmware": "Launch firmware/build_buzzy/firmware"
}
}

26
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,26 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "MAC RTT Streamer",
"type": "shell",
"command": "bash",
"args": [
"-c",
"while true; do nc localhost 19021; sleep 0.2; done"
],
"presentation": {
"echo": false,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": true
},
"runOptions": {
"runOn": "folderOpen"
},
"problemMatcher": []
}
]
}

View File

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

View File

@@ -1,11 +1,2 @@
### Console / Logging: disabled in base board config (enable via debug snippet)
CONFIG_USE_SEGGER_RTT=n
CONFIG_CONSOLE=n
CONFIG_RTT_CONSOLE=n
CONFIG_LOG_BACKEND_RTT=n
CONFIG_UART_CONSOLE=n
CONFIG_LOG_BACKEND_UART=n
# Keep SPI NOR page layout aligned with generated LittleFS block size (4KB).
CONFIG_SPI_NOR_FLASH_LAYOUT_PAGE_SIZE=4096
# Keep QSPI NOR page layout aligned with generated LittleFS block size (4KB).
CONFIG_NORDIC_QSPI_NOR_FLASH_LAYOUT_PAGE_SIZE=4096

View File

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

View File

@@ -54,7 +54,7 @@
charger_status {
compatible = "gpio-keys";
chg_status: chg_status {
gpios = <&gpio0 13 GPIO_ACTIVE_HIGH>;
gpios = <&gpio0 13 (GPIO_ACTIVE_HIGH | GPIO_PULL_UP)>;
label = "ETA6003_CHG_STATUS";
};
};
@@ -137,17 +137,52 @@
* SO/SIO1-> P0.30
* 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";
pinctrl-0 = <&spi3_default>;
pinctrl-1 = <&spi3_sleep>;
pinctrl-0 = <&qspi_default>;
pinctrl-1 = <&qspi_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
mx25r64: flash@0 {
compatible = "jedec,spi-nor";
compatible = "nordic,qspi-nor";
reg = <0>;
spi-max-frequency = <8000000>;
sck-frequency = <32000000>;
jedec-id = [c2 28 17];
size = <DT_SIZE_M(64)>;
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 {
status = "okay";

View File

@@ -1,5 +0,0 @@
### Console / Logging: disabled in base board config (enable via debug snippet)
CONFIG_CONSOLE=n
CONFIG_UART_CONSOLE=n
CONFIG_LOG_BACKEND_UART=n
CONFIG_LOG_BACKEND_RTT=n

View File

@@ -34,3 +34,16 @@
pinctrl-1 = <&i2s0_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,9 +1,29 @@
### Logging
CONFIG_LOG=y
#CONFIG_LOG_MODE_IMMEDIATE=y
CONFIG_DEBUG=y
CONFIG_AUDIO_LOG_LEVEL_DBG=y
CONFIG_DEBUG_OPTIMIZATIONS=y
CONFIG_INIT_STACKS=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
CONFIG_BUZZ_PROTO_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

View File

@@ -4,5 +4,12 @@ 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

View File

@@ -3,5 +3,8 @@ 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():
shutil.rmtree(staging_root)
out_sys = staging_root / "lfs" / "sys"
out_a = staging_root / "lfs" / "a"
# The LittleFS partition is mounted at /lfs in firmware, so image root
# 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_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
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

@@ -5,3 +5,5 @@ add_subdirectory(ble_mgmt)
add_subdirectory(buzz_proto)
add_subdirectory(audio)
add_subdirectory(event_mgmt)
add_subdirectory(batt_mgmt)
add_subdirectory(usb_mgmt)

View File

@@ -5,3 +5,5 @@ rsource "ble_mgmt/Kconfig"
rsource "buzz_proto/Kconfig"
rsource "audio/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);
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)
{
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,86 +3,39 @@ menuconfig BLE_MGMT
default n
select BT
select BT_PERIPHERAL
select BT_LOG_LEVEL_WARN
select BT_DEVICE_NAME_DYNAMIC
help
Library for initializing and managing Bluetooth functionality.
Minimal BLE transport for the buzzer firmware.
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
string "Default Bluetooth Device Name"
default "Edis Buzzer"
config BLE_MGMT_ADV_INT_MIN
int "Minimum Advertising Interval (in 0.625 ms units)"
default 160
string "Default Bluetooth device name"
default "Edis Buzzer 2.0"
help
Minimal advertising interval. 160 equals to 100ms.
config BLE_MGMT_ADV_INT_MAX
int "Maximum Advertising Interval (ms)"
default 160
help
Maximal advertising interval. 160 equals to 100ms.
Device name used when ble_mgmt_init() is called with a NULL name.
# 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_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
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-str = ble_mgmt
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.
* @param data Pointer to the data buffer to send.
* @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);

View File

@@ -1,15 +1,16 @@
#include <zephyr/bluetooth/bluetooth.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 <errno.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 "event_mgmt.h"
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 \
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_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 ble_mgmt_rx_cb_t app_rx_cb = NULL;
static bool notify_enabled = false;
static uint16_t current_tx_mtu = 23;
static uint16_t current_rx_mtu = 23;
#define MAX_ADV_NAME_LEN 29
static ble_mgmt_rx_cb_t app_rx_cb;
static bool ble_ready;
static bool advertising_active;
static uint16_t current_att_mtu = DEFAULT_ATT_MTU;
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[] = {
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 = {
.id = BT_ID_DEFAULT,
.sid = 0,
.secondary_max_skip = 0,
.options = BT_LE_ADV_OPT_CONN,
.interval_min = CONFIG_BLE_MGMT_ADV_INT_MIN,
.interval_max = CONFIG_BLE_MGMT_ADV_INT_MAX,
.peer = NULL,
.interval_min = BT_GAP_ADV_FAST_INT_MIN_2,
.interval_max = BT_GAP_ADV_FAST_INT_MAX_2,
};
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);
current_tx_mtu = tx;
current_rx_mtu = rx;
ARG_UNUSED(conn);
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,
const void *buf, uint16_t len, uint16_t offset, uint8_t flags)
{
LOG_DBG("Received %u bytes", len);
LOG_HEXDUMP_DBG(buf, len, "Data:");
ARG_UNUSED(conn);
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);
}
return len;
}
static void tx_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value)
{
notify_enabled = (value == BT_GATT_CCC_NOTIFY);
LOG_INF("Notifications %s", notify_enabled ? "enabled" : "disabled");
ARG_UNUSED(attr);
LOG_INF("Notification state changed: 0x%04x", value);
}
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_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)
{
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);
return;
}
char addr_str[BT_ADDR_LE_STR_LEN];
struct bt_conn_info info;
advertising_active = false;
int rc = bt_conn_get_info(conn, &info);
if (rc == 0)
{
if (reserve_peer(conn) == NULL) {
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));
LOG_INF("Connected to %s", addr_str);
LOG_INF("Role: %s", info.role == BT_CONN_ROLE_CENTRAL ? "Central" : "Peripheral");
LOG_INF("Connected to %s (%u/%u)", addr_str, (unsigned int)peer_count(),
CONFIG_BT_MAX_CONN);
} else {
LOG_INF("Connected (%u/%u)", (unsigned int)peer_count(), CONFIG_BT_MAX_CONN);
}
else
{
LOG_INF("Connected (info retrieval failed)");
if (peer_count() < CONFIG_BT_MAX_CONN) {
(void)start_advertising();
}
event_mgmt_set_event(EVENT_MGMT_BLE_CONNECTED);
}
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 */
int rc = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
if (rc)
{
LOG_ERR("Advertising failed to restart (err %d)", rc);
}
else
{
LOG_INF("Advertising successfully restarted");
}
LOG_INF("Disconnected (reason 0x%02x, %u/%u active)", reason,
(unsigned int)peer_count(), CONFIG_BT_MAX_CONN);
if (peer_count() < CONFIG_BT_MAX_CONN) {
(void)start_advertising();
}
static void le_phy_updated(struct bt_conn *conn, struct bt_conn_le_phy_info *param)
{
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);
event_mgmt_set_event(EVENT_MGMT_BLE_DISCONNECTED);
}
BT_CONN_CB_DEFINE(conn_callbacks) = {
.connected = connected,
.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 rc;
app_rx_cb = rx_cb;
static struct bt_gatt_cb gatt_callbacks = {
.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);
if (rc)
{
if (rc != 0) {
LOG_ERR("Bluetooth init failed (err %d)", rc);
return rc;
}
const char *name_to_use = (device_name != NULL) ? device_name : CONFIG_BLE_MGMT_DEFAULT_DEVICE_NAME;
LOG_INF("BLE init: set_device_name");
set_device_name(name_to_use);
LOG_INF("ble_mgmt_init: bt_enable done, marking ready");
ble_ready = true;
bt_gatt_cb_register(&gatt_callbacks);
advertising_active = false;
set_device_name(device_name);
LOG_INF("BLE init: bt_le_adv_start");
rc = bt_le_adv_start(&adv_param, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
if (rc)
{
LOG_ERR("Advertising failed to start (err %d)", rc);
LOG_INF("ble_mgmt_init: calling start_advertising");
rc = start_advertising();
if (rc != 0) {
LOG_ERR("start_advertising failed (err %d)", rc);
return rc;
}
LOG_INF("Bluetooth initialized. Adv-Name: %s", current_device_name);
LOG_INF("ble_mgmt_init: complete");
return 0;
}

View File

@@ -36,6 +36,7 @@ enum buzz_data_type
BUZZ_DATA_DEVICE_INFO = 0x02,
BUZZ_DATA_FS_INFO = 0x03,
BUZZ_DATA_FW_INFO = 0x04,
BUZZ_DATA_BATT_INFO = 0x05,
BUZZ_DATA_FILE_GET = 0x20,
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 */
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 */
struct __attribute__((packed)) buzz_rm_file_payload
{

View File

@@ -7,6 +7,7 @@
#include <stdlib.h>
#include "buzz_proto.h"
#include "batt_mgmt.h"
#include "fs_mgmt.h"
#include "fw_mgmt.h"
@@ -293,6 +294,43 @@ 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));
}
LOG_DBG("Battery Info sent: state=%u, level=%u, percent=%u, voltage=%u mV",
batt_info.state, batt_info.level, batt_info.percent, batt_info.voltage_mv);
}
static void handle_ls_request(struct buzz_frame_msg *msg)
{
struct buzz_proto_header *hdr = (struct buzz_proto_header *)msg->data_ptr;
@@ -698,6 +736,11 @@ static void handle_request(struct buzz_frame_msg *msg)
handle_fw_info_request(msg);
break;
case BUZZ_DATA_BATT_INFO:
LOG_DBG("Received BATT Info Request");
handle_batt_info_request(msg);
break;
case BUZZ_DATA_FILE_GET:
LOG_DBG("Received FILE_GET Request");
handle_file_get_request(msg, false);

View File

@@ -7,6 +7,8 @@
#define EVENT_MGMT_AUDIO_READY BIT(1)
#define EVENT_MGMT_BLE_CONNECTED BIT(2)
#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;

View File

@@ -7,7 +7,7 @@ menuconfig FS_MGMT
select FILE_SYSTEM_LITTLEFS
select FILE_SYSTEM_MKFS
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 NORDIC_QSPI_NOR if BOARD_NRF52840DK_NRF52840
help

View File

@@ -17,7 +17,7 @@ LOG_MODULE_REGISTER(fs_mgmt, CONFIG_FS_MGMT_LOG_LEVEL);
* Without PM, we fall back to the DTS node label.
*/
#if defined(PM_littlefs_storage_ID)
#define FokITION_ID(littlefs_storage)
#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)
#else
@@ -38,8 +38,10 @@ static struct fs_mount_t fs_storage_mnt = {
.mnt_point = CONFIG_FS_MGMT_MOUNT_POINT,
};
#if defined(CONFIG_PM_DEVICE) && !defined(CONFIG_FILE_SYSTEM_SHELL)
static int open_count = 0;
static struct k_mutex flash_pm_lock;
#endif
// #define ACK_WATERMARK (CONFIG_BUZZ_PROTO_SLAB_COUNT / 4)
#define INITIAL_CREDITS CONFIG_BUZZ_PROTO_SLAB_COUNT
@@ -79,7 +81,9 @@ static struct
*/
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))
{
return -ENODEV;
@@ -105,7 +109,7 @@ static int fs_mgmt_pm_flash_suspend(void)
}
k_mutex_unlock(&flash_pm_lock);
#endif /* CONFIG_PM_DEVICE */
#endif /* CONFIG_FILE_SYSTEM_SHELL / CONFIG_PM_DEVICE */
return 0;
}
@@ -116,7 +120,9 @@ static int fs_mgmt_pm_flash_suspend(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))
return -ENODEV;
@@ -134,7 +140,7 @@ static int fs_mgmt_pm_flash_resume(void)
open_count++;
k_mutex_unlock(&flash_pm_lock);
#endif /* CONFIG_PM_DEVICE */
#endif /* CONFIG_FILE_SYSTEM_SHELL / CONFIG_PM_DEVICE */
return 0;
}
@@ -182,6 +188,20 @@ int fs_mgmt_pm_unlink(const char *path)
{
LOG_DBG("PM Unlinking file '%s'", path);
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);
fs_mgmt_pm_flash_suspend();
return rc;
@@ -400,7 +420,9 @@ int fs_mgmt_pm_mkdir_recursive(char *path)
static int fs_mgmt_init(void)
{
#if defined(CONFIG_PM_DEVICE) && !defined(CONFIG_FILE_SYSTEM_SHELL)
k_mutex_init(&flash_pm_lock);
#endif
if (!device_is_ready(flash_dev))
{

View File

@@ -2,6 +2,7 @@ menuconfig SETTINGS_MGMT
bool "Settings Management"
default y
select SETTINGS
select SETTINGS_RUNTIME
select ZMS
help
Library for initializing and managing the settings subsystem.

View File

@@ -27,7 +27,11 @@ LOG_MODULE_REGISTER(settings_mgmt, LOG_LEVEL_INF);
/* The "Source of Truth" in RAM */
struct app_settings_t app_cfg = {
.dev_name = "Edis Buzzer",
#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,

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,9 +1,3 @@
### Bluetooth
CONFIG_BLE_MGMT=y
### Audio
CONFIG_BUZZ_AUDIO=y
### Error handling
CONFIG_HW_STACK_PROTECTION=y
CONFIG_RESET_ON_FATAL_ERROR=y
@@ -11,5 +5,9 @@ CONFIG_RESET_ON_FATAL_ERROR=y
### Power management
CONFIG_PM_DEVICE=y
### Boot banner
CONFIG_NCS_APPLICATION_BOOT_BANNER_STRING="Edis Buzzer"
### Stack
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 <string.h>
#include "fs_mgmt.h"
#include "buzz_proto.h"
#include "fw_mgmt.h"
#include "settings_mgmt.h"
#include "batt_mgmt.h"
// #include "fw_mgmt.h"
// #include "audio.h"
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)
#include "ble_mgmt.h"
void ble_rx_cb(const uint8_t *data, uint16_t len)
@@ -49,24 +66,36 @@ void ble_rx_cb(const uint8_t *data, uint16_t len)
int main(void)
{
#if IS_ENABLED(CONFIG_BLE_MGMT)
/* 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) {
LOG_ERR("Failed to initialize BLE management: %d", rc);
return rc;
}
LOG_WRN("After BLE init");
#else
LOG_WRN("BLE not enabled");
#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);
}
#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 (;;) {
int32_t rem_ms = k_sleep(K_FOREVER);
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) |
| `0x03` | `FS_INFO` | aktiv | Dateisystem- und Pfadinfos |
| `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 |
| `0x21` | `FILE_PUT` | aktiv | Datei zum Device hochladen |
| `0x22` | `TAGS_GET` | aktiv | nur Tag-Bereich streamen |
@@ -238,6 +239,7 @@ Request: keine Zusatzdaten
Response:
```c
uint8_t data_type; /* 0x04 */
uint8_t fw_status; /* 0x00: Confirmed, 0x01: Pending, 0x02: Testing, 0xFF: Unbekannt */
uint32_t slot1_size; /* (LE) Grösse des Firmware Update Slots */
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.
### `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`)
Request-Payload:

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import FlashUsage from "./FlashUsage.svelte";
import { deviceInfo, fwInfo } from "../lib/store";
import { FW_STATUS } from "../lib/protocol/constants";
import { battInfo, deviceInfo, fwInfo } from "../lib/store";
import { BATT_STATUS, FW_STATUS } from "../lib/protocol/constants";
import { tooltip } from "../lib/actions/tooltip";
import {
CheckCircleIcon,
@@ -13,6 +13,52 @@
BatteryFullIcon,
BatteryChargingIcon,
} 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>
<div class="text-sm">
@@ -127,7 +173,14 @@
<tr>
<td class="key">Batterie</td>
<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>
</tr>
<tr>

View File

@@ -27,6 +27,7 @@ export const DATA = {
DEVICE_INFO: 0x02,
FS_INFO: 0x03,
FW_INFO: 0x04,
BATT_INFO: 0x05,
FILE_GET: 0x20,
FILE_PUT: 0x21,
@@ -66,3 +67,11 @@ export const FW_STATUS = {
TESTING: 0x02,
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 { 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 { SETTINGS } from '../settings';
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 kernelVersion = new TextDecoder().decode(new Uint8Array(view.buffer, 11 + fw_version_length, kernel_version_length));
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;
@@ -287,7 +299,14 @@ export function parseIncomingFrame(view: DataView, sender: FrameSender) {
case FRAME.ERROR:
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);
if (lsReject) {
const currentReject = lsReject;
@@ -358,9 +377,21 @@ export function buildFWInfoRequest(): ArrayBuffer {
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 {
const encoder = new TextEncoder();
const pathBytes = encoder.encode(path);
console.debug(`[Protocol] LS request for path: ${path}`);
const buffer = new ArrayBuffer(4 + pathBytes.length);
const view = new DataView(buffer);
@@ -442,6 +473,7 @@ export function setFileGetResolver(
export function buildFileGetRequest(path: string): ArrayBuffer {
const encoder = new TextEncoder();
const pathBytes = encoder.encode(path);
console.debug(`[Protocol] FILE_GET request for path: ${path}`);
const buffer = new ArrayBuffer(4 + pathBytes.length);
const view = new DataView(buffer);
@@ -458,6 +490,7 @@ export function buildFileGetRequest(path: string): ArrayBuffer {
export function buildTagsGetRequest(path: string): ArrayBuffer {
const encoder = new TextEncoder();
const pathBytes = encoder.encode(path);
console.debug(`[Protocol] TAGS_GET request for path: ${path}`);
const buffer = new ArrayBuffer(4 + pathBytes.length);
const view = new DataView(buffer);

View File

@@ -7,6 +7,7 @@ export const SETTINGS = {
bluetooth: {
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
batteryPollIntervalMs: 60_000, // Intervall für periodische BATT_INFO-Abfragen
},
ui: {
toastDurationMs: 5000,

View File

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

View File

@@ -1,6 +1,6 @@
import { get } from 'svelte/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 { addToast } from './toast';
import { getLocalFiles, deleteLocalFile, getLocalFile } from './db';
@@ -28,18 +28,25 @@ export async function refreshRemote() {
await requestProtocolInfo();
await requestFSInfo();
await requestFWInfo();
await requestBattInfo();
await requestDeviceInfo();
// Kurze Verzögerung für Store-Propagation
await new Promise(r => setTimeout(r, 100));
const currentFsInfo = get(fsInfo);
console.debug("[Sync] Remote FS info:", currentFsInfo);
// 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));
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);
// Dateien sofort im UI anzeigen, bevor die Tags geladen sind
@@ -101,6 +108,7 @@ export async function refreshLocal() {
export async function downloadSelectedFiles() {
const files = get(buzzerAudioFiles).filter(f => f.selected);
const pathPrefix = get(fsInfo)?.audioPath || "/lfs/a";
console.debug(`[Sync] Download prefix: ${pathPrefix}`);
if (files.length === 0) {
addToast("Keine Dateien zum Herunterladen ausgewählt.", "warning");
@@ -181,6 +189,7 @@ export async function deleteSelectedLocalFiles() {
export async function deleteSelectedRemoteFiles() {
const files = get(buzzerAudioFiles).filter(f => f.selected);
const pathPrefix = get(fsInfo)?.audioPath || "/lfs/a";
console.debug(`[Sync] Delete prefix: ${pathPrefix}`);
if (files.length === 0) return;
@@ -208,6 +217,7 @@ export async function deleteSelectedRemoteFiles() {
export async function uploadSelectedFiles() {
const files = get(localAudioFiles).filter(f => f.selected);
const pathPrefix = get(fsInfo)?.audioPath || "/lfs/a";
console.debug(`[Sync] Upload prefix: ${pathPrefix}`);
if (files.length === 0) {
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 { get } from 'svelte/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
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>;
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) {
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
export async function handleTransportConnect(sender: FrameSender) {
registerTransport(sender);
stopBattPolling();
try {
// Basis-Informationen zwingend vorab laden
@@ -28,9 +65,11 @@ export async function handleTransportConnect(sender: FrameSender) {
await requestFSInfo();
await requestDeviceInfo();
await requestFWInfo();
await requestBattInfo();
// Erst wenn diese Basisdaten da sind, wird die UI freigeschaltet
isConnected.set(true);
startBattPolling();
} catch (error) {
console.error("Transport-Initialisierung fehlgeschlagen:", error);
handleTransportDisconnect();
@@ -58,6 +97,10 @@ export async function requestFWInfo() {
await sendFrame(buildFWInfoRequest());
}
export async function requestBattInfo() {
await sendFrame(buildBattInfoRequest());
}
let isListing = false;
export async function fetchDirectory(path: string): Promise<any[]> {
@@ -66,6 +109,8 @@ export async function fetchDirectory(path: string): Promise<any[]> {
}
isListing = true;
console.debug(`[Transport] fetchDirectory(${path})`);
return new Promise(async (resolve, reject) => {
// Dem Parser sagen, wen er bei Erfolg/Fehler anrufen soll
setLsResolver(
@@ -83,6 +128,7 @@ export async function fetchDirectory(path: string): Promise<any[]> {
}
export function handleTransportDisconnect() {
stopBattPolling();
registerTransport(null);
resetRemote();
}
@@ -95,6 +141,8 @@ export async function getFile(path: string): Promise<boolean> {
}
isFileTransferring = true;
console.debug(`[Transport] getFile(${path})`);
return new Promise(async (resolve, reject) => {
setFileGetResolver(
(result: any) => { isFileTransferring = false; resolve(result.success); },
@@ -116,6 +164,8 @@ export async function getTags(path: string): Promise<Blob> {
}
isFileTransferring = true;
console.debug(`[Transport] getTags(${path})`);
return new Promise(async (resolve, reject) => {
setFileGetResolver(
(result: any) => {