feat(lab-gateway): PPK2 simulation mode for offline development/CI
This commit is contained in:
parent
7d7a591bee
commit
137a594abe
1 changed files with 69 additions and 2 deletions
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue