decoder work missin anglura velicita and accelerations missing

main
Kynsight 3 weeks ago
parent de53882ead
commit c22cab503b

Binary file not shown.

@ -4,168 +4,247 @@ Decoder Module - vzug-e-hinge
============================== ==============================
Decodes raw UART and I2C data into telemetry fields. Decodes raw UART and I2C data into telemetry fields.
Current Implementation: Simple pass-through UART Packet Format (14 bytes, Little Endian / LSB first):
TODO: Implement actual byte unpacking later Byte 0-1: Start marker EF FE
Byte 2-3: MotorCurrent (signed 16-bit, LSB first)
Byte 4-5: EncoderValue (signed 16-bit, LSB first)
Byte 6-7: RelativeEncoderValue (signed 16-bit, LSB first)
Byte 8-9: V24PecDiff (signed 16-bit, LSB first)
Byte 10-11: Reserved0 (signed 16-bit, LSB first)
Byte 12: PWM (unsigned 8-bit, 0-100)
Byte 13: End marker EE
I2C Format (2 bytes, 14-bit angle from AS5600/AS5048A):
Byte 0: High byte (bits 13-6)
Byte 1: Low byte (bits 5-0)
Reference: eHinge Encoder.pdf, Datalogger files
Author: Kynsight Author: Kynsight
Version: 1.0.0 (pass-through) Version: 2.0.0 (full decoding)
Date: 2025-11-09 Date: 2025-11-22
""" """
from typing import Dict, Any from typing import Dict, Any, Optional
# UART Packet Constants
FRAME_LEN = 14
START0 = 0xEF
START1 = 0xFE
END = 0xEE
def _le_s16(b0: int, b1: int) -> int:
"""Decode Little Endian signed 16-bit value (LSB first)."""
u = (b1 << 8) | b0 # b1 is high byte, b0 is low byte
return u - 0x10000 if (u & 0x8000) else u
def decode_uart_packet(packet_bytes: bytes) -> Dict[str, Any]: def decode_uart_packet(packet_bytes: bytes) -> Dict[str, Any]:
""" """
Decode UART packet bytes into telemetry fields. Decode UART packet bytes into telemetry fields.
Current Implementation: Pass-through (returns raw data) Packet format: EF FE [data] EE (14 bytes total, Little Endian / LSB first)
TODO: Implement actual decoding based on packet format
Expected packet format: EF FE [14 bytes] EE (17 bytes total)
Future implementation should extract:
- motor_current (2 bytes)
- encoder_value (2 bytes)
- relative_encoder_value (2 bytes)
- v24_pec_diff (2 bytes)
- pwm (1 byte)
Args: Args:
packet_bytes: Raw packet bytes (including start/end markers) packet_bytes: Raw packet bytes (14 bytes including start/end markers)
Returns: Returns:
Dictionary with decoded fields (currently just raw data) Dictionary with decoded fields or None values if invalid
Example: Example:
packet_bytes = b'\\xEF\\xFE...[14 bytes]...\\xEE' packet_bytes = b'\\xEF\\xFE\\x04\\x00\\xED\\xFF\\x00\\x00\\x20\\x08\\x00\\x00\\x00\\xEE'
decoded = decode_uart_packet(packet_bytes) decoded = decode_uart_packet(packet_bytes)
# Currently returns: {'raw_hex': 'ef fe ... ee', 'raw_bytes': b'...'} # Returns: {'motor_current': 4, 'encoder_value': -19, 'v24_pec_diff': 2080, ...}
# Future: {'motor_current': 123, 'encoder_value': 456, ...}
""" """
# Validate packet
if packet_bytes is None or len(packet_bytes) != FRAME_LEN:
return {
'motor_current': None,
'encoder_value': None,
'relative_encoder_value': None,
'v24_pec_diff': None,
'pwm': None,
'reserved0': None,
'error': f'Invalid packet length: {len(packet_bytes) if packet_bytes else 0}, expected {FRAME_LEN}'
}
# Verify markers
if not (packet_bytes[0] == START0 and packet_bytes[1] == START1 and packet_bytes[13] == END):
return {
'motor_current': None,
'encoder_value': None,
'relative_encoder_value': None,
'v24_pec_diff': None,
'pwm': None,
'reserved0': None,
'error': f'Invalid markers: start={packet_bytes[0]:02X} {packet_bytes[1]:02X}, end={packet_bytes[13]:02X}'
}
# Decode fields (all Little Endian signed 16-bit except PWM)
motor_current = _le_s16(packet_bytes[2], packet_bytes[3])
encoder_value = _le_s16(packet_bytes[4], packet_bytes[5])
relative_encoder_value = _le_s16(packet_bytes[6], packet_bytes[7])
v24_pec_diff = _le_s16(packet_bytes[8], packet_bytes[9])
reserved0 = _le_s16(packet_bytes[10], packet_bytes[11])
pwm = int(packet_bytes[12]) # Unsigned 8-bit (0-100)
# Optional sanity check (adjust bounds as needed)
if not (-40000 <= motor_current <= 40000 and 0 <= pwm <= 100):
return {
'motor_current': motor_current,
'encoder_value': encoder_value,
'relative_encoder_value': relative_encoder_value,
'v24_pec_diff': v24_pec_diff,
'pwm': pwm,
'reserved0': reserved0,
'error': f'Out of range: motor_current={motor_current}, pwm={pwm}'
}
return { return {
'raw_hex': packet_bytes.hex(' '), 'motor_current': motor_current,
'raw_bytes': packet_bytes, 'encoder_value': encoder_value,
'packet_length': len(packet_bytes) 'relative_encoder_value': relative_encoder_value,
'v24_pec_diff': v24_pec_diff,
'pwm': pwm,
'reserved0': reserved0,
'error': None
} }
def decode_i2c_sample(i2c_bytes: bytes) -> Dict[str, Any]: def _wrap14_delta(current: int, zero: int) -> int:
"""
Calculate signed wrap-around delta in 14-bit space.
Maps (current - zero) to range [-8192..+8191] handling wrap-around.
Args:
current: Current 14-bit value (0-16383)
zero: Zero reference 14-bit value (0-16383)
Returns:
Signed delta in range [-8192, +8191]
Example:
_wrap14_delta(100, 16300) = 184 # Wrapped forward
_wrap14_delta(16300, 100) = -184 # Wrapped backward
"""
return ((current - zero + 8192) % 16384) - 8192
def decode_i2c_sample(i2c_bytes: Optional[bytes], zero_ref: int = 0) -> Dict[str, Any]:
""" """
Decode I2C sample bytes into angle telemetry. Decode I2C sample bytes into angle telemetry with zero reference.
Current Implementation: Pass-through (returns raw data) I2C format: 2 bytes (14-bit angle from AS5600/AS5048A sensor)
TODO: Implement actual decoding based on I2C format Byte 0: High byte (bits 13-6 of angle)
Byte 1: Low byte (bits 5-0 of angle)
Expected I2C format: 2 bytes (14-bit angle value)
Future implementation should extract:
- i2c_raw14 (14-bit raw value)
- i2c_angle_deg (converted to degrees)
- i2c_zero_raw14 (zero position)
- i2c_delta_raw14 (delta from zero)
Args: Args:
i2c_bytes: Raw I2C bytes (typically 2 bytes for angle) i2c_bytes: Raw I2C bytes (2 bytes)
zero_ref: Zero reference value (0-16383), default 0
Returns: Returns:
Dictionary with decoded fields (currently just raw data) Dictionary with decoded I2C angle fields
Example: Example:
i2c_bytes = b'\\x3F\\xFF' # 14-bit angle i2c_bytes = b'\\xEC\\x36' # Sample from sensor
decoded = decode_i2c_sample(i2c_bytes) decoded = decode_i2c_sample(i2c_bytes, zero_ref=11318)
# Currently returns: {'raw_hex': '3f ff', 'raw_bytes': b'...'} # Returns: {
# Future: {'i2c_raw14': 16383, 'i2c_angle_deg': 359.98, ...} # 'i2c_raw14': 11318,
# 'i2c_zero_raw14': 11318,
# 'i2c_delta_raw14': 0,
# 'i2c_angle_deg': 0.0,
# 'i2c_zero_angle_deg': 248.69
# }
""" """
# Validate input
if i2c_bytes is None or len(i2c_bytes) != 2:
return {
'i2c_raw14': None,
'i2c_zero_raw14': zero_ref if zero_ref != 0 else None,
'i2c_delta_raw14': None,
'i2c_angle_deg': None,
'i2c_zero_angle_deg': (zero_ref * 360.0 / 16384.0) if zero_ref != 0 else None,
'error': f'Invalid I2C length: {len(i2c_bytes) if i2c_bytes else 0}, expected 2'
}
# Decode 14-bit value
# Format: MSB contains bits [13:6], LSB contains bits [5:0]
msb, lsb = i2c_bytes[0], i2c_bytes[1]
raw14 = ((msb << 6) | (lsb & 0x3F)) & 0x3FFF
# Calculate delta from zero with wrap-around
delta14 = _wrap14_delta(raw14, zero_ref) if zero_ref != 0 else None
# Convert to degrees
# NOTE: Negative sign to match coordinate system (clockwise positive)
angle_deg = -(delta14 * 360.0 / 16384.0) if delta14 is not None else None
zero_angle_deg = (zero_ref * 360.0 / 16384.0) if zero_ref != 0 else None
return { return {
'raw_hex': i2c_bytes.hex(' '), 'i2c_raw14': raw14,
'raw_bytes': i2c_bytes, 'i2c_zero_raw14': zero_ref if zero_ref != 0 else None,
'sample_length': len(i2c_bytes) 'i2c_delta_raw14': delta14,
'i2c_angle_deg': angle_deg,
'i2c_zero_angle_deg': zero_angle_deg,
'error': None
} }
# ============================================================================= # =============================================================================
# Future Implementation Template # Test Code
# ============================================================================= # =============================================================================
# def decode_uart_packet_full(packet_bytes: bytes) -> Dict[str, Any]: if __name__ == "__main__":
# """ print("Decoder Module - Full Implementation")
# Full UART packet decoder (to be implemented). print("=" * 70)
# print()
# Packet format: EF FE [14 bytes] EE
#
# Byte layout:
# [0-1]: Start marker (EF FE)
# [2-3]: motor_current (signed 16-bit, little-endian)
# [4-5]: encoder_value (unsigned 16-bit, little-endian)
# [6-7]: relative_encoder_value (signed 16-bit, little-endian)
# [8-9]: v24_pec_diff (signed 16-bit, little-endian)
# [10]: pwm (unsigned 8-bit)
# [11-15]: Reserved
# [16]: End marker (EE)
# """
# # Verify packet length
# if len(packet_bytes) != 17:
# raise ValueError(f"Invalid packet length: {len(packet_bytes)}, expected 17")
#
# # Verify markers
# if packet_bytes[0:2] != b'\xEF\xFE':
# raise ValueError("Invalid start marker")
# if packet_bytes[16:17] != b'\xEE':
# raise ValueError("Invalid end marker")
#
# # Extract data bytes (skip markers)
# data = packet_bytes[2:16]
#
# return {
# 'motor_current': int.from_bytes(data[0:2], 'little', signed=True),
# 'encoder_value': int.from_bytes(data[2:4], 'little', signed=False),
# 'relative_encoder_value': int.from_bytes(data[4:6], 'little', signed=True),
# 'v24_pec_diff': int.from_bytes(data[6:8], 'little', signed=True),
# 'pwm': data[8]
# }
# def decode_i2c_sample_full(i2c_bytes: bytes) -> Dict[str, Any]:
# """
# Full I2C sample decoder (to be implemented).
#
# I2C format: 2 bytes (14-bit angle value)
#
# Byte layout:
# [0]: High byte (bits 13-6)
# [1]: Low byte (bits 5-0 in upper 6 bits)
# """
# if len(i2c_bytes) != 2:
# raise ValueError(f"Invalid I2C sample length: {len(i2c_bytes)}, expected 2")
#
# # Extract 14-bit value
# raw14 = ((i2c_bytes[0] << 6) | (i2c_bytes[1] >> 2)) & 0x3FFF
#
# # Convert to degrees (14-bit = 0-360°)
# angle_deg = (raw14 / 16384.0) * 360.0
#
# return {
# 'i2c_raw14': raw14,
# 'i2c_angle_deg': angle_deg
# }
# Test UART packet decoding (from PDF example)
# EF FE | 04 00 | ED FF | 00 00 | 20 08 | 00 00 | 00 | EE
# Expected: MotorCurrent=4, EncoderValue=-19, Relative=0, V24=2080, PWM=0
test_uart = bytes([
0xEF, 0xFE, # Start
0x04, 0x00, # MotorCurrent = 4 (Little Endian: LSB first)
0xED, 0xFF, # EncoderValue = -19 (Little Endian: LSB first)
0x00, 0x00, # RelativeEncoderValue = 0
0x20, 0x08, # V24PecDiff = 2080 (Little Endian: LSB first)
0x00, 0x00, # Reserved0 = 0
0x00, # PWM = 0
0xEE # End
])
if __name__ == "__main__":
# Simple test
print("Decoder Module - Pass-Through Mode")
print("=" * 60)
# Test UART packet decoding
test_uart = b'\xEF\xFE' + b'\x01' * 14 + b'\xEE'
decoded_uart = decode_uart_packet(test_uart) decoded_uart = decode_uart_packet(test_uart)
print(f"UART packet decoded: {decoded_uart}") print("UART Packet Test:")
print(f" Hex: {test_uart.hex(' ').upper()}")
# Test I2C sample decoding print(f" Decoded:")
test_i2c = b'\x3F\xFF' for key, val in decoded_uart.items():
decoded_i2c = decode_i2c_sample(test_i2c) print(f" {key:25s} = {val}")
print(f"I2C sample decoded: {decoded_i2c}")
print() print()
print("✓ Decoder ready (pass-through mode)")
print("TODO: Implement actual decoding later") # Test I2C angle decoding (from your log)
# EC 36 = 11318 raw14 = 248.69°
test_i2c = bytes([0xEC, 0x36])
decoded_i2c = decode_i2c_sample(test_i2c, zero_ref=0)
print("I2C Angle Test (no zero):")
print(f" Hex: {test_i2c.hex(' ').upper()}")
print(f" Decoded:")
for key, val in decoded_i2c.items():
if val is not None:
print(f" {key:25s} = {val}")
print()
# Test I2C with zero reference
decoded_i2c_zero = decode_i2c_sample(test_i2c, zero_ref=11318)
print("I2C Angle Test (with zero=11318):")
print(f" Hex: {test_i2c.hex(' ').upper()}")
print(f" Decoded:")
for key, val in decoded_i2c_zero.items():
if val is not None:
print(f" {key:25s} = {val}")
print()
print("✓ Decoder ready (full implementation)")
print("✓ UART: Little Endian (LSB first), signed values")
print("✓ I2C: 14-bit angle with zero reference and wrap-around")

@ -0,0 +1,519 @@
# gui/components/data/decode_raw_data.py
# =============================================================================
# Purpose
# -------
# Decode raw rows from **one session** in 'telemetry' into 'decoded_telemetry'.
#
# Scope
# -----
# - This decoder processes **only a single session_id**.
# - If session_id is not provided, it auto-selects the **most recent** session
# present in 'telemetry' (by MAX(t_ns)).
#
# What it does
# ------------
# - Reads rows from telemetry for the chosen session_id.
# - Copies session metadata **exactly** from telemetry:
# session_id, logger_session, logger_session_number.
# - Decodes UART 14B frames (EF FE ... EE), Big Endian pairs:
# [2..3]=u16 MotorCurrent
# [4..5]=s16 EncoderValue
# [6..7]=s16 RelativeEncoderValue
# [8..9]=s16 V24PecDiff
# [10..11]=s16 Reserved0
# [12]=u8 PWM
# - Computes time_ms per (session_id, logger_session_number) relative to first t_ns.
# - Parses I2C 2B angle (AS5048A) into:
# i2c_raw14, i2c_zero_raw14 (first seen per (session_id, logger_session_number)),
# i2c_delta_raw14 (signed wrap),
# i2c_angle_deg (-(delta*360/16384)),
# i2c_zero_angle_deg (zero*360/16384)
# - Upserts decoded rows into 'decoded_telemetry' (no i2c_raw stored).
#
# Entry points
# ------------
# - decode_into_decoded_telemetry(session_id: str|None = None, batch_size: int = 1000) -> dict
# - run(session_id: str|None = None, **_) -> dict (alias; ignores other filters)
# - main([session_id]) -> int (simple CLI)
# =============================================================================
from __future__ import annotations
from typing import Optional, Tuple, Dict, Any, List
# DB helpers (centralized)
from components.data.db import (
get_connection,
ensure_decoded_schema,
upsert_decoded_rows,
)
# -----------------------------
# UART framing & decode helpers
# -----------------------------
FRAME_LEN = 14
START0 = 0xEF
START1 = 0xFE
END = 0xEE
def _parse_hex_bytes(s: Optional[str]) -> Optional[bytes]:
"""Accepts 'EF FE ... EE' or 'EFFEEE...' (spaces optional). Returns None if invalid/missing."""
if not s:
return None
s2 = s.strip().replace(" ", "")
if len(s2) % 2 != 0:
return None
try:
return bytes.fromhex(s2)
except ValueError:
return None
def _be_u16(b0: int, b1: int) -> int:
return (b0 << 8) | b1
def _be_s16(b0: int, b1: int) -> int:
u = (b0 << 8) | b1
return u - 0x10000 if (u & 0x8000) else u
def _decode_uart_frame(
raw: Optional[bytes],
) -> Optional[Tuple[int, int, int, int, int, int]]:
if raw is None or len(raw) != FRAME_LEN:
return None
if not (raw[0] == START0 and raw[1] == START1 and raw[13] == END):
return None
# motor_current must be signed s16 (two's complement, big endian)
mc = _be_s16(raw[2], raw[3])
enc = _be_s16(raw[4], raw[5])
rel = _be_s16(raw[6], raw[7])
v24 = _be_s16(raw[8], raw[9])
res0 = _be_s16(raw[10], raw[11])
pwm = int(raw[12]) # u8 (0..100)
# Optional sanity guard to drop obvious misframes (tune bounds to your hw)
if not (-40000 <= mc <= 40000 and 0 <= pwm <= 100):
return None
return (mc, enc, rel, v24, res0, pwm)
# -----------------------------
# I2C decode helpers (AS5048A)
# -----------------------------
def _i2c_raw14(i2c_raw: Optional[str]) -> Optional[int]:
"""Decode 2-byte I2C payload into 14-bit angle (0..16383)."""
b = _parse_hex_bytes(i2c_raw)
if b is None or len(b) != 2:
return None
msb, lsb = b[0], b[1]
return ((msb << 6) | (lsb & 0x3F)) & 0x3FFF
def _wrap14_delta(cur: int, zero: int) -> int:
"""Signed wrap-around delta (cur - zero) mapped to [-8192..+8191] in 14-bit space."""
return ((cur - zero + 8192) % 16384) - 8192
# -----------------------------
# Session selection helpers
# -----------------------------
def _resolve_session_id() -> Optional[str]:
"""
Pick the most recent session_id present in telemetry (by MAX(t_ns)).
Returns None if telemetry is empty.
"""
with get_connection() as con:
cur = con.cursor()
cur.execute(
"""
SELECT session_id
FROM telemetry
WHERE t_ns IS NOT NULL
GROUP BY session_id
ORDER BY MAX(t_ns) DESC
LIMIT 1
"""
)
row = cur.fetchone()
return row[0] if row else None
# -----------------------------
# Internal: load rows for one session
# -----------------------------
_SELECT_FOR_SESSION = """
SELECT
id, session_id, logger_session, logger_session_number,
t_ns, uart_raw, i2c_raw
FROM telemetry
WHERE session_id = ?
ORDER BY logger_session_number, t_ns
"""
# -----------------------------
# Public: main decode entry
# -----------------------------
def decode_into_decoded_telemetry(
session_id: Optional[str] = None,
*,
batch_size: int = 1000,
) -> Dict[str, int]:
ensure_decoded_schema()
sess = session_id or _resolve_session_id()
if not sess:
return {"processed": 0, "decoded": 0, "skipped_invalid_uart": 0}
# Load rows for this session
with get_connection() as con:
cur = con.cursor()
cur.execute(_SELECT_FOR_SESSION, (sess,))
rows = cur.fetchall()
processed = decoded = skipped = 0
first_ns_map: Dict[Tuple[str, int], int] = {}
i2c_zero_map: Dict[Tuple[str, int], int] = {}
try:
import config
_CONFIG_ZERO14 = (
((int(config.SESSION_ZERO[0]) & 0xFF) << 8)
| (int(config.SESSION_ZERO[1]) & 0xFF)
) & 0x3FFF
except Exception:
_CONFIG_ZERO14 = None
# 1) PASS: decode & COLLECT per run
from collections import defaultdict
grp_rows: Dict[Tuple[str, int], List[Dict[str, Any]]] = defaultdict(list)
for id_, sess_id, log_tag, log_no, t_ns, uart_raw, i2c_raw in rows:
processed += 1
grp = (sess_id, int(log_no))
if _CONFIG_ZERO14 is not None:
i2c_zero_map.setdefault(grp, _CONFIG_ZERO14)
# baseline for time_ms
if grp not in first_ns_map and t_ns is not None:
first_ns_map[grp] = t_ns
t0 = first_ns_map.get(grp)
time_ms = (
int((t_ns - t0) / 1_000_000)
if (t0 is not None and t_ns is not None)
else None
)
# UART decode
frame = _decode_uart_frame(_parse_hex_bytes(uart_raw))
if frame is None:
skipped += 1
continue
mc, enc, rel, v24, res0, pwm = frame
# I2C decode/angles
raw14 = _i2c_raw14(i2c_raw)
zero14 = i2c_zero_map.get(grp)
if raw14 is not None and zero14 is None:
zero14 = raw14
i2c_zero_map[grp] = zero14
if raw14 is not None and zero14 is not None:
delta14 = _wrap14_delta(raw14, zero14)
angle_deg = -(delta14 * 360.0 / 16384.0)
zero_angle_deg = zero14 * 360.0 / 16384.0
else:
delta14 = None
angle_deg = None
zero_angle_deg = (zero14 * 360.0 / 16384.0) if zero14 is not None else None
grp_rows[grp].append(
{
"id": id_,
"session_id": sess_id,
"logger_session": log_tag,
"logger_session_number": int(log_no),
"t_ns": t_ns,
"time_ms": time_ms,
"motor_current": mc,
"encoder_value": enc,
"relative_encoder_value": rel,
"v24_pec_diff": v24,
"pwm": pwm,
"i2c_raw14": raw14,
"i2c_zero_raw14": zero14,
"i2c_delta_raw14": delta14,
"i2c_angle_deg": angle_deg,
"i2c_zero_angle_deg": zero_angle_deg,
"angular_velocity": None,
}
)
# 2) PASS: compute angular_velocity per group using Kalman RTS on angle vs time
for grp, items in grp_rows.items():
# Build time in seconds with full precision; keep your existing time_ms for storage
time_s_list = []
angle_deg_list = []
for row in items:
t_ns = row["t_ns"]
# use high-precision seconds internally
time_s_list.append(
(t_ns - items[0]["t_ns"]) * 1e-9
if (t_ns is not None and items[0]["t_ns"] is not None)
else None
)
angle_deg_list.append(
row["i2c_angle_deg"]
) # may be None; RTS handles segments
omega_deg_s = _kalman_velocity_over_series(
time_s_list, angle_deg_list, gap_threshold_s=0.1
)
# write back
for row, w in zip(items, omega_deg_s):
row["angular_velocity"] = w
# 3) UPSERT in batches (same API)
out_batch: List[Dict[str, Any]] = []
def _flush_batch():
nonlocal out_batch, decoded
if not out_batch:
return
decoded += upsert_decoded_rows(out_batch)
out_batch = []
for items in grp_rows.values():
for row in items:
out_batch.append(row)
if len(out_batch) >= batch_size:
_flush_batch()
_flush_batch()
return {"processed": processed, "decoded": decoded, "skipped_invalid_uart": skipped}
# -----------------------------
# Kalman RTS (constant-velocity) helpers
# -----------------------------
def _robust_var(values):
"""Robust variance via MAD; returns a small floor if data is tiny."""
xs = [v for v in values if v is not None]
if len(xs) < 5:
return 1e-6
xs_sorted = sorted(xs)
m = xs_sorted[len(xs) // 2]
abs_dev = [abs(v - m) for v in xs_sorted]
mad = sorted(abs_dev)[len(abs_dev) // 2] or 1e-9
# 1.4826 * MAD ~ sigma (Gaussian)
sigma = 1.4826 * mad
v = sigma * sigma
return v if v > 1e-6 else 1e-6
def _kalman_rts_velocity_segment(t_s, y_deg, r_var=None, q_pow=None):
"""
One contiguous segment (strictly increasing times, no big gaps).
State x=[theta, omega], units: theta=deg, omega=deg/s.
F_k = [[1, dt],[0,1]]
Q_k = q * [[dt^3/3, dt^2/2],[dt^2/2, dt]]
H = [1, 0], R = r.
Returns (theta_s, omega_s) lists (len = len(y_deg))
"""
n = len(y_deg)
if n == 0:
return [], []
# Estimate R (measurement variance) if not provided
if r_var is None:
r_var = _robust_var(y_deg)
# Estimate process noise power q if not provided (based on rough accel scale)
if q_pow is None:
# crude accel estimate from finite differences
dtheta = [(y_deg[i + 1] - y_deg[i]) for i in range(n - 1)]
dt = [(t_s[i + 1] - t_s[i]) for i in range(n - 1)]
dv = []
for i in range(len(dtheta) - 1):
dt1 = dt[i] if dt[i] > 1e-6 else 1e-6
dt2 = dt[i + 1] if dt[i + 1] > 1e-6 else 1e-6
v1 = dtheta[i] / dt1
v2 = dtheta[i + 1] / dt2
dv.append(
(v2 - v1) / max((dt1 + dt2) / 2.0, 1e-6)
) # deg/s^2 change per second
# variance-like scale of acceleration
acc_var = _robust_var(dv) if dv else 1.0
med_dt = sorted(dt)[len(dt) // 2] if dt else 0.01
q_pow = max(acc_var * med_dt * 1e-3, 1e-6) # conservative floor
# Allocate
x_f = [[0.0, 0.0] for _ in range(n)]
P_f = [[[0.0, 0.0], [0.0, 0.0]] for _ in range(n)]
x_p = [[0.0, 0.0] for _ in range(n)]
P_p = [[[0.0, 0.0], [0.0, 0.0]] for _ in range(n)]
# Init
x = [y_deg[0], 0.0]
P = [[1.0, 0.0], [0.0, 1000.0]]
H = [1.0, 0.0]
R = r_var
def mat_add(A, B):
return [
[A[0][0] + B[0][0], A[0][1] + B[0][1]],
[A[1][0] + B[1][0], A[1][1] + B[1][1]],
]
def mat_mul(A, B):
return [
[
A[0][0] * B[0][0] + A[0][1] * B[1][0],
A[0][0] * B[0][1] + A[0][1] * B[1][1],
],
[
A[1][0] * B[0][0] + A[1][1] * B[1][0],
A[1][0] * B[0][1] + A[1][1] * B[1][1],
],
]
def mat_transpose(A):
return [[A[0][0], A[1][0]], [A[0][1], A[1][1]]]
def mat_vec(A, v):
return [A[0][0] * v[0] + A[0][1] * v[1], A[1][0] * v[0] + A[1][1] * v[1]]
def mat_inv_2x2(A):
det = A[0][0] * A[1][1] - A[0][1] * A[1][0]
if abs(det) < 1e-12:
det = 1e-12
inv = [[A[1][1] / det, -A[0][1] / det], [-A[1][0] / det, A[0][0] / det]]
return inv
# FILTER
for k in range(n):
if k == 0:
x_p[k] = [x[0], x[1]]
P_p[k] = [[P[0][0], P[0][1]], [P[1][0], P[1][1]]]
else:
dt = t_s[k] - t_s[k - 1]
F = [[1.0, dt], [0.0, 1.0]]
q11 = (dt**3) / 3.0
q12 = (dt**2) / 2.0
q22 = dt
Q = [[q_pow * q11, q_pow * q12], [q_pow * q12, q_pow * q22]]
# Predict
x = [x[0] + dt * x[1], x[1]]
FP = mat_mul(F, P)
FT = mat_transpose(F)
P = mat_add(mat_mul(FP, FT), Q)
x_p[k] = [x[0], x[1]]
P_p[k] = [[P[0][0], P[0][1]], [P[1][0], P[1][1]]]
# Update with measurement y_deg[k]
z = y_deg[k]
# S = H*P*H^T + R (scalar)
S = P[0][0] + R
K0 = P[0][0] / S # first row, col 0 of K
K1 = P[1][0] / S # second row, col 0 of K
innov = z - x[0]
x = [x[0] + K0 * innov, x[1] + K1 * innov]
# P = (I - K*H)P
# (I - K H) = [[1-K0*1, -K0*0],[-K1*1, 1- K1*0]] = [[1-K0, 0],[-K1, 1]]
IKH = [[1.0 - K0, 0.0], [-K1, 1.0]]
P = mat_mul(IKH, P)
x_f[k] = [x[0], x[1]]
P_f[k] = [[P[0][0], P[0][1]], [P[1][0], P[1][1]]]
# RTS SMOOTHER
x_s = [[x_f[i][0], x_f[i][1]] for i in range(n)]
P_s = [
[[P_f[i][0][0], P_f[i][0][1]], [P_f[i][1][0], P_f[i][1][1]]] for i in range(n)
]
for k in range(n - 2, -1, -1):
dt = t_s[k + 1] - t_s[k]
F = [[1.0, dt], [0.0, 1.0]]
FT = mat_transpose(F)
Pp_inv = mat_inv_2x2(P_p[k + 1])
# Ck = P_f[k] * F^T * P_p[k+1]^{-1}
C_left = mat_mul(P_f[k], FT)
Ck = mat_mul(C_left, Pp_inv)
# x_s[k] = x_f[k] + Ck * (x_s[k+1] - x_p[k+1])
dx = [x_s[k + 1][0] - x_p[k + 1][0], x_s[k + 1][1] - x_p[k + 1][1]]
corr = mat_vec(Ck, dx)
x_s[k] = [x_f[k][0] + corr[0], x_f[k][1] + corr[1]]
# P_s[k] not needed for velocity output, so we skip computing it for speed
theta_s = [x_s[i][0] for i in range(n)]
omega_s = [x_s[i][1] for i in range(n)]
return theta_s, omega_s
def _kalman_velocity_over_series(
time_s_list, angle_deg_list, *, gap_threshold_s=0.1
) -> list:
"""
Splits the series at big gaps or invalid points and runs the RTS smoother per segment.
Returns a list of angular velocities (deg/s) aligned with inputs (None where undefined).
"""
n = len(angle_deg_list)
out = [None] * n
# Build valid indices where both time and angle exist
valid = [
(time_s_list[i] is not None) and (angle_deg_list[i] is not None)
for i in range(n)
]
i = 0
while i < n:
# skip invalids
while i < n and not valid[i]:
i += 1
if i >= n:
break
j = i + 1
# extend segment until gap or invalid
while j < n and valid[j]:
if (time_s_list[j] - time_s_list[j - 1]) > gap_threshold_s:
break
j += 1
# segment [i, j)
t_seg = [time_s_list[k] for k in range(i, j)]
y_seg = [angle_deg_list[k] for k in range(i, j)]
theta_s, omega_s = _kalman_rts_velocity_segment(t_seg, y_seg)
for k, w in enumerate(omega_s):
out[i + k] = w
i = j
return out
# Friendly alias for UART_logic.stop_logger()
def run(session_id: Optional[str] = None, **_ignored) -> Dict[str, int]:
"""
Alias to decode_into_decoded_telemetry with **only** session_id honored.
Other kwargs are ignored intentionally.
"""
return decode_into_decoded_telemetry(session_id=session_id)
def main() -> int:
"""CLI: python -m gui.components.data.decode_raw_data [session_id]"""
import sys
sess = sys.argv[1] if len(sys.argv) > 1 else None
stats = decode_into_decoded_telemetry(session_id=sess)
print(stats)
return 0
if __name__ == "__main__":
main()

@ -0,0 +1,5 @@
"""
vzug-e-hinge Graph Module
=========================
Data visualization and graph generation.
"""

@ -0,0 +1,3 @@
"""
Graph Kit - Core plotting and widgets
"""

@ -93,7 +93,7 @@ def plot_overlay(
continue continue
# Updated label format: "session_name run_no (name)" # Updated label format: "session_name run_no (name)"
label = f"{data.session_id} {data.run_no} ({data.run_name})" label = f"{data.session_id} {data.run_no} ({data.session_name})"
ax.plot(x_data, y_data, label=label, alpha=0.8, ax.plot(x_data, y_data, label=label, alpha=0.8,
linestyle=config.linestyle, marker=config.marker, linestyle=config.linestyle, marker=config.marker,
markersize=config.markersize) markersize=config.markersize)
@ -164,7 +164,7 @@ def plot_subplots(
if x_data is None or y_data is None: if x_data is None or y_data is None:
ax.text(0.5, 0.5, 'No data', ha='center', va='center') ax.text(0.5, 0.5, 'No data', ha='center', va='center')
ax.set_title(f"{data.session_id} - Run {data.run_no} ({data.run_name})") ax.set_title(f"{data.session_id} - Run {data.run_no} ({data.session_name})")
continue continue
ax.plot(x_data, y_data, alpha=0.8, ax.plot(x_data, y_data, alpha=0.8,
@ -173,7 +173,7 @@ def plot_subplots(
ax.set_xlabel(xlabel) ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel) ax.set_ylabel(ylabel)
# Updated title format # Updated title format
ax.set_title(f"{data.session_id} {data.run_no} ({data.run_name})") ax.set_title(f"{data.session_id} {data.run_no} ({data.session_name})")
if config.grid: if config.grid:
ax.grid(True, alpha=0.3) ax.grid(True, alpha=0.3)
@ -230,7 +230,7 @@ def plot_comparison(
# Plot reference as baseline (zero) # Plot reference as baseline (zero)
ax.axhline(y=0, color='black', linestyle='--', ax.axhline(y=0, color='black', linestyle='--',
# Updated label format # Updated label format
label=f'Reference: {reference.session_id} {reference.run_no} ({reference.run_name})') label=f'Reference: {reference.session_id} {reference.run_no} ({reference.session_name})')
# Set color cycle # Set color cycle
ax.set_prop_cycle(color=plt.cm.tab10.colors) ax.set_prop_cycle(color=plt.cm.tab10.colors)
@ -256,7 +256,7 @@ def plot_comparison(
deviation = y_interp - ref_y deviation = y_interp - ref_y
# Updated label format # Updated label format
label = f"{data.session_id} {data.run_no} ({data.run_name})" label = f"{data.session_id} {data.run_no} ({data.session_name})"
ax.plot(ref_x, deviation, label=label, alpha=0.8, ax.plot(ref_x, deviation, label=label, alpha=0.8,
linestyle=config.linestyle, marker=config.marker, linestyle=config.linestyle, marker=config.marker,
markersize=config.markersize) markersize=config.markersize)
@ -331,7 +331,7 @@ def plot_multi_series(
continue continue
# Updated label format # Updated label format
label = f"{data.session_id} {data.run_no} ({data.run_name})" label = f"{data.session_id} {data.run_no} ({data.session_name})"
ax.plot(x_data, y_data, label=label, alpha=0.8, ax.plot(x_data, y_data, label=label, alpha=0.8,
linestyle=config.linestyle, marker=config.marker, linestyle=config.linestyle, marker=config.marker,
markersize=config.markersize) markersize=config.markersize)
@ -395,7 +395,7 @@ def plot_xy_scatter(
continue continue
# Updated label format # Updated label format
label = f"{data.session_id} {data.run_no} ({data.run_name})" label = f"{data.session_id} {data.run_no} ({data.session_name})"
ax.plot(x_data, y_data, label=label, alpha=0.8, ax.plot(x_data, y_data, label=label, alpha=0.8,
linestyle=config.linestyle, marker=config.marker or 'o', linestyle=config.linestyle, marker=config.marker or 'o',
markersize=config.markersize if config.marker else 2, markersize=config.markersize if config.marker else 2,
@ -467,7 +467,7 @@ def export_csv(
writer = csv.writer(f) writer = csv.writer(f)
# Header: metadata + X column + selected Y columns # Header: metadata + X column + selected Y columns
header = ['session_id', 'run_name', 'run_no', x_column] + y_columns header = ['session_id', 'session_name', 'run_no', x_column] + y_columns
writer.writerow(header) writer.writerow(header)
# Data rows # Data rows
@ -485,7 +485,7 @@ def export_csv(
for i in range(length): for i in range(length):
row = [ row = [
data.session_id, data.session_id,
data.run_name, data.session_name,
data.run_no, data.run_no,
x_data[i] x_data[i]
] ]

@ -25,6 +25,7 @@ from PyQt6.QtWidgets import (
) )
from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QFont from PyQt6.QtGui import QFont
from typing import Optional
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
@ -42,7 +43,7 @@ from graph_table_query import (
) )
# Import from graph_core (pure plotting) # Import from graph_core (pure plotting)
from graph_core import ( from .graph_core import (
PlotConfig, PlotConfig,
plot_overlay, plot_overlay,
plot_subplots, plot_subplots,
@ -449,7 +450,7 @@ class GraphWidget(QWidget):
for run in runs: for run in runs:
run_item = QTreeWidgetItem(session_item) run_item = QTreeWidgetItem(session_item)
run_item.setText(0, f"Run {run.run_number} ({run.run_name})") run_item.setText(0, f"Run {run.run_number} ({run.session_name})")
run_item.setText(1, f"{run.sample_count} samples") run_item.setText(1, f"{run.sample_count} samples")
run_item.setText(2, f"{run.duration_ms:.1f}ms") run_item.setText(2, f"{run.duration_ms:.1f}ms")
run_item.setFlags(run_item.flags() | Qt.ItemFlag.ItemIsUserCheckable) run_item.setFlags(run_item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
@ -735,7 +736,7 @@ class GraphWidget(QWidget):
continue continue
# Label format: "session_name run_no (name) - series" # Label format: "session_name run_no (name) - series"
label = f"{data.session_id} {data.run_no} ({data.run_name}) - {ylabel}" label = f"{data.session_id} {data.run_no} ({data.session_name}) - {ylabel}"
ax.plot(x_data, y_data, label=label, alpha=0.8, ax.plot(x_data, y_data, label=label, alpha=0.8,
linestyle=linestyle, marker=marker, markersize=markersize) linestyle=linestyle, marker=marker, markersize=markersize)
@ -775,22 +776,49 @@ class GraphWidget(QWidget):
def _update_canvas(self, figure): def _update_canvas(self, figure):
"""Update canvas with new figure - recreates canvas to fix matplotlib toolbar.""" """Update canvas with new figure - recreates canvas to fix matplotlib toolbar."""
# Remove old canvas and toolbar from PyQt6.QtWidgets import QApplication
if hasattr(self, 'canvas'): import matplotlib.pyplot as plt
self.plot_layout.removeWidget(self.canvas)
self.plot_layout.removeWidget(self.toolbar) # Close old matplotlib figure to free memory
self.canvas.deleteLater() if hasattr(self, 'figure') and self.figure is not None:
self.toolbar.deleteLater() try:
plt.close(self.figure)
except Exception:
pass
# Remove old canvas and toolbar safely
if hasattr(self, 'canvas') and self.canvas is not None:
try:
self.plot_layout.removeWidget(self.canvas)
self.plot_layout.removeWidget(self.toolbar)
self.canvas.close()
self.toolbar.close()
self.canvas.deleteLater()
self.toolbar.deleteLater()
self.canvas = None
self.toolbar = None
# Process events to ensure cleanup
QApplication.processEvents()
except RuntimeError:
# Widget already deleted
pass
# Store figure reference
self.figure = figure
# Create fresh canvas and toolbar # Create fresh canvas and toolbar
self.canvas = FigureCanvas(figure) self.canvas = FigureCanvas(figure)
self.toolbar = NavigationToolbar(self.canvas, self) self.toolbar = NavigationToolbar(self.canvas, self)
# Add to layout # Add to layout
self.plot_layout.insertWidget(1, self.canvas) # After controls self.plot_layout.insertWidget(1, self.canvas) # After controls
self.plot_layout.insertWidget(2, self.toolbar) # After canvas self.plot_layout.insertWidget(2, self.toolbar) # After canvas
self.figure = figure # Draw the canvas
try:
self.canvas.draw()
except Exception as e:
print(f"Warning: Canvas draw failed: {e}")
def _show_plot_window(self, figure): def _show_plot_window(self, figure):
"""Show plot in separate window.""" """Show plot in separate window."""

@ -45,7 +45,7 @@ class SessionInfo:
class RunInfo: class RunInfo:
"""Run metadata.""" """Run metadata."""
session_id: str session_id: str
run_name: str session_name: str
run_number: int run_number: int
sample_count: int sample_count: int
start_time_ns: int start_time_ns: int
@ -57,7 +57,7 @@ class RunInfo:
class TelemetryData: class TelemetryData:
"""Decoded telemetry data (source-agnostic).""" """Decoded telemetry data (source-agnostic)."""
session_id: str session_id: str
run_name: str session_name: str
run_no: int run_no: int
# Time axes # Time axes
@ -199,7 +199,7 @@ class SQLiteAdapter(DataAdapter):
s.description, s.description,
COUNT(DISTINCT t.run_no) as run_count COUNT(DISTINCT t.run_no) as run_count
FROM sessions s FROM sessions s
LEFT JOIN decoded_telemetry t ON s.session_id = t.session_id LEFT JOIN telemetry_decoded t ON s.session_id = t.session_id
GROUP BY s.session_id GROUP BY s.session_id
ORDER BY s.created_at DESC ORDER BY s.created_at DESC
""") """)
@ -228,27 +228,27 @@ class SQLiteAdapter(DataAdapter):
cursor = self._conn.cursor() cursor = self._conn.cursor()
cursor.execute(""" cursor.execute("""
SELECT SELECT
session_id, session_id,
run_name, session_name,
run_no, run_no,
COUNT(*) as sample_count, COUNT(*) as sample_count,
MIN(t_ns) as start_time_ns, MIN(t_ns) as start_time_ns,
MAX(t_ns) as end_time_ns MAX(t_ns) as end_time_ns
FROM decoded_telemetry FROM telemetry_decoded
WHERE session_id = ? WHERE session_id = ?
GROUP BY session_id, run_no, run_name GROUP BY session_id, run_no, session_name
ORDER BY run_no ORDER BY run_no
""", (session_id,)) """, (session_id,))
runs = [] runs = []
for row in cursor.fetchall(): for row in cursor.fetchall():
duration_ns = row['end_time_ns'] - row['start_time_ns'] duration_ns = row['end_time_ns'] - row['start_time_ns']
duration_ms = duration_ns / 1_000_000.0 duration_ms = duration_ns / 1_000_000.0
runs.append(RunInfo( runs.append(RunInfo(
session_id=row['session_id'], session_id=row['session_id'],
run_name=row['run_name'], session_name=row['session_name'],
run_number=row['run_no'], run_number=row['run_no'],
sample_count=row['sample_count'], sample_count=row['sample_count'],
start_time_ns=row['start_time_ns'], start_time_ns=row['start_time_ns'],
@ -271,8 +271,8 @@ class SQLiteAdapter(DataAdapter):
cursor = self._conn.cursor() cursor = self._conn.cursor()
cursor.execute(""" cursor.execute("""
SELECT SELECT
run_name, session_name,
t_ns, t_ns,
time_ms, time_ms,
motor_current, motor_current,
@ -286,23 +286,23 @@ class SQLiteAdapter(DataAdapter):
i2c_angle_deg, i2c_angle_deg,
i2c_zero_angle_deg, i2c_zero_angle_deg,
angular_velocity angular_velocity
FROM decoded_telemetry FROM telemetry_decoded
WHERE session_id = ? AND run_no = ? WHERE session_id = ? AND run_no = ?
ORDER BY t_ns ORDER BY t_ns
""", (session_id, run_no)) """, (session_id, run_no))
rows = cursor.fetchall() rows = cursor.fetchall()
if not rows: if not rows:
return None return None
# Get run_name from first row # Get session_name from first row
run_name = rows[0]['run_name'] session_name = rows[0]['session_name']
# Extract columns # Extract columns
data = TelemetryData( data = TelemetryData(
session_id=session_id, session_id=session_id,
run_name=run_name, session_name=session_name,
run_no=run_no, run_no=run_no,
t_ns=self._extract_column(rows, 't_ns', dtype=np.int64), t_ns=self._extract_column(rows, 't_ns', dtype=np.int64),
time_ms=self._extract_column(rows, 'time_ms', dtype=np.int64), time_ms=self._extract_column(rows, 'time_ms', dtype=np.int64),
@ -352,7 +352,7 @@ class CSVAdapter(DataAdapter):
CSV file adapter. CSV file adapter.
Expected CSV format: Expected CSV format:
session_id,run_name,run_no,t_ns,time_ms,motor_current,encoder_value,... session_id,session_name,run_no,t_ns,time_ms,motor_current,encoder_value,...
Single CSV file containing all sessions and runs. Single CSV file containing all sessions and runs.
""" """
@ -374,7 +374,7 @@ class CSVAdapter(DataAdapter):
self._df = pd.read_csv(self.csv_path) self._df = pd.read_csv(self.csv_path)
# Validate required columns # Validate required columns
required = ['session_id', 'run_name', 'run_no'] required = ['session_id', 'session_name', 'run_no']
missing = [col for col in required if col not in self._df.columns] missing = [col for col in required if col not in self._df.columns]
if missing: if missing:
@ -438,8 +438,8 @@ class CSVAdapter(DataAdapter):
for run_no, group in run_groups: for run_no, group in run_groups:
sample_count = len(group) sample_count = len(group)
# Get run_name # Get session_name
run_name = str(group['run_name'].iloc[0]) if 'run_name' in group.columns else '' session_name = str(group['session_name'].iloc[0]) if 'session_name' in group.columns else ''
# Get time info # Get time info
if 't_ns' in group.columns: if 't_ns' in group.columns:
@ -453,7 +453,7 @@ class CSVAdapter(DataAdapter):
runs.append(RunInfo( runs.append(RunInfo(
session_id=session_id, session_id=session_id,
run_name=run_name, session_name=session_name,
run_number=int(run_no), run_number=int(run_no),
sample_count=sample_count, sample_count=sample_count,
start_time_ns=start_time_ns, start_time_ns=start_time_ns,
@ -489,13 +489,13 @@ class CSVAdapter(DataAdapter):
if 't_ns' in run_df.columns: if 't_ns' in run_df.columns:
run_df = run_df.sort_values('t_ns') run_df = run_df.sort_values('t_ns')
# Get run_name # Get session_name
run_name = str(run_df['run_name'].iloc[0]) if 'run_name' in run_df.columns else '' session_name = str(run_df['session_name'].iloc[0]) if 'session_name' in run_df.columns else ''
# Extract columns # Extract columns
data = TelemetryData( data = TelemetryData(
session_id=session_id, session_id=session_id,
run_name=run_name, session_name=session_name,
run_no=run_no, run_no=run_no,
t_ns=self._extract_column_csv(run_df, 't_ns', dtype=np.int64), t_ns=self._extract_column_csv(run_df, 't_ns', dtype=np.int64),
time_ms=self._extract_column_csv(run_df, 'time_ms', dtype=np.int64), time_ms=self._extract_column_csv(run_df, 'time_ms', dtype=np.int64),
@ -674,7 +674,7 @@ def align_data_to_reference(
# Create aligned data object # Create aligned data object
aligned = TelemetryData( aligned = TelemetryData(
session_id=data.session_id, session_id=data.session_id,
run_name=data.run_name, session_name=data.session_name,
run_no=data.run_no run_no=data.run_no
) )

@ -34,16 +34,13 @@ from PyQt6.QtGui import QAction
# Import database # Import database
from database.init_database import DatabaseManager from database.init_database import DatabaseManager
# Import widgets (will be created next) # Import widgets
# from session_widget import SessionWidget from session_widget import SessionWidget
# from uart_widget import UARTWidget from configure_interface_widget import ConfigureInterfaceWidget
# from uart_logger_widget import UARTLoggerWidget from configure_session_widget import ConfigureSessionWidget
# from i2c_widget import I2CWidget from uart.uart_integrated_widget import UARTControlWidget
# from i2c_logger_widget import I2CLoggerWidget from i2c.i2c_integrated_widget import I2CControlWidget
# from graph_widget import GraphWidget from graph.graph_kit.graph_core_widget import GraphWidget
# Import session manager (will be created next)
# from session_manager import SessionManager
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
@ -242,8 +239,14 @@ class MainWindow(QMainWindow):
self.i2c_widget = None self.i2c_widget = None
# Tab 6: Graph # Tab 6: Graph
self.tab_graph = self._create_placeholder_tab("Graph Visualization") try:
self.tabs.addTab(self.tab_graph, "Graph") self.graph_widget = GraphWidget(db_path=self.db_path)
self.tabs.addTab(self.graph_widget, "Graph")
except Exception as e:
print(f"[WARN] Failed to load Graph widget: {e}")
self.tab_graph = self._create_placeholder_tab("Graph Visualization\n(Error loading)")
self.tabs.addTab(self.tab_graph, "Graph")
self.graph_widget = None
def _create_placeholder_tab(self, title: str) -> QWidget: def _create_placeholder_tab(self, title: str) -> QWidget:
"""Create placeholder tab widget.""" """Create placeholder tab widget."""

@ -146,10 +146,6 @@ class RunExecutor:
""" """
callback_count[0] += 1 callback_count[0] += 1
# Debug first few callbacks
if callback_count[0] <= 3:
print(f"[DEBUG] I2C callback triggered #{callback_count[0]} at {timestamp_ns}")
if i2c_port: if i2c_port:
# Read I2C angle immediately # Read I2C angle immediately
status, i2c_bytes = i2c_read_block( status, i2c_bytes = i2c_read_block(
@ -566,7 +562,7 @@ class RunExecutor:
""" """
# Decode packets # Decode packets
decoded_uart = decode_uart_packet(packet_info.data) decoded_uart = decode_uart_packet(packet_info.data)
decoded_i2c = decode_i2c_sample(i2c_bytes) if i2c_bytes else None decoded_i2c = decode_i2c_sample(i2c_bytes, self.i2c_zero_reference) if i2c_bytes else None
# Calculate relative time from run start # Calculate relative time from run start
time_ms = (packet_info.start_timestamp - run_start_ns) / 1_000_000.0 time_ms = (packet_info.start_timestamp - run_start_ns) / 1_000_000.0
@ -590,14 +586,14 @@ class RunExecutor:
self.i2c_zero_reference # Zero reference (0 if not zeroed) self.i2c_zero_reference # Zero reference (0 if not zeroed)
)) ))
# Save to telemetry_decoded (main data) # Save to telemetry_decoded (main data - all decoded fields)
# For now, just save timestamps (decoder is pass-through)
# TODO: Update when decoder is fully implemented
cursor.execute(""" cursor.execute("""
INSERT INTO telemetry_decoded ( INSERT INTO telemetry_decoded (
session_id, session_name, run_no, run_command_id, session_id, session_name, run_no, run_command_id,
t_ns, time_ms, i2c_zero_ref t_ns, time_ms,
) VALUES (?, ?, ?, ?, ?, ?, ?) motor_current, encoder_value, relative_encoder_value, v24_pec_diff, pwm,
i2c_raw14, i2c_zero_raw14, i2c_delta_raw14, i2c_angle_deg, i2c_zero_angle_deg, i2c_zero_ref
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""", ( """, (
session_id, session_id,
session_name, session_name,
@ -605,13 +601,21 @@ class RunExecutor:
run_command_id, run_command_id,
packet_info.start_timestamp, packet_info.start_timestamp,
time_ms, time_ms,
# UART decoded fields
decoded_uart.get('motor_current'),
decoded_uart.get('encoder_value'),
decoded_uart.get('relative_encoder_value'),
decoded_uart.get('v24_pec_diff'),
decoded_uart.get('pwm'),
# I2C decoded fields
decoded_i2c.get('i2c_raw14') if decoded_i2c else None,
decoded_i2c.get('i2c_zero_raw14') if decoded_i2c else None,
decoded_i2c.get('i2c_delta_raw14') if decoded_i2c else None,
decoded_i2c.get('i2c_angle_deg') if decoded_i2c else None,
decoded_i2c.get('i2c_zero_angle_deg') if decoded_i2c else None,
self.i2c_zero_reference # Zero reference (0 if not zeroed) self.i2c_zero_reference # Zero reference (0 if not zeroed)
)) ))
# TODO: When decoder is fully implemented, also save:
# UART: motor_current, encoder_value, relative_encoder_value, v24_pec_diff, pwm
# I2C: i2c_raw14, i2c_angle_deg, i2c_zero_raw14, etc.
# ============================================================================= # =============================================================================
# Convenience function for external use # Convenience function for external use

Loading…
Cancel
Save