feat(lab-gateway): PPK2 simulation mode for offline development/CI

This commit is contained in:
fabiorafaelcoutada 2026-07-06 02:27:00 +01:00
parent 7d7a591bee
commit 137a594abe

View file

@ -5,6 +5,9 @@ Uses ppk2-api for serial communication with the PPK2 device.
Provides real-time power measurements via WebSocket-compatible async generator.
"""
import math
import os
import random
import time
import threading
import json
@ -19,6 +22,63 @@ except ImportError:
print("[PPK2] ppk2-api not installed — PPK2 features disabled")
SIMULATION = os.environ.get("PPK2_SIMULATION", "0") == "1"
class SimulatedPPK2:
"""Software PPK2 stand-in for development and CI."""
def __init__(self):
self.mode = "AMPERE_MODE"
self.current_vdd = 3300
self._running = False
self._start_time = time.time()
self._lock = threading.Lock()
def use_ampere_meter(self):
self.mode = "AMPERE_MODE"
def use_source_meter(self):
self.mode = "SOURCE_MODE"
def set_source_voltage(self, mv: int):
with self._lock:
self.current_vdd = mv
def toggle_DUT_power(self, state: str):
pass
def start_measuring(self):
self._running = True
def stop_measuring(self):
self._running = False
def get_data(self) -> bytes:
if not self._running:
return b""
with self._lock:
vdd = self.current_vdd
t = time.time() - self._start_time
base = 25000 + (4200 - vdd) * 5 + random.uniform(-200, 200)
if int(t * 2) % 10 == 0:
base += 80000 + random.uniform(-1000, 1000)
analog = [int(base + math.sin(i * 0.1) * 50) for i in range(100)]
raw = b"".join(int((a & 0x3FFF) | (0 << 14)).to_bytes(4, "little") for a in analog)
return raw
def get_samples(self, raw: bytes):
"""Parse packed samples into (analog_values, [digital_values])."""
analog = []
digital = []
for i in range(0, len(raw), 4):
sample = int.from_bytes(raw[i : i + 4], "little")
analog.append(float(sample & 0x3FFF))
digital.append((sample >> 24) & 0xFF)
return (analog, digital)
class PPK2Manager:
"""Manages the Nordic PPK2 power profiler."""
@ -33,9 +93,15 @@ class PPK2Manager:
self._voltage_mv = 3300
self._sample_buffer: deque = deque(maxlen=10000)
self._stats = {"avg_ua": 0, "peak_ua": 0, "min_ua": 0, "total_samples": 0, "energy_uwh": 0}
self._simulation = SIMULATION or not HAS_PPK2
def connect(self) -> bool:
"""Connect to the PPK2 device."""
"""Connect to the PPK2 device (or simulation if configured)."""
if self._simulation:
self.ppk2 = SimulatedPPK2()
print("[PPK2] Using simulation backend")
return True
if not HAS_PPK2:
return False
@ -179,5 +245,6 @@ class PPK2Manager:
"voltage_mv": self._voltage_mv,
"measuring": self._running,
"stats": self._stats,
"available": HAS_PPK2,
"available": HAS_PPK2 or self._simulation,
"simulation": self._simulation,
}