6 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
40 changed files with 1357 additions and 119 deletions

7
.vscode/launch.json vendored
View File

@@ -7,6 +7,13 @@
"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

@@ -8,6 +8,7 @@
],
"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_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,2 +1,2 @@
# 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_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_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";

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,6 +1,6 @@
### Logging
CONFIG_LOG=y
CONFIG_LOG_MODE_IMMEDIATE=y
#CONFIG_LOG_MODE_IMMEDIATE=y
CONFIG_DEBUG=y
CONFIG_DEBUG_OPTIMIZATIONS=y
@@ -8,16 +8,22 @@ CONFIG_DEBUG_OPTIMIZATIONS=y
CONFIG_INIT_STACKS=y
CONFIG_THREAD_STACK_INFO=y
### Increase logging thread stack to prevent overflow when shell active
CONFIG_LOG_PROCESS_THREAD_STACK_SIZE=2048
CONFIG_LOG_BUFFER_SIZE=4096
### 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 subsystem logging (reduced noise)
### 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=n
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,4 +4,6 @@ add_subdirectory(fs_mgmt)
add_subdirectory(ble_mgmt)
add_subdirectory(buzz_proto)
add_subdirectory(audio)
add_subdirectory(event_mgmt)
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

@@ -10,7 +10,7 @@ menuconfig BLE_MGMT
if BLE_MGMT
config BLE_MGMT_DEFAULT_DEVICE_NAME
string "Default Bluetooth device name"
default "Edis Buzzer"
default "Edis Buzzer 2.0"
help
Device name used when ble_mgmt_init() is called with a NULL name.

View File

@@ -10,6 +10,7 @@
#include <zephyr/logging/log.h>
#include "ble_mgmt.h"
#include "event_mgmt.h"
LOG_MODULE_REGISTER(ble_mgmt, CONFIG_BLE_MGMT_LOG_LEVEL);
@@ -239,6 +240,8 @@ static void connected(struct bt_conn *conn, uint8_t err)
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)
@@ -252,6 +255,8 @@ static void disconnected(struct bt_conn *conn, uint8_t reason)
if (peer_count() < CONFIG_BT_MAX_CONN) {
(void)start_advertising();
}
event_mgmt_set_event(EVENT_MGMT_BLE_DISCONNECTED);
}
BT_CONN_CB_DEFINE(conn_callbacks) = {

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

@@ -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=n
### Error handling
CONFIG_HW_STACK_PROTECTION=y
CONFIG_RESET_ON_FATAL_ERROR=y
@@ -11,6 +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

@@ -3,11 +3,29 @@
#include <string.h>
#include "buzz_proto.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)
@@ -48,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");
// 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);
#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
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,
@@ -65,4 +66,12 @@ export const FW_STATUS = {
PENDING: 0x01,
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;
@@ -365,6 +377,17 @@ 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);

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,6 +28,7 @@ export async function refreshRemote() {
await requestProtocolInfo();
await requestFSInfo();
await requestFWInfo();
await requestBattInfo();
await requestDeviceInfo();
// Kurze Verzögerung für Store-Propagation

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[]> {
@@ -85,6 +128,7 @@ export async function fetchDirectory(path: string): Promise<any[]> {
}
export function handleTransportDisconnect() {
stopBattPolling();
registerTransport(null);
resetRemote();
}