diff --git a/firmware/debug.conf b/firmware/debug.conf index 27b5647..9f2bc4b 100644 --- a/firmware/debug.conf +++ b/firmware/debug.conf @@ -1,6 +1,6 @@ ### Logging CONFIG_LOG=y -CONFIG_LOG_MODE_IMMEDIATE=y +#CONFIG_LOG_MODE_IMMEDIATE=y CONFIG_DEBUG=y CONFIG_DEBUG_OPTIMIZATIONS=y @@ -14,15 +14,15 @@ CONFIG_BATT_MGMT_LOG_LEVEL_DBG=y CONFIG_USB_MGMT_LOG_LEVEL_DBG=y ### Bluetooth -CONFIG_BLE_MGMT=n -CONFIG_BT_LOG_LEVEL_WRN=n +CONFIG_BLE_MGMT=y +CONFIG_BT_LOG_LEVEL_WRN=y ### Audio CONFIG_BUZZ_AUDIO=n ### Shell features shared by all debug variants CONFIG_SHELL=y -CONFIG_SHELL_LOG_BACKEND=n +CONFIG_SHELL_LOG_BACKEND=y CONFIG_FILE_SYSTEM_SHELL=y CONFIG_SHELL_STACK_SIZE=2048 CONFIG_FILE_SYSTEM_SHELL_LS_SIZE=y diff --git a/firmware/libs/batt_mgmt/include/batt_mgmt.h b/firmware/libs/batt_mgmt/include/batt_mgmt.h index 6f70c1b..c5f8397 100644 --- a/firmware/libs/batt_mgmt/include/batt_mgmt.h +++ b/firmware/libs/batt_mgmt/include/batt_mgmt.h @@ -8,11 +8,11 @@ #define BATT_MGMT_OVERSAMPLING_16X 16U typedef enum { - BATT_STATE_DISCHARGING = 0, - BATT_STATE_FULL, - BATT_STATE_CHARGING, - BATT_STATE_ERROR, - BATT_STATE_UNKNOWN, + 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 { diff --git a/firmware/libs/buzz_proto/include/buzz_proto.h b/firmware/libs/buzz_proto/include/buzz_proto.h index fc1ae35..d31c289 100644 --- a/firmware/libs/buzz_proto/include/buzz_proto.h +++ b/firmware/libs/buzz_proto/include/buzz_proto.h @@ -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 { diff --git a/firmware/libs/buzz_proto/src/buzz_proto.c b/firmware/libs/buzz_proto/src/buzz_proto.c index 0ef801c..0c00219 100644 --- a/firmware/libs/buzz_proto/src/buzz_proto.c +++ b/firmware/libs/buzz_proto/src/buzz_proto.c @@ -7,6 +7,7 @@ #include #include "buzz_proto.h" +#include "batt_mgmt.h" #include "fs_mgmt.h" #include "fw_mgmt.h" @@ -293,6 +294,41 @@ static void handle_fw_info_request(struct buzz_frame_msg *msg) } } +static void handle_batt_info_request(struct buzz_frame_msg *msg) +{ + struct buzz_proto_header *hdr = (struct buzz_proto_header *)msg->data_ptr; + struct buzz_resp_batt_info *resp_data = (struct buzz_resp_batt_info *)(msg->data_ptr + sizeof(*hdr)); + batt_mgmt_info_t batt_info; + uint16_t voltage_mv = 0; + int rc = batt_mgmt_get_info(&batt_info); + + if (rc < 0) + { + LOG_WRN("Failed to get battery info: %d", rc); + send_error_frame(msg, abs(rc)); + return; + } + + if (batt_info.voltage_mv > 0) + { + voltage_mv = (batt_info.voltage_mv > UINT16_MAX) ? UINT16_MAX : (uint16_t)batt_info.voltage_mv; + } + + hdr->frame_type = BUZZ_FRAME_RESPONSE; + hdr->payload_length = sys_cpu_to_le16(sizeof(struct buzz_resp_batt_info)); + + resp_data->data_type = BUZZ_DATA_BATT_INFO; + resp_data->batt_status = (uint8_t)batt_info.state; + resp_data->batt_level = batt_info.level; + resp_data->batt_percent = batt_info.percent; + resp_data->batt_voltage_mv = sys_cpu_to_le16(voltage_mv); + + if (msg->reply_cb) + { + msg->reply_cb(msg->data_ptr, sizeof(struct buzz_proto_header) + sizeof(struct buzz_resp_batt_info)); + } +} + static void handle_ls_request(struct buzz_frame_msg *msg) { struct buzz_proto_header *hdr = (struct buzz_proto_header *)msg->data_ptr; @@ -698,6 +734,11 @@ static void handle_request(struct buzz_frame_msg *msg) handle_fw_info_request(msg); 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); diff --git a/firmware/src/main.c b/firmware/src/main.c index 3453b30..d564e01 100644 --- a/firmware/src/main.c +++ b/firmware/src/main.c @@ -93,9 +93,23 @@ int main(void) } else { LOG_WRN("Battery info read failed: %d", batt_rc); } + for (;;) { + k_sleep(K_SECONDS(5)); + batt_rc = batt_mgmt_get_info(&batt_info); + if (batt_rc == 0) { + LOG_INF("Battery: %d mV, %u%%, level=%u, state=%s (%d)", + batt_info.voltage_mv, + batt_info.percent, + batt_info.level, + battery_state_to_str(batt_info.state), + batt_info.state); + } else { + LOG_WRN("Battery info read failed: %d", batt_rc); + } + } #endif // CONFIG_BATT_MGMT #endif // CONFIG_LOG - + for (;;) { int32_t rem_ms = k_sleep(K_FOREVER); LOG_WRN("main woke unexpectedly (remaining=%d ms)", rem_ms); diff --git a/protocol.md b/protocol.md index 51a8373..b4aeae8 100644 --- a/protocol.md +++ b/protocol.md @@ -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: diff --git a/webpage/src/components/DeviceInfo.svelte b/webpage/src/components/DeviceInfo.svelte index e6db300..04a4105 100644 --- a/webpage/src/components/DeviceInfo.svelte +++ b/webpage/src/components/DeviceInfo.svelte @@ -1,7 +1,7 @@
@@ -127,7 +173,14 @@ Batterie - 85% 1200mAh + {#if $battInfo} + {$battInfo.battPercent}% + + {$battInfo.battVoltageMv} mV + ({battStatusText}) + {:else} + unbekannt + {/if} diff --git a/webpage/src/lib/protocol/constants.ts b/webpage/src/lib/protocol/constants.ts index 5d2d749..6782062 100644 --- a/webpage/src/lib/protocol/constants.ts +++ b/webpage/src/lib/protocol/constants.ts @@ -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, -} \ No newline at end of file +} + +export const BATT_STATUS = { + DISCHARGING: 0x00, + FULL: 0x01, + CHARGING: 0x02, + ERROR: 0x03, + UNKNOWN: 0x04, +}; \ No newline at end of file diff --git a/webpage/src/lib/protocol/parser.ts b/webpage/src/lib/protocol/parser.ts index 337f9fb..d7e79b1 100644 --- a/webpage/src/lib/protocol/parser.ts +++ b/webpage/src/lib/protocol/parser.ts @@ -1,5 +1,5 @@ import { FRAME, DATA, ZEPHYR_ERRORS } from './constants'; -import { protocolInfo, deviceInfo, fsInfo, transferStats, fwInfo, resetTransferStats, transferDetails } from '../store'; +import { protocolInfo, deviceInfo, fsInfo, transferStats, fwInfo, battInfo, resetTransferStats, transferDetails } from '../store'; import { addToast } from '../toast'; import { SETTINGS } from '../settings'; import { crc32 } from './crc32'; @@ -84,6 +84,18 @@ export function parseIncomingFrame(view: DataView, sender: FrameSender) { const fwVersion = new TextDecoder().decode(new Uint8Array(view.buffer, 11, fw_version_length)); const kernelVersion = new TextDecoder().decode(new Uint8Array(view.buffer, 11 + fw_version_length, kernel_version_length)); fwInfo.set({ fwStatus, slot1Size, fwVersion, kernelVersion }); + break; + case DATA.BATT_INFO: + if (payloadLength < 6) { + console.warn(`Invalid BATT_INFO payload length: ${payloadLength}`); + break; + } + const battStatus = view.getUint8(4); + const battLevel = view.getUint8(5); + const battPercent = view.getUint8(6); + const battVoltageMv = view.getUint16(7, true); + battInfo.set({ battStatus, battLevel, battPercent, battVoltageMv }); + break; } break; @@ -365,6 +377,17 @@ export function buildFWInfoRequest(): ArrayBuffer { return buffer; } +export function buildBattInfoRequest(): ArrayBuffer { + const buffer = new ArrayBuffer(4); + const view = new DataView(buffer); + + view.setUint8(0, FRAME.REQUEST); + view.setUint16(1, 1, true); + view.setUint8(3, DATA.BATT_INFO); + + return buffer; +} + export function buildLSRequest(path: string): ArrayBuffer { const encoder = new TextEncoder(); const pathBytes = encoder.encode(path); diff --git a/webpage/src/lib/store.ts b/webpage/src/lib/store.ts index ddb3790..b3d966d 100644 --- a/webpage/src/lib/store.ts +++ b/webpage/src/lib/store.ts @@ -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(null); export const deviceInfo = writable(null); export const fsInfo = writable(null); export const fwInfo = writable(null); +export const battInfo = writable(null); // Dateilisten export const buzzerAudioFiles = writable([]); @@ -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([]); diff --git a/webpage/src/lib/sync.ts b/webpage/src/lib/sync.ts index d1ff1e9..f3c04f5 100644 --- a/webpage/src/lib/sync.ts +++ b/webpage/src/lib/sync.ts @@ -1,6 +1,6 @@ import { get } from 'svelte/store'; import { isConnected, deviceInfo, fsInfo, fwInfo, buzzerAudioFiles, buzzerSysFiles, isTransferingRemote, isFetchingLocal, storageUsage, localAudioFiles, transferStats } from './store'; -import { requestProtocolInfo, requestFSInfo, fetchDirectory, getFile, putFile, deleteRemoteFile, requestDeviceInfo, requestFWInfo } from './transport'; +import { requestProtocolInfo, requestFSInfo, fetchDirectory, getFile, putFile, deleteRemoteFile, requestDeviceInfo, requestFWInfo, requestBattInfo } from './transport'; import type { BuzzerFile } from './types'; import { addToast } from './toast'; import { getLocalFiles, deleteLocalFile, getLocalFile } from './db'; @@ -28,6 +28,7 @@ export async function refreshRemote() { await requestProtocolInfo(); await requestFSInfo(); await requestFWInfo(); + await requestBattInfo(); await requestDeviceInfo(); // Kurze Verzögerung für Store-Propagation diff --git a/webpage/src/lib/transport.ts b/webpage/src/lib/transport.ts index 1003419..4b71d5f 100644 --- a/webpage/src/lib/transport.ts +++ b/webpage/src/lib/transport.ts @@ -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 = 60_000; export type FrameSender = (buffer: ArrayBuffer) => Promise; let currentSender: FrameSender | null = null; +let battPollTimer: ReturnType | 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 { @@ -85,6 +128,7 @@ export async function fetchDirectory(path: string): Promise { } export function handleTransportDisconnect() { + stopBattPolling(); registerTransport(null); resetRemote(); }