19 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
32fe244fcf sync 2026-05-11 19:28:06 +02:00
5ae098962d feat: add settings mgmt and split debug transport snippets 2026-05-11 16:19:37 +02:00
3fed249430 fix: add full_name field to board.yml for NCS 3.3.0 schema compliance 2026-05-11 14:26:36 +02:00
7381173026 Merge buzzy_integration into main 2026-05-11 14:02:07 +02:00
e85d51488e Update filesystem tooling and LittleFS defaults 2026-05-11 14:01:56 +02:00
12e82d35d2 sync 2026-05-11 13:04:32 +02:00
7a2428f54d Merge branch 'buzzy_integration' of gitea.iten.pro:edi/buzzer_2 into buzzy_integration 2026-05-11 12:21:09 +02:00
fca4aeb115 sync 2026-05-11 12:18:14 +02:00
35639a2615 Zwischenstand 2026-05-11 11:49:20 +02:00
5d20d2820a feat(logging): board-specific log backends (RTT for buzzy, UART for DK) 2026-05-07 11:23:12 +02:00
ad2dc19641 feat(buzzy): add buzzy board support with SPI flash, fix build warnings
- Register BOARD_ROOT in CMakeLists.txt and sysbuild/CMakeLists.txt
- Add boardRoots to VS Code settings for board picker
- buzzy.dts: add nordic,pm-ext-flash chosen, external-flash and i2s-audio aliases
- nrf52840dk overlay: add external-flash alias (mirrors qspi-flash)
- fs_mgmt/Kconfig: select SPI_NOR for BOARD_BUZZY, NORDIC_QSPI_NOR for DK
- fs_mgmt.c: use external-flash alias instead of qspi-flash
- audio.c: bound snprintf to avoid truncation warning
- prj.conf: remove STACK_SENTINEL (conflicts with MPU_STACK_GUARD)
- mcuboot.conf: remove UART_CONSOLE (no SERIAL on buzzy)
- Delete mcuboot.overlay (no serial recovery needed)

Both buzzy/nrf52840 and nrf52840dk/nrf52840 build cleanly with zero warnings.
2026-05-07 11:00:14 +02:00
63 changed files with 2616 additions and 430 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"
}
]
}

10
.vscode/settings.json vendored
View File

@@ -2,5 +2,13 @@
"svelte.plugin.svelte.format.config.printWidth": 300,
"nrf-connect.applications": [
"${workspaceFolder}/firmware"
]
],
"nrf-connect.boardRoots": [
"${workspaceFolder}/firmware"
],
"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 = 2
PATCHLEVEL = 77
VERSION_TWEAK = 0
#if (IS_ENABLED(CONFIG_LOG))
EXTRAVERSION = debug

View File

@@ -0,0 +1,2 @@
# Keep QSPI NOR page layout aligned with generated LittleFS block size (4KB).
CONFIG_NORDIC_QSPI_NOR_FLASH_LAYOUT_PAGE_SIZE=4096

View File

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

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

@@ -14,6 +14,7 @@
zephyr,sram = &sram0;
zephyr,flash = &flash0;
zephyr,code-partition = &slot0_partition;
nordic,pm-ext-flash = &mx25r64;
};
/* SD_MODE pin for MAX98357A power gating */
@@ -53,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";
};
};
@@ -88,6 +89,7 @@
chg-status = &chg_status;
chg-fast = &chg_fast;
external-flash = &mx25r64;
i2s-audio = &i2s0;
};
};
@@ -135,19 +137,54 @@
* 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(8)>;
size = <DT_SIZE_M(64)>;
has-dpd;
t-enter-dpd = <10000>;
t-exit-dpd = <35000>;
@@ -165,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(8)>;
// 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

@@ -2,4 +2,4 @@ CONFIG_ARM_MPU=y
CONFIG_HW_STACK_PROTECTION=y
CONFIG_CLOCK_CONTROL_NRF_K32SRC_XTAL=y
CONFIG_CLOCK_CONTROL_NRF_K32SRC_20PPM=y
CONFIG_CLOCK_CONTROL_NRF_K32SRC_20PPM=y

View File

@@ -3,6 +3,7 @@
nordic,pm-ext-flash = &mx25r64;
};
aliases {
external-flash = &mx25r64;
qspi-flash = &mx25r64;
i2s-audio = &i2s0;
};
@@ -32,4 +33,17 @@
pinctrl-0 = <&i2s0_default>;
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

29
firmware/debug.conf Normal file
View File

@@ -0,0 +1,29 @@
### Logging
CONFIG_LOG=y
#CONFIG_LOG_MODE_IMMEDIATE=y
CONFIG_DEBUG=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

15
firmware/debug_rtt.conf Normal file
View File

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

10
firmware/debug_uart.conf Normal file
View File

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

3
firmware/fs/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
lfs_external_flash.hex
.tmp_lfs_build/
*.wav

86
firmware/fs/README.md Normal file
View File

@@ -0,0 +1,86 @@
# FS Tooling
This directory contains the tooling to build and flash the external LittleFS image used by the firmware.
## Python Version
Use Python 3.11.
The filesystem/audio build tool is currently tested with Python 3.11.
Python 3.13 is not supported at the moment, especially on Windows, because some dependencies in the TTS toolchain may fall back to source builds and fail to install.
In particular, the dependency chain around `kokoro`, `spacy`, `thinc`, and `blis` may fail to install on Windows when no compatible wheels are available.
## Install Dependencies
`ffmpeg` is required in addition to the Python packages.
```sh
python3.11 -m venv venv
. venv/bin/activate
pip install -r requirements.txt
```
On Windows:
```bat
py -3.11 -m venv venv
venv\Scripts\activate
pip install -r requirements.txt
```
## Build LittleFS Image
```sh
./venv/bin/python build_lfs_audio.py
```
Example with imported WAV files and a custom system prompt YAML:
```sh
./venv/bin/python build_lfs_audio.py \
--wav-dir ./my_wavs \
--sys-yaml ./sys_prompts.yaml
```
Notes:
- `--wav-dir` imports all `.wav` files from the given directory into `/lfs/a`
- `--sys-yaml` overrides the default TTS prompt list for `/lfs/sys`
- generated audio is converted to mono 16 kHz PCM before it is written into the LittleFS image
This generates:
- `lfs_external_flash.hex`
The default image layout is aligned to a 4 KiB flash page layout.
After the build, the script also prints a usage summary for `/lfs/sys`, `/lfs/a`, and the combined image.
Example:
```text
Usage summary:
sys: files=4, raw=48.0 KiB, fs=56.0 KiB (14 blocks, standalone estimate)
audio: files=12, raw=224.0 KiB, fs=264.0 KiB (66 blocks, standalone estimate)
total: raw=272.0 KiB, fs=300.0 KiB (75 blocks in combined image)
```
`raw` is the sum of file sizes in the staging tree.
`fs` is the estimated or measured LittleFS space usage with the configured block size.
## Flash Image
On macOS/Linux:
```sh
./program.sh
```
On Windows:
```bat
program.bat
```
Both scripts use paths relative to their own location, so they can be started from any working directory.

View File

@@ -0,0 +1,284 @@
#!/usr/bin/env python3
import argparse
import shutil
import subprocess
import tempfile
from pathlib import Path
import soundfile as sf
import yaml
from intelhex import IntelHex
from kokoro import KPipeline
from littlefs import LittleFS
DEFAULT_SAMPLE_RATE = 16000
DEFAULT_VOICE = "af_bella"
DEFAULT_BLOCK_SIZE = 4096
DEFAULT_BLOCK_COUNT = 2048
DEFAULT_READ_SIZE = 512
DEFAULT_LOOKAHEAD_SIZE = 256
DEFAULT_FILTERS = [
"highpass=f=120",
"lowpass=f=6000",
"acompressor=threshold=-18dB:ratio=3:attack=5:release=80",
"loudnorm=I=-16:TP=-1.0",
]
DEFAULT_SYS_PROMPTS = [
{"id": "404", "text": "No sound sample was found on the device."},
{"id": "update", "text": "Firmware updated. Awaiting confirmation."},
{"id": "confirm", "text": "State confirmed."},
{"id": "voltest", "text": "Volume test. This is a sample of the current volume level."},
]
def run_ffmpeg(cmd: list[str]) -> None:
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def export_raw_pcm(input_path: Path, output_path: Path, sample_rate: int, filters: list[str]) -> None:
output_path.parent.mkdir(parents=True, exist_ok=True)
filter_str = ",".join(filters)
run_ffmpeg([
"ffmpeg",
"-y",
"-i",
str(input_path),
"-af",
filter_str,
"-ar",
str(sample_rate),
"-ac",
"1",
"-f",
"s16le",
"-acodec",
"pcm_s16le",
str(output_path),
])
def load_sys_prompts(yaml_path: Path | None) -> list[dict]:
if yaml_path is None:
return DEFAULT_SYS_PROMPTS
with open(yaml_path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f) or []
prompts: list[dict] = []
if isinstance(data, list):
for item in data:
if isinstance(item, dict) and item.get("id") and item.get("text"):
prompts.append({"id": item["id"], "text": item["text"]})
elif isinstance(data, dict):
for item in data.get("assets", []):
if isinstance(item, dict) and item.get("id") and item.get("text"):
prompts.append({"id": item["id"], "text": item["text"]})
return prompts
def build_sys_tts(prompts: list[dict], out_sys_dir: Path, sample_rate: int) -> int:
pipeline = KPipeline(lang_code="a")
generated = 0
with tempfile.TemporaryDirectory(prefix="tts_tmp_") as tmp_dir_name:
tmp_dir = Path(tmp_dir_name)
for prompt in prompts:
asset_id = prompt["id"]
text = prompt["text"]
tmp_wav = tmp_dir / f"{asset_id}.wav"
generator = pipeline(text, voice=DEFAULT_VOICE, speed=1.0)
for _, _, audio in generator:
sf.write(tmp_wav, audio, 24000)
break
out_file = out_sys_dir / asset_id
export_raw_pcm(tmp_wav, out_file, sample_rate=sample_rate, filters=DEFAULT_FILTERS)
generated += 1
return generated
def import_wavs_to_a(wav_dir: Path | None, out_a_dir: Path, sample_rate: int) -> int:
if wav_dir is None:
return 0
if not wav_dir.exists() or not wav_dir.is_dir():
raise FileNotFoundError(f"WAV directory not found: {wav_dir}")
count = 0
for wav_file in sorted(wav_dir.glob("*.wav")):
out_file = out_a_dir / wav_file.stem
export_raw_pcm(wav_file, out_file, sample_rate=sample_rate, filters=DEFAULT_FILTERS)
count += 1
return count
def add_files_recursive(fs: LittleFS, local_path: Path, lfs_path: str = "/") -> None:
if not local_path.exists():
return
for item in sorted(local_path.iterdir()):
target_path = f"{lfs_path.rstrip('/')}/{item.name}".replace("//", "/")
if item.is_file():
with open(item, "rb") as f:
with fs.open(target_path, "wb") as lfs_file:
lfs_file.write(f.read())
elif item.is_dir():
fs.mkdir(target_path)
add_files_recursive(fs, item, target_path)
def build_littlefs_image(source_folder: Path, block_size: int, block_count: int) -> tuple[LittleFS, int]:
fs = LittleFS(
block_size=block_size,
block_count=block_count,
read_size=DEFAULT_READ_SIZE,
prog_size=256,
lookahead_size=DEFAULT_LOOKAHEAD_SIZE,
cache_size=4096,
)
add_files_recursive(fs, source_folder)
used_blocks = 0
lfs_buffer = fs.context.buffer
for idx in range(block_count):
offset = idx * block_size
block_data = lfs_buffer[offset : offset + block_size]
if any(byte != 0xFF for byte in block_data):
used_blocks += 1
return fs, used_blocks
def build_littlefs_hex(source_folder: Path, output_hex: Path, block_size: int, block_count: int, start_addr: int) -> int:
fs, used_blocks = build_littlefs_image(source_folder, block_size, block_count)
ih = IntelHex()
lfs_buffer = fs.context.buffer
for idx in range(block_count):
offset = idx * block_size
block_data = lfs_buffer[offset : offset + block_size]
if any(byte != 0xFF for byte in block_data):
ih.frombytes(block_data, offset=start_addr + offset)
ih.tofile(str(output_hex), format="hex")
return used_blocks
def summarize_directory(directory: Path) -> tuple[int, int]:
if not directory.exists():
return 0, 0
file_count = 0
raw_bytes = 0
for file_path in directory.rglob("*"):
if file_path.is_file():
file_count += 1
raw_bytes += file_path.stat().st_size
return file_count, raw_bytes
def format_bytes(size: int) -> str:
units = ["B", "KiB", "MiB", "GiB"]
value = float(size)
for unit in units:
if value < 1024.0 or unit == units[-1]:
if unit == "B":
return f"{int(value)} {unit}"
return f"{value:.1f} {unit}"
value /= 1024.0
def report_directory_usage(label: str, directory: Path, block_size: int, block_count: int) -> None:
file_count, raw_bytes = summarize_directory(directory)
_, used_blocks = build_littlefs_image(directory, block_size, block_count)
fs_bytes = used_blocks * block_size
print(
f" {label}: files={file_count}, raw={format_bytes(raw_bytes)}, "
f"fs={format_bytes(fs_bytes)} ({used_blocks} blocks, standalone estimate)"
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Generate Bella-only TTS in /lfs/sys, optionally import WAVs into /lfs/a, "
"then create LittleFS HEX."
)
)
parser.add_argument("--wav-dir", default=None, help="Optional directory with WAV files for /lfs/a")
parser.add_argument("--sys-yaml", default=None, help="Optional YAML with system prompts (id/text)")
parser.add_argument("--sample-rate", type=int, default=DEFAULT_SAMPLE_RATE)
parser.add_argument("--block-size", type=int, default=DEFAULT_BLOCK_SIZE)
parser.add_argument("--block-count", type=int, default=DEFAULT_BLOCK_COUNT)
parser.add_argument("--start-addr", type=lambda x: int(x, 0), default=0x12000000)
parser.add_argument(
"--output-hex",
default=str(Path(__file__).resolve().parent / "lfs_external_flash.hex"),
help="Output path for generated HEX",
)
parser.add_argument(
"--keep-staging",
action="store_true",
help="Keep temporary staging directory for inspection",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
fs_dir = Path(__file__).resolve().parent
output_hex = Path(args.output_hex).resolve()
wav_dir = Path(args.wav_dir).resolve() if args.wav_dir else None
sys_yaml = Path(args.sys_yaml).resolve() if args.sys_yaml else None
staging_root = fs_dir / ".tmp_lfs_build"
if staging_root.exists():
shutil.rmtree(staging_root)
# 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)
prompts = load_sys_prompts(sys_yaml)
if not prompts:
raise RuntimeError("No system prompts available. Provide --sys-yaml or use defaults.")
tts_count = build_sys_tts(prompts, out_sys, args.sample_rate)
wav_count = import_wavs_to_a(wav_dir, out_a, args.sample_rate)
total_used_blocks = build_littlefs_hex(
source_folder=staging_root,
output_hex=output_hex,
block_size=args.block_size,
block_count=args.block_count,
start_addr=args.start_addr,
)
print(f"Done. TTS assets: {tts_count}, WAV imports: {wav_count}, HEX: {output_hex}")
print("Usage summary:")
report_directory_usage("sys", out_sys, args.block_size, args.block_count)
report_directory_usage("audio", out_a, args.block_size, args.block_count)
print(
f" total: raw={format_bytes(summarize_directory(staging_root)[1])}, "
f"fs={format_bytes(total_used_blocks * args.block_size)} "
f"({total_used_blocks} blocks in combined image)"
)
if not args.keep_staging and staging_root.exists():
shutil.rmtree(staging_root)
if __name__ == "__main__":
main()

23
firmware/fs/buzzy.json Normal file
View File

@@ -0,0 +1,23 @@
{
"firmware_config": {
"peripheral": "QSPI",
"compress": true
},
"pins": {
"sck": 2,
"csn": 5,
"io0": 29,
"io1": 30,
"io2": 31,
"io3": 45
},
"flash_size": 8388608,
"sck_frequency": 8000000,
"address_mode": "MODE24BIT",
"readoc": "READ4IO",
"writeoc": "PP4IO",
"pp_size": "PPSIZE256",
"sck_delay": 0,
"rx_delay": 2,
"page_size": 4096
}

46
firmware/fs/buzzy.toml Normal file
View File

@@ -0,0 +1,46 @@
[qspi]
mem_size = 0x800000
read_mode = "READ4IO"
write_mode = "PP4IO"
address_mode = "BIT24"
frequency = "M16"
spi_mode = "MODE0"
rx_delay = 2
wip_index = 0
page_program_size = "PAGE256"
retain_ram = true
# Pin-Zuweisungen (angepasst an die Netlist)
[qspi.sck]
delay = 0x80
pin = 2
port = 0
[qspi.csn]
pin = 5
port = 0
[qspi.dio0]
pin = 29
port = 0
[qspi.dio1]
pin = 30
port = 0
[qspi.dio2]
pin = 31
port = 0
[qspi.dio3]
pin = 13
port = 1
[qspi.custom]
io2_level = "LEVEL_LOW"
io3_level = "LEVEL_HIGH"
# Aktiviert Quad-IO und High-Performance Mode für Macronix MX25R
instructions = [
{command=0x06, data=[]},
{command=0x01, data=[0x40, 0, 0x2]}
]

1
firmware/fs/program.bat Normal file
View File

@@ -0,0 +1 @@
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

4
firmware/fs/program.sh Executable file
View File

@@ -0,0 +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 chip_erase_mode=ERASE_NONE,verify=VERIFY_READ,reset=RESET_SYSTEM,ext_mem_erase_mode=ERASE_RANGES_TOUCHED_BY_FIRMWARE

View File

@@ -0,0 +1,7 @@
kokoro>=0.1.0
soundfile
numpy
torch
PyYAML
littlefs-python
intelhex

View File

@@ -1,6 +1,9 @@
add_subdirectory(settings_mgmt)
add_subdirectory(fw_mgmt)
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

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

View File

@@ -1,4 +1,4 @@
if(CONFIG_AUDIO)
if(CONFIG_BUZZ_AUDIO)
zephyr_library()
zephyr_library_sources(src/audio.c)
zephyr_include_directories(include)

View File

@@ -1,10 +1,10 @@
menuconfig AUDIO
menuconfig BUZZ_AUDIO
bool "Audio handling"
default y
default n
select I2S
select POLL
if AUDIO
if BUZZ_AUDIO
config AUDIO_NO_SAMPLES_SAMPLE
string "Audio no samples sample"
default "404"
@@ -57,4 +57,4 @@ if AUDIO
module = AUDIO
module-str = audio
source "subsys/logging/Kconfig.template.log_config"
endif # AUDIO
endif # BUZZ_AUDIO

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)",
@@ -186,7 +195,8 @@ static int audio_select_random_to_buf(char *buf, size_t buf_size)
{
if (current_index == random_index)
{
snprintf(buf, buf_size, "%s/%s", FS_AUDIO_PATH, entry.name);
snprintf(buf, buf_size, "%s/%.*s", FS_AUDIO_PATH,
(int)(buf_size - sizeof(FS_AUDIO_PATH) - 1U), entry.name);
LOG_DBG("Selected random audio file: %s", buf);
found = true;
break;

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,87 +3,38 @@ 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
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.
# 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
string "Default Bluetooth device name"
default "Edis Buzzer 2.0"
help
Device name used when ble_mgmt_init() is called with a NULL name.
# Buffers
config BT_BUF_ACL_TX_COUNT
default 15
config BT_L2CAP_TX_BUF_COUNT
default 15
config BT_CONN_TX_MAX
default 15
config BT_CTLR_SDC_TX_PACKET_COUNT
default 15
config BT_CTLR_SDC_RX_PACKET_COUNT
default 15
config BT_BUF_EVT_RX_COUNT
default 16
# Callbacks
config BT_USER_PHY_UPDATE
default y
config BT_USER_DATA_LEN_UPDATE
default y
# Automatic updates
config BT_AUTO_PHY_UPDATE
default y
config BT_AUTO_DATA_LEN_UPDATE
default y
config BT_GAP_AUTO_UPDATE_CONN_PARAMS
default y
# Preferred defaults
config BT_PERIPHERAL_PREF_MIN_INT
default 6
config BT_PERIPHERAL_PREF_MAX_INT
default 40
config BT_PERIPHERAL_PREF_LATENCY
default 0
config BT_PERIPHERAL_PREF_TIMEOUT
default 400
# Connections
config BT_MAX_CONN
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

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,217 +51,326 @@ 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,
BT_GATT_PRIMARY_SERVICE(&buzz_service_uuid),
BT_GATT_CHARACTERISTIC(&buzz_rx_uuid.uuid, BT_GATT_CHRC_WRITE_WITHOUT_RESP,
BT_GATT_PERM_WRITE, NULL, rx_cb, NULL),
BT_GATT_CHARACTERISTIC(&buzz_tx_uuid.uuid, BT_GATT_CHRC_NOTIFY,
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;
}
BT_GATT_PRIMARY_SERVICE(&buzz_service_uuid),
BT_GATT_CHARACTERISTIC(&buzz_rx_uuid.uuid, BT_GATT_CHRC_WRITE_WITHOUT_RESP,
BT_GATT_PERM_WRITE, NULL, rx_cb, NULL),
BT_GATT_CHARACTERISTIC(&buzz_tx_uuid.uuid, BT_GATT_CHRC_NOTIFY,
BT_GATT_PERM_NONE, NULL, NULL, NULL),
BT_GATT_CCC(tx_ccc_cfg_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE));
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);
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();
}
else
{
LOG_INF("Advertising successfully restarted");
}
}
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;
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);
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,9 @@ menuconfig FS_MGMT
select FILE_SYSTEM_LITTLEFS
select FILE_SYSTEM_MKFS
select FLASH_PAGE_LAYOUT
select NORDIC_QSPI_NOR if SOC_SERIES_NRF52X && (SOC_NRF52840_QIAA || SOC_NRF52833_QIAA)
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
Library for initializing and managing the file system.
@@ -56,13 +58,13 @@ if FS_MGMT
endif # SOC_SERIES_NRF52X
config FS_LITTLEFS_READ_SIZE
default 256
default 512
config FS_LITTLEFS_PROG_SIZE
default 256
config FS_LITTLEFS_CACHE_SIZE
default 4096
config FS_LITTLEFS_LOOKAHEAD_SIZE
default 512
default 256
module = FS_MGMT
module-str = fs_mgmt

View File

@@ -1,5 +1,6 @@
#include <zephyr/fs/littlefs.h>
#include <zephyr/fs/fs.h>
#include <zephyr/storage/flash_map.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/logging/log.h>
#include <zephyr/pm/device.h>
@@ -11,11 +12,21 @@
LOG_MODULE_REGISTER(fs_mgmt, CONFIG_FS_MGMT_LOG_LEVEL);
#define FS_PARTITION_ID FLASH_AREA_ID(littlefs_storage)
/*
* Under sysbuild, Partition Manager generates PM_<name>_ID symbols.
* Without PM, we fall back to the DTS node label.
*/
#if defined(PM_littlefs_storage_ID)
#define FS_PARTITION_ID PM_littlefs_storage_ID
#elif DT_NODE_EXISTS(DT_NODELABEL(ext_flash_lfs))
#define FS_PARTITION_ID FIXED_PARTITION_ID(ext_flash_lfs)
#else
#error "No compatible LittleFS partition found (expected PM littlefs_storage or DTS ext_flash_lfs)"
#endif
FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(fs_storage_data);
#define QSPI_FLASH_NODE DT_ALIAS(qspi_flash)
static const struct device *flash_dev = DEVICE_DT_GET(QSPI_FLASH_NODE);
#define EXTERNAL_FLASH_NODE DT_ALIAS(external_flash)
static const struct device *flash_dev = DEVICE_DT_GET(EXTERNAL_FLASH_NODE);
#define TAG_MAGIC "TAG!"
#define TAG_FORMAT_VERSION 1U
@@ -27,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
@@ -68,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;
@@ -94,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;
}
@@ -105,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;
@@ -123,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;
}
@@ -171,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;
@@ -389,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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2
firmware/output.txt Normal file
View File

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

40
firmware/pm_static.yml Normal file
View File

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

View File

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

View File

@@ -1,10 +1,3 @@
### Logging
CONFIG_LOG=y
CONFIG_AUDIO_LOG_LEVEL_DBG=y
### Bluetooth
CONFIG_BLE_MGMT=y
### Error handling
CONFIG_HW_STACK_PROTECTION=y
CONFIG_RESET_ON_FATAL_ERROR=y
@@ -12,8 +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_INIT_STACKS=y
CONFIG_THREAD_STACK_INFO=y
CONFIG_STACK_SENTINEL=y
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 "audio.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)
@@ -44,24 +61,43 @@ void ble_rx_cb(const uint8_t *data, uint16_t len)
buzz_proto_buf_free(&buf); /* Speicher bei Fehler sofort wieder freigeben */
}
}
#endif
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;
}
#else
LOG_WRN("BLE not enabled");
#endif
LOG_INF("Init complete.");
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);
k_sleep(K_FOREVER);
#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

@@ -1,6 +1,5 @@
CONFIG_LOG=y
# CONFIG_MCUBOOT_SERIAL=y
CONFIG_UART_CONSOLE=y
# CONFIG_SINGLE_APPLICATION_SLOT=n
# CONFIG_MCUBOOT_INDICATION_LED=y
# CONFIG_BOOT_SERIAL_CDC_ACM=y

View File

@@ -1,13 +0,0 @@
/ {
aliases {
mcuboot-button0 = &button0;
mcuboot-led0 = &led0;
};
};
/* Step 2.1 - Configure CDC ACM */
&zephyr_udc0 {
cdc_acm_uart0: cdc_acm_uart0 {
compatible = "zephyr,cdc-acm-uart";
};
};

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;
@@ -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) => {