This commit is contained in:
2026-05-26 17:19:45 +02:00
parent 87cba0b419
commit 2d3ea34603
12 changed files with 239 additions and 17 deletions

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { FRAME, DATA, ZEPHYR_ERRORS } from './constants';
import { protocolInfo, deviceInfo, fsInfo, transferStats, fwInfo, resetTransferStats, transferDetails } from '../store';
import { protocolInfo, deviceInfo, fsInfo, transferStats, fwInfo, battInfo, resetTransferStats, transferDetails } from '../store';
import { addToast } from '../toast';
import { SETTINGS } from '../settings';
import { crc32 } from './crc32';
@@ -84,6 +84,18 @@ export function parseIncomingFrame(view: DataView, sender: FrameSender) {
const fwVersion = new TextDecoder().decode(new Uint8Array(view.buffer, 11, fw_version_length));
const kernelVersion = new TextDecoder().decode(new Uint8Array(view.buffer, 11 + fw_version_length, kernel_version_length));
fwInfo.set({ fwStatus, slot1Size, fwVersion, kernelVersion });
break;
case DATA.BATT_INFO:
if (payloadLength < 6) {
console.warn(`Invalid BATT_INFO payload length: ${payloadLength}`);
break;
}
const battStatus = view.getUint8(4);
const battLevel = view.getUint8(5);
const battPercent = view.getUint8(6);
const battVoltageMv = view.getUint16(7, true);
battInfo.set({ battStatus, battLevel, battPercent, battVoltageMv });
break;
}
break;
@@ -365,6 +377,17 @@ export function buildFWInfoRequest(): ArrayBuffer {
return buffer;
}
export function buildBattInfoRequest(): ArrayBuffer {
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint8(0, FRAME.REQUEST);
view.setUint16(1, 1, true);
view.setUint8(3, DATA.BATT_INFO);
return buffer;
}
export function buildLSRequest(path: string): ArrayBuffer {
const encoder = new TextEncoder();
const pathBytes = encoder.encode(path);

View File

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

View File

@@ -1,6 +1,6 @@
import { get } from 'svelte/store';
import { isConnected, deviceInfo, fsInfo, fwInfo, buzzerAudioFiles, buzzerSysFiles, isTransferingRemote, isFetchingLocal, storageUsage, localAudioFiles, transferStats } from './store';
import { requestProtocolInfo, requestFSInfo, fetchDirectory, getFile, putFile, deleteRemoteFile, requestDeviceInfo, requestFWInfo } from './transport';
import { requestProtocolInfo, requestFSInfo, fetchDirectory, getFile, putFile, deleteRemoteFile, requestDeviceInfo, requestFWInfo, requestBattInfo } from './transport';
import type { BuzzerFile } from './types';
import { addToast } from './toast';
import { getLocalFiles, deleteLocalFile, getLocalFile } from './db';
@@ -28,6 +28,7 @@ export async function refreshRemote() {
await requestProtocolInfo();
await requestFSInfo();
await requestFWInfo();
await requestBattInfo();
await requestDeviceInfo();
// Kurze Verzögerung für Store-Propagation

View File

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