mirror of
https://github.com/espressif/esp-idf.git
synced 2026-10-02 11:10:54 +03:00
fix(ble_log_console): resolve pre-commit flake8, mypy, and import ordering failures
Replace Python 3.10+ X | Y union syntax with Optional/Union from typing, remove unsupported @dataclass(slots=True), fix flake8 E203 slice whitespace, and apply auto-fixed import ordering and quote style.
This commit is contained in:
@@ -1,6 +1,5 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
if sys.version_info < (3, 10): # noqa: UP036 — runtime guard for users on old Python
|
if sys.version_info < (3, 10): # noqa: UP036 — runtime guard for users on old Python
|
||||||
|
|||||||
@@ -1,59 +1,57 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
"""Textual App wiring backend Worker to frontend widgets.
|
"""Textual App wiring backend Worker to frontend widgets.
|
||||||
|
|
||||||
See Spec Section 6.
|
See Spec Section 6.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import struct
|
import struct
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import serial
|
import serial
|
||||||
from textual.app import App
|
|
||||||
from textual.app import ComposeResult
|
|
||||||
from textual.binding import Binding
|
|
||||||
from textual.message import Message
|
|
||||||
|
|
||||||
from src.backend.frame_parser import FrameParser
|
from src.backend.frame_parser import FrameParser
|
||||||
from src.backend.internal_decoder import decode_internal_frame
|
from src.backend.internal_decoder import decode_internal_frame
|
||||||
from src.backend.models import FRAME_OVERHEAD
|
|
||||||
from src.backend.models import LL_TS_OFFSET
|
|
||||||
from src.backend.models import BackendStopped
|
from src.backend.models import BackendStopped
|
||||||
from src.backend.models import BleLogSource
|
from src.backend.models import BleLogSource
|
||||||
from src.backend.models import BufUtilEntry
|
from src.backend.models import BufUtilEntry
|
||||||
from src.backend.models import BufUtilResult
|
from src.backend.models import BufUtilResult
|
||||||
from src.backend.models import EnhStatResult
|
from src.backend.models import EnhStatResult
|
||||||
|
from src.backend.models import FRAME_OVERHEAD
|
||||||
from src.backend.models import FrameLossDetected
|
from src.backend.models import FrameLossDetected
|
||||||
from src.backend.models import FunnelSnapshot
|
from src.backend.models import FunnelSnapshot
|
||||||
|
from src.backend.models import has_os_ts
|
||||||
from src.backend.models import InfoResult
|
from src.backend.models import InfoResult
|
||||||
from src.backend.models import InternalFrameDecoded
|
from src.backend.models import InternalFrameDecoded
|
||||||
from src.backend.models import InternalSource
|
from src.backend.models import InternalSource
|
||||||
|
from src.backend.models import is_ll_source
|
||||||
from src.backend.models import LaunchConfig
|
from src.backend.models import LaunchConfig
|
||||||
|
from src.backend.models import LL_TS_OFFSET
|
||||||
from src.backend.models import LogLine
|
from src.backend.models import LogLine
|
||||||
from src.backend.models import LossType
|
from src.backend.models import LossType
|
||||||
from src.backend.models import ParsedFrame
|
from src.backend.models import ParsedFrame
|
||||||
|
from src.backend.models import resolve_source_name
|
||||||
from src.backend.models import SourcePeakWrite
|
from src.backend.models import SourcePeakWrite
|
||||||
from src.backend.models import StatsUpdated
|
from src.backend.models import StatsUpdated
|
||||||
from src.backend.models import SyncState
|
from src.backend.models import SyncState
|
||||||
from src.backend.models import SyncStateChanged
|
from src.backend.models import SyncStateChanged
|
||||||
from src.backend.models import TrafficSpikeDetected
|
from src.backend.models import TrafficSpikeDetected
|
||||||
from src.backend.models import has_os_ts
|
|
||||||
from src.backend.models import is_ll_source
|
|
||||||
from src.backend.models import resolve_source_name
|
|
||||||
from src.backend.stats import StatsAccumulator
|
from src.backend.stats import StatsAccumulator
|
||||||
from src.backend.uart_transport import UART_BLOCK_SIZE
|
|
||||||
from src.backend.uart_transport import open_serial
|
from src.backend.uart_transport import open_serial
|
||||||
|
from src.backend.uart_transport import UART_BLOCK_SIZE
|
||||||
from src.frontend.launch_screen import LaunchScreen
|
from src.frontend.launch_screen import LaunchScreen
|
||||||
from src.frontend.log_view import LogView
|
from src.frontend.log_view import LogView
|
||||||
from src.frontend.shortcut_screen import ShortcutScreen
|
from src.frontend.shortcut_screen import ShortcutScreen
|
||||||
from src.frontend.stats_screen import BufUtilScreen
|
from src.frontend.stats_screen import BufUtilScreen
|
||||||
from src.frontend.stats_screen import StatsScreen
|
from src.frontend.stats_screen import StatsScreen
|
||||||
from src.frontend.status_panel import StatusPanel
|
from src.frontend.status_panel import StatusPanel
|
||||||
|
from textual.app import App
|
||||||
|
from textual.app import ComposeResult
|
||||||
|
from textual.binding import Binding
|
||||||
|
from textual.message import Message
|
||||||
|
|
||||||
STATS_INTERVAL = 0.25 # seconds
|
STATS_INTERVAL = 0.25 # seconds
|
||||||
|
|
||||||
@@ -85,21 +83,21 @@ class BLELogApp(App):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
port: str | None = None,
|
port: Optional[str] = None,
|
||||||
baudrate: int = 3_000_000,
|
baudrate: int = 3_000_000,
|
||||||
log_dir: Path | None = None,
|
log_dir: Optional[Path] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._port = port
|
self._port = port
|
||||||
self._baudrate = baudrate
|
self._baudrate = baudrate
|
||||||
self._log_dir = log_dir or Path.cwd()
|
self._log_dir = log_dir or Path.cwd()
|
||||||
self._output_path: Path | None = None
|
self._output_path: Optional[Path] = None
|
||||||
self._serial: serial.Serial | None = None
|
self._serial: Optional[serial.Serial] = None
|
||||||
# All-time per-source chip write peak (updated from StatsUpdated messages)
|
# All-time per-source chip write peak (updated from StatsUpdated messages)
|
||||||
self._max_per_source_peak: dict[int, SourcePeakWrite] | None = None
|
self._max_per_source_peak: Optional[dict[int, SourcePeakWrite]] = None
|
||||||
self._ll_max_per_source_peak: dict[int, SourcePeakWrite] | None = None
|
self._ll_max_per_source_peak: Optional[dict[int, SourcePeakWrite]] = None
|
||||||
# Console-side per-source received bytes (from StatsUpdated snapshots)
|
# Console-side per-source received bytes (from StatsUpdated snapshots)
|
||||||
self._per_source_rx_bytes: dict[int, int] | None = None
|
self._per_source_rx_bytes: Optional[dict[int, int]] = None
|
||||||
self._funnel_snapshots: list[FunnelSnapshot] = []
|
self._funnel_snapshots: list[FunnelSnapshot] = []
|
||||||
self._buf_util_snapshots: list[BufUtilEntry] = []
|
self._buf_util_snapshots: list[BufUtilEntry] = []
|
||||||
# Wall-clock capture start (set when backend loop begins)
|
# Wall-clock capture start (set when backend loop begins)
|
||||||
@@ -115,7 +113,10 @@ class BLELogApp(App):
|
|||||||
self._resolve_output_path()
|
self._resolve_output_path()
|
||||||
self.run_worker(self._backend_loop, thread=True, exclusive=True)
|
self.run_worker(self._backend_loop, thread=True, exclusive=True)
|
||||||
else:
|
else:
|
||||||
self.push_screen(LaunchScreen(default_log_dir=self._log_dir), callback=self._on_launch_result)
|
self.push_screen(
|
||||||
|
LaunchScreen(default_log_dir=self._log_dir),
|
||||||
|
callback=self._on_launch_result,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def funnel_snapshots(self) -> list[FunnelSnapshot]:
|
def funnel_snapshots(self) -> list[FunnelSnapshot]:
|
||||||
@@ -125,7 +126,7 @@ class BLELogApp(App):
|
|||||||
def buf_util_snapshots(self) -> list[BufUtilEntry]:
|
def buf_util_snapshots(self) -> list[BufUtilEntry]:
|
||||||
return self._buf_util_snapshots
|
return self._buf_util_snapshots
|
||||||
|
|
||||||
def _on_launch_result(self, config: LaunchConfig | None) -> None:
|
def _on_launch_result(self, config: Optional[LaunchConfig]) -> None:
|
||||||
"""Handle Launch Screen dismissal."""
|
"""Handle Launch Screen dismissal."""
|
||||||
if config is None:
|
if config is None:
|
||||||
self.exit()
|
self.exit()
|
||||||
@@ -146,7 +147,9 @@ class BLELogApp(App):
|
|||||||
"""Thread-safe message posting from backend worker."""
|
"""Thread-safe message posting from backend worker."""
|
||||||
self.call_from_thread(self.post_message, msg)
|
self.call_from_thread(self.post_message, msg)
|
||||||
|
|
||||||
def _emit_stats(self, stats: StatsAccumulator, parser: FrameParser, last_time: float) -> float:
|
def _emit_stats(
|
||||||
|
self, stats: StatsAccumulator, parser: FrameParser, last_time: float
|
||||||
|
) -> float:
|
||||||
"""Emit a stats snapshot if the interval has elapsed. Returns updated timestamp."""
|
"""Emit a stats snapshot if the interval has elapsed. Returns updated timestamp."""
|
||||||
now = time.perf_counter()
|
now = time.perf_counter()
|
||||||
if now - last_time < STATS_INTERVAL:
|
if now - last_time < STATS_INTERVAL:
|
||||||
@@ -190,14 +193,18 @@ class BLELogApp(App):
|
|||||||
# Lazy file handles — created on first data arrival
|
# Lazy file handles — created on first data arrival
|
||||||
output_file = None
|
output_file = None
|
||||||
console_log_file = None
|
console_log_file = None
|
||||||
console_log_path = self._output_path.with_name(self._output_path.stem + '_console.log')
|
console_log_path = self._output_path.with_name(
|
||||||
|
self._output_path.stem + '_console.log'
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
with self._serial_lock:
|
with self._serial_lock:
|
||||||
block = ser.read(UART_BLOCK_SIZE)
|
block = ser.read(UART_BLOCK_SIZE)
|
||||||
if not block:
|
if not block:
|
||||||
last_snapshot_time = self._emit_stats(stats, parser, last_snapshot_time)
|
last_snapshot_time = self._emit_stats(
|
||||||
|
stats, parser, last_snapshot_time
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 1. Save raw binary (lazy-open on first block)
|
# 1. Save raw binary (lazy-open on first block)
|
||||||
@@ -223,18 +230,31 @@ class BLELogApp(App):
|
|||||||
if isinstance(item, ParsedFrame):
|
if isinstance(item, ParsedFrame):
|
||||||
frame_size = len(item.payload) + FRAME_OVERHEAD
|
frame_size = len(item.payload) + FRAME_OVERHEAD
|
||||||
if item.source_code != BleLogSource.INTERNAL:
|
if item.source_code != BleLogSource.INTERNAL:
|
||||||
stats.record_frame(frame_size, item.source_code, item.frame_sn)
|
stats.record_frame(
|
||||||
|
frame_size, item.source_code, item.frame_sn
|
||||||
|
)
|
||||||
stats.record_frame_traffic(frame_size, item.source_code)
|
stats.record_frame_traffic(frame_size, item.source_code)
|
||||||
else:
|
else:
|
||||||
stats.record_frame() # count frame for transport metrics, no SN tracking
|
stats.record_frame() # count frame for transport metrics, no SN tracking
|
||||||
if has_os_ts(item.source_code) and item.source_code != BleLogSource.INTERNAL:
|
if (
|
||||||
stats.record_frame_ts(item.os_ts_ms, frame_size, item.source_code)
|
has_os_ts(item.source_code)
|
||||||
|
and item.source_code != BleLogSource.INTERNAL
|
||||||
|
):
|
||||||
|
stats.record_frame_ts(
|
||||||
|
item.os_ts_ms, frame_size, item.source_code
|
||||||
|
)
|
||||||
elif is_ll_source(item.source_code) and len(item.payload) >= 6:
|
elif is_ll_source(item.source_code) and len(item.payload) >= 6:
|
||||||
(lc_ts_us,) = struct.unpack_from('<I', item.payload, LL_TS_OFFSET)
|
(lc_ts_us,) = struct.unpack_from(
|
||||||
stats.record_ll_frame_ts(lc_ts_us, frame_size, item.source_code)
|
'<I', item.payload, LL_TS_OFFSET
|
||||||
|
)
|
||||||
|
stats.record_ll_frame_ts(
|
||||||
|
lc_ts_us, frame_size, item.source_code
|
||||||
|
)
|
||||||
elif item.source_code == BleLogSource.REDIR:
|
elif item.source_code == BleLogSource.REDIR:
|
||||||
wall_ms = int(time.perf_counter() * 1000) & 0xFFFFFFFF
|
wall_ms = int(time.perf_counter() * 1000) & 0xFFFFFFFF
|
||||||
stats.record_frame_wall_ts(wall_ms, frame_size, item.source_code)
|
stats.record_frame_wall_ts(
|
||||||
|
wall_ms, frame_size, item.source_code
|
||||||
|
)
|
||||||
|
|
||||||
# Decode internal frames
|
# Decode internal frames
|
||||||
if item.source_code == BleLogSource.INTERNAL:
|
if item.source_code == BleLogSource.INTERNAL:
|
||||||
@@ -251,7 +271,10 @@ class BLELogApp(App):
|
|||||||
|
|
||||||
self._post(InternalFrameDecoded(int_src, decoded))
|
self._post(InternalFrameDecoded(int_src, decoded))
|
||||||
|
|
||||||
if int_src in (InternalSource.INIT_DONE, InternalSource.INFO):
|
if int_src in (
|
||||||
|
InternalSource.INIT_DONE,
|
||||||
|
InternalSource.INFO,
|
||||||
|
):
|
||||||
info = cast(InfoResult, decoded)
|
info = cast(InfoResult, decoded)
|
||||||
stats.set_firmware_version(info['version'])
|
stats.set_firmware_version(info['version'])
|
||||||
if int_src == InternalSource.INIT_DONE:
|
if int_src == InternalSource.INIT_DONE:
|
||||||
@@ -269,7 +292,9 @@ class BLELogApp(App):
|
|||||||
baudrate=self._baudrate,
|
baudrate=self._baudrate,
|
||||||
)
|
)
|
||||||
if new_frames > 0:
|
if new_frames > 0:
|
||||||
source_name = resolve_source_name(enh['src_code'])
|
source_name = resolve_source_name(
|
||||||
|
enh['src_code']
|
||||||
|
)
|
||||||
self._post(
|
self._post(
|
||||||
FrameLossDetected(
|
FrameLossDetected(
|
||||||
source_name,
|
source_name,
|
||||||
@@ -290,7 +315,9 @@ class BLELogApp(App):
|
|||||||
# A single log line may span multiple frames due to
|
# A single log line may span multiple frames due to
|
||||||
# batch sealing, so buffer partial lines until '\n'.
|
# batch sealing, so buffer partial lines until '\n'.
|
||||||
elif item.source_code == BleLogSource.REDIR:
|
elif item.source_code == BleLogSource.REDIR:
|
||||||
payload_text = item.payload.decode('ascii', errors='replace')
|
payload_text = item.payload.decode(
|
||||||
|
'ascii', errors='replace'
|
||||||
|
)
|
||||||
|
|
||||||
# Write raw payload to console log (independent of line buffering)
|
# Write raw payload to console log (independent of line buffering)
|
||||||
if console_log_file is None:
|
if console_log_file is None:
|
||||||
@@ -356,7 +383,9 @@ class BLELogApp(App):
|
|||||||
if msg.int_src == InternalSource.INIT_DONE:
|
if msg.int_src == InternalSource.INIT_DONE:
|
||||||
info = cast(InfoResult, msg.payload)
|
info = cast(InfoResult, msg.payload)
|
||||||
log_view = self.query_one(LogView)
|
log_view = self.query_one(LogView)
|
||||||
log_view.write_info(f'BLE Log v{info["version"]} initialized - statistics reset')
|
log_view.write_info(
|
||||||
|
f"BLE Log v{info['version']} initialized - statistics reset"
|
||||||
|
)
|
||||||
elif msg.int_src == InternalSource.FLUSH:
|
elif msg.int_src == InternalSource.FLUSH:
|
||||||
log_view = self.query_one(LogView)
|
log_view = self.query_one(LogView)
|
||||||
log_view.write_info('Firmware flush - SN counters reset')
|
log_view.write_info('Firmware flush - SN counters reset')
|
||||||
@@ -379,13 +408,17 @@ class BLELogApp(App):
|
|||||||
|
|
||||||
def on_traffic_spike_detected(self, msg: TrafficSpikeDetected) -> None:
|
def on_traffic_spike_detected(self, msg: TrafficSpikeDetected) -> None:
|
||||||
top_sources = sorted(msg.per_source.items(), key=lambda x: x[1], reverse=True)
|
top_sources = sorted(msg.per_source.items(), key=lambda x: x[1], reverse=True)
|
||||||
src_parts = ', '.join(f'{resolve_source_name(s)} {p:.0f}%' for s, p in top_sources if p >= 1.0)
|
src_parts = ', '.join(
|
||||||
|
f'{resolve_source_name(s)} {p:.0f}%' for s, p in top_sources if p >= 1.0
|
||||||
|
)
|
||||||
if msg.utilization_pct >= 100.0:
|
if msg.utilization_pct >= 100.0:
|
||||||
util_str = 'saturated'
|
util_str = 'saturated'
|
||||||
else:
|
else:
|
||||||
util_str = f'{msg.utilization_pct:.0f}% wire'
|
util_str = f'{msg.utilization_pct:.0f}% wire'
|
||||||
log_view = self.query_one(LogView)
|
log_view = self.query_one(LogView)
|
||||||
log_view.write_traffic(f'{msg.throughput_kbs:.0f} KB/s ({util_str}) over {msg.duration_ms:.0f}ms | {src_parts}')
|
log_view.write_traffic(
|
||||||
|
f'{msg.throughput_kbs:.0f} KB/s ({util_str}) over {msg.duration_ms:.0f}ms | {src_parts}'
|
||||||
|
)
|
||||||
|
|
||||||
# --- Actions ---
|
# --- Actions ---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
"""Checksum implementations matching BLE Log firmware (ble_log_util.c).
|
"""Checksum implementations matching BLE Log firmware (ble_log_util.c).
|
||||||
|
|
||||||
Two algorithms:
|
Two algorithms:
|
||||||
@@ -11,7 +10,6 @@ The firmware's ror32 alignment compensation makes the XOR checksum
|
|||||||
alignment-independent — simple word-by-word XOR produces the same result
|
alignment-independent — simple word-by-word XOR produces the same result
|
||||||
regardless of the original buffer alignment.
|
regardless of the original buffer alignment.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
"""Frame sync state machine with checksum auto-detection.
|
"""Frame sync state machine with checksum auto-detection.
|
||||||
|
|
||||||
Parses BLE Log module frames from a raw byte stream.
|
Parses BLE Log module frames from a raw byte stream.
|
||||||
See Spec Sections 7, 8.
|
See Spec Sections 7, 8.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from src.backend.checksum import sum_checksum
|
from src.backend.checksum import sum_checksum
|
||||||
from src.backend.checksum import xor_checksum
|
from src.backend.checksum import xor_checksum
|
||||||
from src.backend.models import CHECKSUM_STRUCT
|
from src.backend.models import CHECKSUM_STRUCT
|
||||||
|
from src.backend.models import ChecksumAlgorithm
|
||||||
|
from src.backend.models import ChecksumMode
|
||||||
|
from src.backend.models import ChecksumScope
|
||||||
from src.backend.models import FRAME_HEADER_SIZE
|
from src.backend.models import FRAME_HEADER_SIZE
|
||||||
from src.backend.models import FRAME_OVERHEAD
|
from src.backend.models import FRAME_OVERHEAD
|
||||||
from src.backend.models import HEADER_STRUCT
|
from src.backend.models import HEADER_STRUCT
|
||||||
from src.backend.models import MAX_FRAME_SIZE
|
from src.backend.models import MAX_FRAME_SIZE
|
||||||
from src.backend.models import MAX_REMAINDER_SIZE
|
from src.backend.models import MAX_REMAINDER_SIZE
|
||||||
from src.backend.models import ChecksumAlgorithm
|
|
||||||
from src.backend.models import ChecksumMode
|
|
||||||
from src.backend.models import ChecksumScope
|
|
||||||
from src.backend.models import ParsedFrame
|
from src.backend.models import ParsedFrame
|
||||||
from src.backend.models import SyncState
|
from src.backend.models import SyncState
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ class FrameParser:
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._remained = b''
|
self._remained = b''
|
||||||
self._sync_state = SyncState.SEARCHING
|
self._sync_state = SyncState.SEARCHING
|
||||||
self._checksum_mode: ChecksumMode | None = None
|
self._checksum_mode: Optional[ChecksumMode] = None
|
||||||
self._confirm_count = 0
|
self._confirm_count = 0
|
||||||
self._loss_count = 0
|
self._loss_count = 0
|
||||||
self._ascii_buffer = ''
|
self._ascii_buffer = ''
|
||||||
@@ -53,10 +53,10 @@ class FrameParser:
|
|||||||
return self._sync_state
|
return self._sync_state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def checksum_mode(self) -> ChecksumMode | None:
|
def checksum_mode(self) -> Optional[ChecksumMode]:
|
||||||
return self._checksum_mode
|
return self._checksum_mode
|
||||||
|
|
||||||
def feed(self, data: bytes) -> list[ParsedFrame | str]:
|
def feed(self, data: bytes) -> list[Union[ParsedFrame, str]]:
|
||||||
"""Feed raw bytes into the parser.
|
"""Feed raw bytes into the parser.
|
||||||
|
|
||||||
Returns a list of:
|
Returns a list of:
|
||||||
@@ -64,7 +64,7 @@ class FrameParser:
|
|||||||
- str for ASCII log lines or warning messages
|
- str for ASCII log lines or warning messages
|
||||||
"""
|
"""
|
||||||
self._remained += data
|
self._remained += data
|
||||||
results: list[ParsedFrame | str] = []
|
results: list[Union[ParsedFrame, str]] = []
|
||||||
|
|
||||||
# Bounded buffer check (Review Correction #2)
|
# Bounded buffer check (Review Correction #2)
|
||||||
if len(self._remained) > MAX_REMAINDER_SIZE:
|
if len(self._remained) > MAX_REMAINDER_SIZE:
|
||||||
@@ -85,7 +85,10 @@ class FrameParser:
|
|||||||
results.append(frame)
|
results.append(frame)
|
||||||
offset = next_offset
|
offset = next_offset
|
||||||
self._on_frame_found(mode)
|
self._on_frame_found(mode)
|
||||||
elif self._sync_state == SyncState.CONFIRMING_SYNC and self._might_be_incomplete_frame(buf, offset):
|
elif (
|
||||||
|
self._sync_state == SyncState.CONFIRMING_SYNC
|
||||||
|
and self._might_be_incomplete_frame(buf, offset)
|
||||||
|
):
|
||||||
break
|
break
|
||||||
elif (
|
elif (
|
||||||
self._sync_state == SyncState.SEARCHING
|
self._sync_state == SyncState.SEARCHING
|
||||||
@@ -95,7 +98,7 @@ class FrameParser:
|
|||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
if not self._ever_synced:
|
if not self._ever_synced:
|
||||||
self._collect_ascii(buf[offset : offset + 1], results)
|
self._collect_ascii(buf[offset:offset + 1], results)
|
||||||
offset += 1
|
offset += 1
|
||||||
else:
|
else:
|
||||||
# SYNCED or CONFIRMING_LOSS: use locked checksum mode
|
# SYNCED or CONFIRMING_LOSS: use locked checksum mode
|
||||||
@@ -132,7 +135,7 @@ class FrameParser:
|
|||||||
offset: int,
|
offset: int,
|
||||||
checksum_fn: Callable[[bytes], int],
|
checksum_fn: Callable[[bytes], int],
|
||||||
scope: ChecksumScope,
|
scope: ChecksumScope,
|
||||||
) -> tuple[ParsedFrame, int] | None:
|
) -> Optional[tuple[ParsedFrame, int]]:
|
||||||
"""Try to parse a frame at the given offset with specific checksum params."""
|
"""Try to parse a frame at the given offset with specific checksum params."""
|
||||||
if offset + FRAME_HEADER_SIZE > len(buf):
|
if offset + FRAME_HEADER_SIZE > len(buf):
|
||||||
return None
|
return None
|
||||||
@@ -145,8 +148,10 @@ class FrameParser:
|
|||||||
if offset + FRAME_OVERHEAD + payload_len > len(buf):
|
if offset + FRAME_OVERHEAD + payload_len > len(buf):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
header = buf[offset : offset + FRAME_HEADER_SIZE]
|
header = buf[offset:offset + FRAME_HEADER_SIZE]
|
||||||
payload = buf[offset + FRAME_HEADER_SIZE : offset + FRAME_HEADER_SIZE + payload_len]
|
payload = buf[
|
||||||
|
offset + FRAME_HEADER_SIZE:offset + FRAME_HEADER_SIZE + payload_len
|
||||||
|
]
|
||||||
checksum_offset = offset + FRAME_HEADER_SIZE + payload_len
|
checksum_offset = offset + FRAME_HEADER_SIZE + payload_len
|
||||||
stored_checksum = CHECKSUM_STRUCT.unpack_from(buf, checksum_offset)[0]
|
stored_checksum = CHECKSUM_STRUCT.unpack_from(buf, checksum_offset)[0]
|
||||||
|
|
||||||
@@ -177,7 +182,9 @@ class FrameParser:
|
|||||||
next_offset = offset + FRAME_OVERHEAD + payload_len
|
next_offset = offset + FRAME_OVERHEAD + payload_len
|
||||||
return frame, next_offset
|
return frame, next_offset
|
||||||
|
|
||||||
def _try_parse_with_probe(self, buf: bytes, offset: int) -> tuple[ParsedFrame, int, ChecksumMode] | None:
|
def _try_parse_with_probe(
|
||||||
|
self, buf: bytes, offset: int
|
||||||
|
) -> Optional[tuple[ParsedFrame, int, ChecksumMode]]:
|
||||||
"""Try all checksum combinations at the given offset (SEARCHING mode)."""
|
"""Try all checksum combinations at the given offset (SEARCHING mode)."""
|
||||||
for algo, scope, fn in _CHECKSUM_PROBES:
|
for algo, scope, fn in _CHECKSUM_PROBES:
|
||||||
result = self._try_parse_at(buf, offset, fn, scope)
|
result = self._try_parse_at(buf, offset, fn, scope)
|
||||||
@@ -187,11 +194,17 @@ class FrameParser:
|
|||||||
return frame, next_offset, mode
|
return frame, next_offset, mode
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _try_parse_locked(self, buf: bytes, offset: int) -> tuple[ParsedFrame, int] | None:
|
def _try_parse_locked(
|
||||||
|
self, buf: bytes, offset: int
|
||||||
|
) -> Optional[tuple[ParsedFrame, int]]:
|
||||||
"""Try to parse with the locked checksum mode."""
|
"""Try to parse with the locked checksum mode."""
|
||||||
if self._checksum_mode is None:
|
if self._checksum_mode is None:
|
||||||
return None
|
return None
|
||||||
fn = xor_checksum if self._checksum_mode.algorithm == ChecksumAlgorithm.XOR else sum_checksum
|
fn = (
|
||||||
|
xor_checksum
|
||||||
|
if self._checksum_mode.algorithm == ChecksumAlgorithm.XOR
|
||||||
|
else sum_checksum
|
||||||
|
)
|
||||||
return self._try_parse_at(buf, offset, fn, self._checksum_mode.scope)
|
return self._try_parse_at(buf, offset, fn, self._checksum_mode.scope)
|
||||||
|
|
||||||
def _on_frame_found(self, mode: ChecksumMode) -> None:
|
def _on_frame_found(self, mode: ChecksumMode) -> None:
|
||||||
@@ -241,7 +254,10 @@ class FrameParser:
|
|||||||
return True
|
return True
|
||||||
if remaining >= FRAME_HEADER_SIZE:
|
if remaining >= FRAME_HEADER_SIZE:
|
||||||
payload_len, _ = HEADER_STRUCT.unpack_from(buf, offset)
|
payload_len, _ = HEADER_STRUCT.unpack_from(buf, offset)
|
||||||
if payload_len <= MAX_FRAME_SIZE and remaining < FRAME_OVERHEAD + payload_len:
|
if (
|
||||||
|
payload_len <= MAX_FRAME_SIZE
|
||||||
|
and remaining < FRAME_OVERHEAD + payload_len
|
||||||
|
):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -250,7 +266,9 @@ class FrameParser:
|
|||||||
self._ever_synced = True
|
self._ever_synced = True
|
||||||
self._sync_state = new_state
|
self._sync_state = new_state
|
||||||
|
|
||||||
def _collect_ascii(self, byte_data: bytes, results: list[ParsedFrame | str]) -> None:
|
def _collect_ascii(
|
||||||
|
self, byte_data: bytes, results: list[Union[ParsedFrame, str]]
|
||||||
|
) -> None:
|
||||||
"""Collect bytes for ASCII line assembly.
|
"""Collect bytes for ASCII line assembly.
|
||||||
|
|
||||||
Only printable ASCII (0x20-0x7E) and newline (0x0A) are collected.
|
Only printable ASCII (0x20-0x7E) and newline (0x0A) are collected.
|
||||||
@@ -265,7 +283,7 @@ class FrameParser:
|
|||||||
results.append(self._ascii_buffer)
|
results.append(self._ascii_buffer)
|
||||||
self._ascii_buffer = ''
|
self._ascii_buffer = ''
|
||||||
|
|
||||||
def _flush_ascii(self, results: list[ParsedFrame | str]) -> None:
|
def _flush_ascii(self, results: list[Union[ParsedFrame, str]]) -> None:
|
||||||
"""Flush any pending ASCII buffer."""
|
"""Flush any pending ASCII buffer."""
|
||||||
if self._ascii_buffer:
|
if self._ascii_buffer:
|
||||||
results.append(self._ascii_buffer)
|
results.append(self._ascii_buffer)
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
"""Decode BLE_LOG_SRC_INTERNAL(0) frame payloads.
|
"""Decode BLE_LOG_SRC_INTERNAL(0) frame payloads.
|
||||||
|
|
||||||
Payload format on wire: [4B os_ts][1B int_src_code][variable sub-payload]
|
Payload format on wire: [4B os_ts][1B int_src_code][variable sub-payload]
|
||||||
See Spec Section 9.
|
See Spec Section 9.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import struct
|
import struct
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from src.backend.models import BufUtilResult
|
from src.backend.models import BufUtilResult
|
||||||
from src.backend.models import EnhStatResult
|
from src.backend.models import EnhStatResult
|
||||||
@@ -28,7 +27,7 @@ _ENH_STAT_STRUCT = struct.Struct('<BBIIII')
|
|||||||
_BUF_UTIL_STRUCT = struct.Struct('<BBBB')
|
_BUF_UTIL_STRUCT = struct.Struct('<BBBB')
|
||||||
|
|
||||||
|
|
||||||
def decode_internal_frame(payload: bytes) -> InternalDecoderResult | None:
|
def decode_internal_frame(payload: bytes) -> Optional[InternalDecoderResult]:
|
||||||
"""Decode an INTERNAL frame payload.
|
"""Decode an INTERNAL frame payload.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -65,9 +64,14 @@ def decode_internal_frame(payload: bytes) -> InternalDecoderResult | None:
|
|||||||
if int_src == InternalSource.ENH_STAT:
|
if int_src == InternalSource.ENH_STAT:
|
||||||
if len(sub_payload) < _ENH_STAT_STRUCT.size:
|
if len(sub_payload) < _ENH_STAT_STRUCT.size:
|
||||||
return None
|
return None
|
||||||
_, src_code, written_frame_cnt, lost_frame_cnt, written_bytes_cnt, lost_bytes_cnt = (
|
(
|
||||||
_ENH_STAT_STRUCT.unpack_from(sub_payload, 0)
|
_,
|
||||||
)
|
src_code,
|
||||||
|
written_frame_cnt,
|
||||||
|
lost_frame_cnt,
|
||||||
|
written_bytes_cnt,
|
||||||
|
lost_bytes_cnt,
|
||||||
|
) = _ENH_STAT_STRUCT.unpack_from(sub_payload, 0)
|
||||||
return EnhStatResult(
|
return EnhStatResult(
|
||||||
int_src=int_src,
|
int_src=int_src,
|
||||||
src_code=src_code,
|
src_code=src_code,
|
||||||
@@ -81,7 +85,9 @@ def decode_internal_frame(payload: bytes) -> InternalDecoderResult | None:
|
|||||||
if int_src == InternalSource.BUF_UTIL:
|
if int_src == InternalSource.BUF_UTIL:
|
||||||
if len(sub_payload) < _BUF_UTIL_STRUCT.size:
|
if len(sub_payload) < _BUF_UTIL_STRUCT.size:
|
||||||
return None
|
return None
|
||||||
_, lbm_id, trans_cnt, inflight_peak = _BUF_UTIL_STRUCT.unpack_from(sub_payload, 0)
|
_, lbm_id, trans_cnt, inflight_peak = _BUF_UTIL_STRUCT.unpack_from(
|
||||||
|
sub_payload, 0
|
||||||
|
)
|
||||||
pool = (lbm_id >> 4) & 0x0F
|
pool = (lbm_id >> 4) & 0x0F
|
||||||
index = lbm_id & 0x0F
|
index = lbm_id & 0x0F
|
||||||
return BufUtilResult(
|
return BufUtilResult(
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import struct
|
import struct
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from dataclasses import field
|
from dataclasses import field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from textual.message import Message
|
from textual.message import Message
|
||||||
|
|
||||||
@@ -87,7 +88,9 @@ _NO_OS_TS_SOURCES: frozenset[int] = frozenset(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
_LL_SOURCES: frozenset[int] = frozenset({BleLogSource.LL_TASK, BleLogSource.LL_HCI, BleLogSource.LL_ISR})
|
_LL_SOURCES: frozenset[int] = frozenset(
|
||||||
|
{BleLogSource.LL_TASK, BleLogSource.LL_HCI, BleLogSource.LL_ISR}
|
||||||
|
)
|
||||||
|
|
||||||
LL_TS_OFFSET = 2 # lc_ts starts at payload[2:6]
|
LL_TS_OFFSET = 2 # lc_ts starts at payload[2:6]
|
||||||
LL_TS_SIZE = 4
|
LL_TS_SIZE = 4
|
||||||
@@ -130,13 +133,13 @@ class BufUtilPool(int, Enum):
|
|||||||
# --- Data classes ---
|
# --- Data classes ---
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass
|
||||||
class ChecksumMode:
|
class ChecksumMode:
|
||||||
algorithm: ChecksumAlgorithm
|
algorithm: ChecksumAlgorithm
|
||||||
scope: ChecksumScope
|
scope: ChecksumScope
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass
|
||||||
class ParsedFrame:
|
class ParsedFrame:
|
||||||
source_code: int
|
source_code: int
|
||||||
frame_sn: int
|
frame_sn: int
|
||||||
@@ -144,7 +147,7 @@ class ParsedFrame:
|
|||||||
os_ts_ms: int # extracted from first 4 bytes of payload; only valid when has_os_ts(source_code) is True
|
os_ts_ms: int # extracted from first 4 bytes of payload; only valid when has_os_ts(source_code) is True
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass
|
||||||
class SourcePeakWrite:
|
class SourcePeakWrite:
|
||||||
"""Peak write burst for a single source within a 1ms window."""
|
"""Peak write burst for a single source within a 1ms window."""
|
||||||
|
|
||||||
@@ -152,7 +155,7 @@ class SourcePeakWrite:
|
|||||||
peak_bytes: int = 0 # total bytes in that same window
|
peak_bytes: int = 0 # total bytes in that same window
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass
|
||||||
class SourceStats:
|
class SourceStats:
|
||||||
"""Console-side accumulated per-source statistics (resilient to firmware counter resets)."""
|
"""Console-side accumulated per-source statistics (resilient to firmware counter resets)."""
|
||||||
|
|
||||||
@@ -202,7 +205,7 @@ def resolve_lbm_name(pool: int, index: int) -> str:
|
|||||||
return f'lbm_{pool}_{index}'
|
return f'lbm_{pool}_{index}'
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass
|
||||||
class TransportSnapshot:
|
class TransportSnapshot:
|
||||||
"""Snapshot of transport-layer metrics for the current stats interval."""
|
"""Snapshot of transport-layer metrics for the current stats interval."""
|
||||||
|
|
||||||
@@ -212,7 +215,7 @@ class TransportSnapshot:
|
|||||||
fps: float = 0.0
|
fps: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass
|
||||||
class LossSnapshot:
|
class LossSnapshot:
|
||||||
"""Snapshot of firmware-reported cumulative loss."""
|
"""Snapshot of firmware-reported cumulative loss."""
|
||||||
|
|
||||||
@@ -220,12 +223,12 @@ class LossSnapshot:
|
|||||||
total_bytes: int = 0
|
total_bytes: int = 0
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass
|
||||||
class PeakBurstSnapshot:
|
class PeakBurstSnapshot:
|
||||||
"""Peak write burst metrics for a single clock domain (os_ts or lc_ts)."""
|
"""Peak write burst metrics for a single clock domain (os_ts or lc_ts)."""
|
||||||
|
|
||||||
per_source: dict[SourceCode, SourcePeakWrite] | None = None
|
per_source: Optional[dict[SourceCode, SourcePeakWrite]] = None
|
||||||
max_per_source: dict[SourceCode, SourcePeakWrite] | None = None
|
max_per_source: Optional[dict[SourceCode, SourcePeakWrite]] = None
|
||||||
|
|
||||||
|
|
||||||
class LossType(str, Enum):
|
class LossType(str, Enum):
|
||||||
@@ -271,7 +274,7 @@ class FunnelSnapshot:
|
|||||||
throughput: ThroughputInfo
|
throughput: ThroughputInfo
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass
|
||||||
class LaunchConfig:
|
class LaunchConfig:
|
||||||
"""Configuration returned by the Launch Screen."""
|
"""Configuration returned by the Launch Screen."""
|
||||||
|
|
||||||
@@ -280,7 +283,7 @@ class LaunchConfig:
|
|||||||
log_dir: Path
|
log_dir: Path
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass
|
||||||
class FrameStats:
|
class FrameStats:
|
||||||
"""Periodic stats snapshot with metrics grouped by dimension."""
|
"""Periodic stats snapshot with metrics grouped by dimension."""
|
||||||
|
|
||||||
@@ -288,10 +291,10 @@ class FrameStats:
|
|||||||
loss: LossSnapshot = field(default_factory=LossSnapshot)
|
loss: LossSnapshot = field(default_factory=LossSnapshot)
|
||||||
os_peak: PeakBurstSnapshot = field(default_factory=PeakBurstSnapshot)
|
os_peak: PeakBurstSnapshot = field(default_factory=PeakBurstSnapshot)
|
||||||
ll_peak: PeakBurstSnapshot = field(default_factory=PeakBurstSnapshot)
|
ll_peak: PeakBurstSnapshot = field(default_factory=PeakBurstSnapshot)
|
||||||
per_source_rx_bytes: dict[SourceCode, int] | None = None
|
per_source_rx_bytes: Optional[dict[SourceCode, int]] = None
|
||||||
sync_state: SyncState = SyncState.SEARCHING
|
sync_state: SyncState = SyncState.SEARCHING
|
||||||
checksum_algorithm: ChecksumAlgorithm | None = None
|
checksum_algorithm: Optional[ChecksumAlgorithm] = None
|
||||||
checksum_scope: ChecksumScope | None = None
|
checksum_scope: Optional[ChecksumScope] = None
|
||||||
|
|
||||||
|
|
||||||
# --- TypedDicts for internal decoder results ---
|
# --- TypedDicts for internal decoder results ---
|
||||||
@@ -323,7 +326,7 @@ class BufUtilResult(TypedDict):
|
|||||||
os_ts_ms: int
|
os_ts_ms: int
|
||||||
|
|
||||||
|
|
||||||
InternalDecoderResult = InfoResult | EnhStatResult | BufUtilResult
|
InternalDecoderResult = Union[InfoResult, EnhStatResult, BufUtilResult]
|
||||||
|
|
||||||
|
|
||||||
# --- Textual Messages (backend -> frontend) ---
|
# --- Textual Messages (backend -> frontend) ---
|
||||||
@@ -339,8 +342,8 @@ class StatsUpdated(Message):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
stats: FrameStats,
|
stats: FrameStats,
|
||||||
funnel_snapshots: list[FunnelSnapshot] | None = None,
|
funnel_snapshots: Optional[list[FunnelSnapshot]] = None,
|
||||||
buf_util_snapshots: list[BufUtilEntry] | None = None,
|
buf_util_snapshots: Optional[list[BufUtilEntry]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.stats = stats
|
self.stats = stats
|
||||||
@@ -368,7 +371,7 @@ class FrameLossDetected(Message):
|
|||||||
loss_type: LossType,
|
loss_type: LossType,
|
||||||
lost_frames: int,
|
lost_frames: int,
|
||||||
lost_bytes: int,
|
lost_bytes: int,
|
||||||
sn_range: tuple[int, int] | None = None,
|
sn_range: Optional[tuple[int, int]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.source_name = source_name
|
self.source_name = source_name
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
"""Stats package -- re-exports for backward-compatible imports."""
|
"""Stats package -- re-exports for backward-compatible imports."""
|
||||||
|
|
||||||
from src.backend.stats.accumulator import StatsAccumulator
|
from src.backend.stats.accumulator import StatsAccumulator
|
||||||
from src.backend.stats.firmware_loss import FirmwareLossTracker
|
from src.backend.stats.firmware_loss import FirmwareLossTracker
|
||||||
from src.backend.stats.firmware_written import FirmwareWrittenTracker
|
from src.backend.stats.firmware_written import FirmwareWrittenTracker
|
||||||
from src.backend.stats.peak_burst import WRITE_RATE_WINDOW_MS
|
|
||||||
from src.backend.stats.peak_burst import PeakBurstTracker
|
from src.backend.stats.peak_burst import PeakBurstTracker
|
||||||
|
from src.backend.stats.peak_burst import WRITE_RATE_WINDOW_MS
|
||||||
from src.backend.stats.sn_gap import REORDER_WINDOW
|
from src.backend.stats.sn_gap import REORDER_WINDOW
|
||||||
from src.backend.stats.sn_gap import SN_MAX
|
from src.backend.stats.sn_gap import SN_MAX
|
||||||
from src.backend.stats.sn_gap import SNGapTracker
|
from src.backend.stats.sn_gap import SNGapTracker
|
||||||
@@ -16,8 +14,8 @@ from src.backend.stats.traffic_spike import TRAFFIC_THRESHOLD_PCT
|
|||||||
from src.backend.stats.traffic_spike import TRAFFIC_WINDOW_SEC
|
from src.backend.stats.traffic_spike import TRAFFIC_WINDOW_SEC
|
||||||
from src.backend.stats.traffic_spike import TrafficSpikeDetector
|
from src.backend.stats.traffic_spike import TrafficSpikeDetector
|
||||||
from src.backend.stats.traffic_spike import TrafficSpikeResult
|
from src.backend.stats.traffic_spike import TrafficSpikeResult
|
||||||
from src.backend.stats.transport import UART_BITS_PER_BYTE
|
|
||||||
from src.backend.stats.transport import TransportMetrics
|
from src.backend.stats.transport import TransportMetrics
|
||||||
|
from src.backend.stats.transport import UART_BITS_PER_BYTE
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'FirmwareLossTracker',
|
'FirmwareLossTracker',
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from src.backend.models import BufUtilEntry
|
from src.backend.models import BufUtilEntry
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
"""Firmware ENH_STAT loss tracking with incremental delta accumulation.
|
"""Firmware ENH_STAT loss tracking with incremental delta accumulation.
|
||||||
|
|
||||||
Resilient to firmware counter resets from ``ble_log_bench_reset_stat``.
|
Resilient to firmware counter resets from ``ble_log_bench_reset_stat``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from src.backend.models import LossSnapshot
|
from src.backend.models import LossSnapshot
|
||||||
from src.backend.models import SourceCode
|
from src.backend.models import SourceCode
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
from src.backend.models import SourceCode
|
from src.backend.models import SourceCode
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
"""Sliding-window peak write burst tracker.
|
"""Sliding-window peak write burst tracker.
|
||||||
|
|
||||||
Tracks the densest burst of log writes within a configurable time window
|
Tracks the densest burst of log writes within a configurable time window
|
||||||
@@ -12,7 +11,6 @@ happen at microsecond frequency, many frames share the same ms timestamp.
|
|||||||
Instead of computing an inaccurate fps, we count frames and bytes within
|
Instead of computing an inaccurate fps, we count frames and bytes within
|
||||||
the densest window.
|
the densest window.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
|
||||||
from src.backend.models import PeakBurstSnapshot
|
from src.backend.models import PeakBurstSnapshot
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
"""Sliding receive window gap tracker for per-source frame sequence numbers.
|
"""Sliding receive window gap tracker for per-source frame sequence numbers.
|
||||||
|
|
||||||
Frames are only declared lost when the receive window advances past their SN
|
Frames are only declared lost when the receive window advances past their SN
|
||||||
without them being received, tolerating out-of-order delivery up to
|
without them being received, tolerating out-of-order delivery up to
|
||||||
REORDER_WINDOW frames.
|
REORDER_WINDOW frames.
|
||||||
"""
|
"""
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from src.backend.models import SourceCode
|
from src.backend.models import SourceCode
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ class SNGapTracker:
|
|||||||
"""Return cumulative confirmed gap count per source."""
|
"""Return cumulative confirmed gap count per source."""
|
||||||
return dict(self._gap_accum)
|
return dict(self._gap_accum)
|
||||||
|
|
||||||
def reset(self, src_code: SourceCode | None = None) -> None:
|
def reset(self, src_code: Optional[SourceCode] = None) -> None:
|
||||||
"""Reset tracker state.
|
"""Reset tracker state.
|
||||||
|
|
||||||
If src_code is None, resets all sources.
|
If src_code is None, resets all sources.
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
"""Real-time traffic spike detection using a sliding window over wall-clock time."""
|
"""Real-time traffic spike detection using a sliding window over wall-clock time."""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from src.backend.models import SourceCode
|
from src.backend.models import SourceCode
|
||||||
|
|
||||||
@@ -14,7 +13,7 @@ TRAFFIC_THRESHOLD_PCT = 0.8 # 80% of wire max
|
|||||||
TRAFFIC_ALERT_COOLDOWN_SEC = 2.0 # minimum interval between alerts
|
TRAFFIC_ALERT_COOLDOWN_SEC = 2.0 # minimum interval between alerts
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass
|
||||||
class TrafficSpikeResult:
|
class TrafficSpikeResult:
|
||||||
throughput_kbs: float
|
throughput_kbs: float
|
||||||
wire_max_kbs: float
|
wire_max_kbs: float
|
||||||
@@ -41,9 +40,11 @@ class TrafficSpikeDetector:
|
|||||||
def record(self, frame_size: int, src_code: SourceCode) -> None:
|
def record(self, frame_size: int, src_code: SourceCode) -> None:
|
||||||
self._window.append((time.perf_counter(), frame_size, src_code))
|
self._window.append((time.perf_counter(), frame_size, src_code))
|
||||||
if self._spike_active:
|
if self._spike_active:
|
||||||
self._spike_per_source[src_code] = self._spike_per_source.get(src_code, 0) + frame_size
|
self._spike_per_source[src_code] = (
|
||||||
|
self._spike_per_source.get(src_code, 0) + frame_size
|
||||||
|
)
|
||||||
|
|
||||||
def check(self) -> TrafficSpikeResult | None:
|
def check(self) -> Optional[TrafficSpikeResult]:
|
||||||
now = time.perf_counter()
|
now = time.perf_counter()
|
||||||
window = self._window
|
window = self._window
|
||||||
cutoff = now - TRAFFIC_WINDOW_SEC
|
cutoff = now - TRAFFIC_WINDOW_SEC
|
||||||
@@ -83,7 +84,9 @@ class TrafficSpikeDetector:
|
|||||||
|
|
||||||
spike_bps = self._spike_peak_bps
|
spike_bps = self._spike_peak_bps
|
||||||
src_total = max(sum(self._spike_per_source.values()), 1)
|
src_total = max(sum(self._spike_per_source.values()), 1)
|
||||||
src_pcts = {src: v / src_total * 100.0 for src, v in self._spike_per_source.items()}
|
src_pcts = {
|
||||||
|
src: v / src_total * 100.0 for src, v in self._spike_per_source.items()
|
||||||
|
}
|
||||||
|
|
||||||
return TrafficSpikeResult(
|
return TrafficSpikeResult(
|
||||||
throughput_kbs=spike_bps / 1024.0,
|
throughput_kbs=spike_bps / 1024.0,
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
"""Transport-layer metrics: RX bytes, throughput (bps), frame rate (fps)."""
|
"""Transport-layer metrics: RX bytes, throughput (bps), frame rate (fps)."""
|
||||||
|
|
||||||
from src.backend.models import TransportSnapshot
|
from src.backend.models import TransportSnapshot
|
||||||
|
|
||||||
UART_BITS_PER_BYTE = 10 # 8 data + 1 start + 1 stop
|
UART_BITS_PER_BYTE = 10 # 8 data + 1 start + 1 stop
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
"""UART read loop with raw binary file writing.
|
"""UART read loop with raw binary file writing.
|
||||||
|
|
||||||
See Spec Sections 6, 12.
|
See Spec Sections 6, 12.
|
||||||
"""
|
"""
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import serial
|
|
||||||
import serial.tools.list_ports
|
import serial.tools.list_ports
|
||||||
|
|
||||||
UART_READ_TIMEOUT = 0.1
|
UART_READ_TIMEOUT = 0.1
|
||||||
@@ -18,7 +17,7 @@ def list_serial_ports() -> list[str]:
|
|||||||
return [port.device for port in ports]
|
return [port.device for port in ports]
|
||||||
|
|
||||||
|
|
||||||
def validate_uart_port(port: str) -> str | None:
|
def validate_uart_port(port: str) -> Optional[str]:
|
||||||
"""Validate port exists and is accessible. Returns error message or None if valid."""
|
"""Validate port exists and is accessible. Returns error message or None if valid."""
|
||||||
available = list_serial_ports()
|
available = list_serial_ports()
|
||||||
if port not in available:
|
if port not in available:
|
||||||
@@ -28,6 +27,8 @@ def validate_uart_port(port: str) -> str | None:
|
|||||||
|
|
||||||
def open_serial(port: str, baudrate: int) -> serial.Serial:
|
def open_serial(port: str, baudrate: int) -> serial.Serial:
|
||||||
try:
|
try:
|
||||||
return serial.Serial(port, baudrate=baudrate, timeout=UART_READ_TIMEOUT, exclusive=True)
|
return serial.Serial(
|
||||||
|
port, baudrate=baudrate, timeout=UART_READ_TIMEOUT, exclusive=True
|
||||||
|
)
|
||||||
except (ValueError, serial.SerialException):
|
except (ValueError, serial.SerialException):
|
||||||
return serial.Serial(port, baudrate=baudrate, timeout=UART_READ_TIMEOUT)
|
return serial.Serial(port, baudrate=baudrate, timeout=UART_READ_TIMEOUT)
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
"""Launch Screen — interactive setup for port, baud rate, and log directory.
|
"""Launch Screen — interactive setup for port, baud rate, and log directory.
|
||||||
|
|
||||||
Shown on startup when --port is not provided via CLI.
|
Shown on startup when --port is not provided via CLI.
|
||||||
Dismissed with a LaunchConfig result on Connect, or None on quit.
|
Dismissed with a LaunchConfig result on Connect, or None on quit.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from src.backend.models import LaunchConfig
|
||||||
|
from src.backend.uart_transport import list_serial_ports
|
||||||
from textual import on
|
from textual import on
|
||||||
from textual import work
|
from textual import work
|
||||||
from textual.app import ComposeResult
|
from textual.app import ComposeResult
|
||||||
@@ -22,14 +23,11 @@ from textual.widgets import Label
|
|||||||
from textual.widgets import Select
|
from textual.widgets import Select
|
||||||
from textual_fspicker import SelectDirectory
|
from textual_fspicker import SelectDirectory
|
||||||
|
|
||||||
from src.backend.models import LaunchConfig
|
|
||||||
from src.backend.uart_transport import list_serial_ports
|
|
||||||
|
|
||||||
BAUD_RATES: list[int] = [115200, 230400, 460800, 921600, 1500000, 2000000, 3000000]
|
BAUD_RATES: list[int] = [115200, 230400, 460800, 921600, 1500000, 2000000, 3000000]
|
||||||
DEFAULT_BAUD_RATE: int = 3000000
|
DEFAULT_BAUD_RATE: int = 3000000
|
||||||
|
|
||||||
|
|
||||||
class LaunchScreen(Screen[LaunchConfig | None]):
|
class LaunchScreen(Screen[Optional[LaunchConfig]]):
|
||||||
"""Interactive setup screen for BLE Log Console."""
|
"""Interactive setup screen for BLE Log Console."""
|
||||||
|
|
||||||
DEFAULT_CSS = """
|
DEFAULT_CSS = """
|
||||||
@@ -93,7 +91,7 @@ class LaunchScreen(Screen[LaunchConfig | None]):
|
|||||||
Binding('ctrl+c', 'quit', show=False, priority=True),
|
Binding('ctrl+c', 'quit', show=False, priority=True),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, default_log_dir: Path | None = None) -> None:
|
def __init__(self, default_log_dir: Optional[Path] = None) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._default_log_dir = default_log_dir or Path.cwd()
|
self._default_log_dir = default_log_dir or Path.cwd()
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
"""Scrollable log view widget.
|
"""Scrollable log view widget.
|
||||||
|
|
||||||
See Spec Section 11.
|
See Spec Section 11.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
from textual.widgets import RichLog
|
from textual.widgets import RichLog
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
"""Modal screen showing available keyboard shortcuts.
|
"""Modal screen showing available keyboard shortcuts.
|
||||||
|
|
||||||
Pushed by the 'h' keybinding; dismissed by Escape or 'h' again.
|
Pushed by the 'h' keybinding; dismissed by Escape or 'h' again.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
from textual.app import ComposeResult
|
from textual.app import ComposeResult
|
||||||
from textual.binding import Binding
|
from textual.binding import Binding
|
||||||
|
|||||||
@@ -1,32 +1,29 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
"""Modal screen for per-source frame statistics display.
|
"""Modal screen for per-source frame statistics display.
|
||||||
|
|
||||||
Pushed by the 'd' keybinding; dismissed by Escape or 'd' again.
|
Pushed by the 'd' keybinding; dismissed by Escape or 'd' again.
|
||||||
Refreshes every second to show live throughput data.
|
Refreshes every second to show live throughput data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
from src.backend.models import BufUtilEntry
|
||||||
|
from src.backend.models import format_bytes
|
||||||
|
from src.backend.models import format_throughput
|
||||||
|
from src.backend.models import FunnelSnapshot
|
||||||
|
from src.backend.models import resolve_lbm_name
|
||||||
|
from src.backend.models import resolve_pool_name
|
||||||
|
from src.backend.models import resolve_source_name
|
||||||
from textual.app import ComposeResult
|
from textual.app import ComposeResult
|
||||||
from textual.binding import Binding
|
from textual.binding import Binding
|
||||||
from textual.containers import Vertical
|
from textual.containers import Vertical
|
||||||
from textual.screen import ModalScreen
|
from textual.screen import ModalScreen
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
from src.backend.models import BufUtilEntry
|
|
||||||
from src.backend.models import FunnelSnapshot
|
|
||||||
from src.backend.models import format_bytes
|
|
||||||
from src.backend.models import format_throughput
|
|
||||||
from src.backend.models import resolve_lbm_name
|
|
||||||
from src.backend.models import resolve_pool_name
|
|
||||||
from src.backend.models import resolve_source_name
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from src.app import BLELogApp
|
from src.app import BLELogApp
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import struct
|
import struct
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
from src.backend.stats.buf_util import BufUtilTracker
|
from src.backend.stats.buf_util import BufUtilTracker
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
from src.backend.checksum import sum_checksum
|
from src.backend.checksum import sum_checksum
|
||||||
from src.backend.checksum import xor_checksum
|
from src.backend.checksum import xor_checksum
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
from src.backend.stats.firmware_loss import FirmwareLossTracker
|
from src.backend.stats.firmware_loss import FirmwareLossTracker
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
from src.backend.stats.firmware_written import FirmwareWrittenTracker
|
from src.backend.stats.firmware_written import FirmwareWrittenTracker
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
from src.backend.internal_decoder import decode_internal_frame
|
from src.backend.internal_decoder import decode_internal_frame
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
from src.backend.stats.peak_burst import WRITE_RATE_WINDOW_MS
|
|
||||||
from src.backend.stats.peak_burst import PeakBurstTracker
|
|
||||||
from src.backend.stats.peak_burst import _ts_delta_ms
|
from src.backend.stats.peak_burst import _ts_delta_ms
|
||||||
|
from src.backend.stats.peak_burst import PeakBurstTracker
|
||||||
|
from src.backend.stats.peak_burst import WRITE_RATE_WINDOW_MS
|
||||||
|
|
||||||
_SRC = 1
|
_SRC = 1
|
||||||
_SRC_B = 2
|
_SRC_B = 2
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
from typing import Optional
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from src.backend.stats.traffic_spike import TRAFFIC_ALERT_COOLDOWN_SEC
|
from src.backend.stats.traffic_spike import TRAFFIC_ALERT_COOLDOWN_SEC
|
||||||
@@ -17,7 +17,7 @@ def _make_detector(baudrate: int = 3_000_000) -> TrafficSpikeDetector:
|
|||||||
|
|
||||||
def _trigger_spike(
|
def _trigger_spike(
|
||||||
d: TrafficSpikeDetector, mock_time: object, t: float, hot_bytes: int, src: int = 1
|
d: TrafficSpikeDetector, mock_time: object, t: float, hot_bytes: int, src: int = 1
|
||||||
) -> TrafficSpikeResult | None:
|
) -> Optional[TrafficSpikeResult]:
|
||||||
mock_time.perf_counter.return_value = t # type: ignore[attr-defined]
|
mock_time.perf_counter.return_value = t # type: ignore[attr-defined]
|
||||||
d.record(hot_bytes, src)
|
d.record(hot_bytes, src)
|
||||||
mock_time.perf_counter.return_value = t + 0.05 # type: ignore[attr-defined]
|
mock_time.perf_counter.return_value = t + 0.05 # type: ignore[attr-defined]
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
from src.backend.stats.transport import TransportMetrics
|
from src.backend.stats.transport import TransportMetrics
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from src.backend.uart_transport import validate_uart_port
|
from src.backend.uart_transport import validate_uart_port
|
||||||
|
|||||||
Reference in New Issue
Block a user