|
|
# -----------------------
|
|
|
# File: components/i2c/i2c_logic.py
|
|
|
#
|
|
|
# Purpose
|
|
|
# -------
|
|
|
# Unified I²C logic that supports:
|
|
|
# 1) One-shot command send under an exclusive "capture window"
|
|
|
# 2) A continuous background logger (thread) that politely backs off during capture
|
|
|
#
|
|
|
# Key Ideas
|
|
|
# ---------
|
|
|
# - We reuse the global coordinator (components.scheduler.coordinator) to mark an
|
|
|
# exclusive "capture window" while a one-shot I²C command is in flight.
|
|
|
# During this window, other sends are rejected, and the logger throttles
|
|
|
# output to keep the console readable and the UI snappy — same as UART.
|
|
|
# - The logger runs in its own daemon thread and performs periodic READs from a
|
|
|
# chosen register/length at a configurable interval.
|
|
|
# - All SMBus interactions are serialized by a single RLock so disconnect/close
|
|
|
# cannot race against in-flight reads/writes (prevents SIGSEGV).
|
|
|
#
|
|
|
# Tuning / Knobs
|
|
|
# --------------
|
|
|
# - self._logger_sleep_s: how often the logger loop yields when idle/throttled
|
|
|
# (default ~50 Hz). Increase for less CPU, decrease for more responsiveness.
|
|
|
# - _throttle_ns (inside _logger_loop): during capture, the logger emits at most
|
|
|
# once every 50 ms. Increase to further reduce noise.
|
|
|
# - The default capture window in send_command() is ~300 ms; adjust if needed.
|
|
|
#
|
|
|
# Logging Format
|
|
|
# --------------
|
|
|
# - TX (read): "➡️ | addr 0xAA | reg 0xRR | action READ | len N"
|
|
|
# - RX (n bytes): "⬅️ | addr 0xAA | reg 0xRR | data [0xV0, 0xV1, ...]"
|
|
|
# - TX (write1): "➡️ | addr 0xAA | reg 0xRR | action WRITE | value 0xVV"
|
|
|
# - TX (writeN): "➡️ | addr 0xAA | reg 0xRR | action WRITE | bytes [0x.., ...]"
|
|
|
# - OK (write): "✅ | addr 0xAA | reg 0xRR | status OK"
|
|
|
# - Errors: "❌ | addr 0xAA | reg 0xRR | error <text>"
|
|
|
#
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
import os
|
|
|
import time
|
|
|
import threading
|
|
|
from enum import Enum, auto
|
|
|
from typing import List, Optional, Union
|
|
|
import statistics
|
|
|
import smbus2
|
|
|
import config.config as config
|
|
|
|
|
|
from components.console.console_registry import log_main_console
|
|
|
from components.data.db import get_i2c_commands
|
|
|
|
|
|
# Shared coordinator + timebase (UART-style exclusivity/throttling)
|
|
|
from components.scheduler.coordinator import (
|
|
|
claim_i2c_capture,
|
|
|
release_i2c_capture,
|
|
|
i2c_capture_active,
|
|
|
)
|
|
|
from components.scheduler.timebase import now_ns
|
|
|
|
|
|
|
|
|
# ---------------------------
|
|
|
# Helpers (parsing and formatting)
|
|
|
# ---------------------------
|
|
|
|
|
|
|
|
|
def _coerce_int(val: Union[str, int], base: int = 10) -> int:
|
|
|
"""Turn '1', '0x40', '64' or 1 into int. Raises ValueError on failure."""
|
|
|
if isinstance(val, int):
|
|
|
return val
|
|
|
s = str(val).strip().lower()
|
|
|
if s.startswith("0x"):
|
|
|
return int(s, 16)
|
|
|
return int(s, base)
|
|
|
|
|
|
|
|
|
def _parse_hex_bytes_list(s: str) -> List[int]:
|
|
|
"""
|
|
|
Parse strings like '0x01,0x02,0x03' or '01 02 03' or '01, 02, 03' into a list of ints.
|
|
|
"""
|
|
|
if not s:
|
|
|
return []
|
|
|
raw = [tok.strip() for tok in s.replace(",", " ").split()]
|
|
|
out = []
|
|
|
for t in raw:
|
|
|
if not t:
|
|
|
continue
|
|
|
# accept '0x..' or plain hex/dec
|
|
|
out.append(int(t, 0))
|
|
|
return out
|
|
|
|
|
|
|
|
|
# ---------------------------
|
|
|
# State Machine
|
|
|
# ---------------------------
|
|
|
|
|
|
|
|
|
class I2CState(Enum):
|
|
|
DISCONNECTED = auto()
|
|
|
CONNECTED_IDLE = auto()
|
|
|
LOGGING = auto()
|
|
|
CAPTURE_ACTIVE = auto()
|
|
|
|
|
|
|
|
|
# ---------------------------
|
|
|
# Main logic
|
|
|
# ---------------------------
|
|
|
|
|
|
|
|
|
class I2CLogic:
|
|
|
"""
|
|
|
Single class for:
|
|
|
- Connecting / disconnecting an I²C bus
|
|
|
- Sending one-shot register read/write commands under an exclusive capture window
|
|
|
- Running a continuous logger on the same bus (polls reg/length at an interval)
|
|
|
|
|
|
Threading model
|
|
|
---------------
|
|
|
- The logger runs in a background daemon thread started by start_logger().
|
|
|
- All SMBus calls (read/write/close) are serialized by self._bus_lock (RLock).
|
|
|
- The command send path (send_command) runs in the UI/main thread, but it
|
|
|
briefly "claims" exclusivity via the shared coordinator so:
|
|
|
* Other sends are rejected during capture,
|
|
|
* The logger throttles its output during the capture window.
|
|
|
"""
|
|
|
|
|
|
def __init__(self):
|
|
|
# Connection
|
|
|
self.bus: Optional[smbus2.SMBus] = None
|
|
|
self.bus_id: int = 1 # Raspberry Pi default /dev/i2c-1
|
|
|
|
|
|
# Threading & safety
|
|
|
self._bus_lock = threading.RLock() # serialize ALL SMBus I/O
|
|
|
self._logger_thread: Optional[threading.Thread] = None
|
|
|
self._logger_running: bool = False
|
|
|
self._logger_sleep_s: float = 0.05 # ~20 Hz idle yield
|
|
|
self._throttle_ns: int = 50_000_000 # 50 ms between emissions during capture
|
|
|
|
|
|
# Logger configuration
|
|
|
self._log_addr: Optional[int] = None
|
|
|
self._log_reg: int = 0
|
|
|
self._log_len: int = 1
|
|
|
self._log_interval_s: float = 0.20 # default 200 ms between polls
|
|
|
self._shutting_down = False # prevent new work during disconnects
|
|
|
self._2_bytes_read_counter = (
|
|
|
0 # to cinfurm how many times we were in the read 2 bytes function
|
|
|
)
|
|
|
# State
|
|
|
self._state = I2CState.DISCONNECTED
|
|
|
|
|
|
# Logger injection (UI provides console logger)
|
|
|
self.log = lambda level, msg: None # default: drop logs until UI injects one
|
|
|
|
|
|
# Inject the console logger (must be a callable: (type, message) -> None)
|
|
|
# components/i2c/i2c_logic.py
|
|
|
def set_logger(self, log_func):
|
|
|
if callable(log_func):
|
|
|
self.log = log_func
|
|
|
|
|
|
# ----- convenience formatters -----
|
|
|
def _fmt_hex(self, v: int, width: int = 2) -> str:
|
|
|
return f"0x{v:0{width}X}"
|
|
|
|
|
|
def _fmt_bytes(self, data) -> str:
|
|
|
if data is None:
|
|
|
return "[]"
|
|
|
return "[" + ", ".join(f"0x{b:02X}" for b in data) + "]"
|
|
|
|
|
|
def _to_int_any(self, v, default=None):
|
|
|
try:
|
|
|
return int(str(v).strip(), 0) # accepts "0xFE" or "254"
|
|
|
except Exception:
|
|
|
return default
|
|
|
|
|
|
def _parse_hex_list(self, s: str) -> List[int]:
|
|
|
return _parse_hex_bytes_list(s)
|
|
|
|
|
|
# ---------------------------
|
|
|
# State & Channels
|
|
|
# ---------------------------
|
|
|
|
|
|
def state(self) -> I2CState:
|
|
|
return self._state
|
|
|
|
|
|
def get_channels(self) -> List[str]:
|
|
|
"""List available /dev/i2c-* buses as strings of bus ids."""
|
|
|
i2c_devices = []
|
|
|
try:
|
|
|
for entry in os.listdir("/dev"):
|
|
|
if entry.startswith("i2c-"):
|
|
|
try:
|
|
|
bus_id = int(entry.split("-")[1])
|
|
|
i2c_devices.append(str(bus_id))
|
|
|
except ValueError:
|
|
|
continue
|
|
|
except Exception as e:
|
|
|
self.log("warn", f"Could not list /dev for i2c buses: {e}")
|
|
|
return sorted(i2c_devices)
|
|
|
|
|
|
def scan_bus(self) -> List[str]:
|
|
|
"""
|
|
|
Scan the currently opened bus for devices.
|
|
|
Returns like ['0x40', '0x41'].
|
|
|
"""
|
|
|
found: List[str] = []
|
|
|
with self._bus_lock:
|
|
|
if self.bus is None:
|
|
|
return found
|
|
|
try:
|
|
|
for address in range(0x03, 0x78): # valid 7-bit range
|
|
|
try:
|
|
|
# write_quick is the classic probe; some devices NACK writes.
|
|
|
self.bus.write_quick(address)
|
|
|
found.append(f"0x{address:02X}")
|
|
|
except OSError:
|
|
|
continue
|
|
|
except Exception as e:
|
|
|
self.log("warn", f"Unexpected at 0x{address:02X}: {e}")
|
|
|
continue
|
|
|
self.log("info", f"Scan complete. Found devices: {found}")
|
|
|
except Exception as e:
|
|
|
self.log("error", f"Scan failed: {e}")
|
|
|
return found
|
|
|
|
|
|
# ---------------------------
|
|
|
# Connection lifecycle
|
|
|
# ---------------------------
|
|
|
|
|
|
def connect(self, port: Union[int, str]) -> bool:
|
|
|
"""
|
|
|
Open the I²C bus. 'port' can be an int or string ('1' -> /dev/i2c-1).
|
|
|
"""
|
|
|
if config.DEBUG_MODE:
|
|
|
self._state = I2CState.CONNECTED_IDLE
|
|
|
self.log("info", "DEBUG: simulate I²C connect")
|
|
|
self.log("success", f"🔗 Connected to I²C bus {port} (DEBUG)")
|
|
|
return True
|
|
|
|
|
|
try:
|
|
|
self.bus_id = _coerce_int(port)
|
|
|
with self._bus_lock:
|
|
|
self.bus = smbus2.SMBus(self.bus_id)
|
|
|
self._state = I2CState.CONNECTED_IDLE
|
|
|
self.log("success", f"🔗 Connected to I²C bus {self.bus_id}")
|
|
|
return True
|
|
|
except FileNotFoundError:
|
|
|
self.log("error", f"I²C bus {port} not found (no /dev/i2c-{port}).")
|
|
|
except Exception as e:
|
|
|
self.log("error", f"❌ Connection failed: {e}")
|
|
|
|
|
|
self._state = I2CState.DISCONNECTED
|
|
|
return False
|
|
|
|
|
|
def disconnect(self) -> bool:
|
|
|
"""
|
|
|
Stop logger (join) and close the bus cleanly.
|
|
|
Safe to call even if already disconnected.
|
|
|
"""
|
|
|
if config.DEBUG_MODE:
|
|
|
self._state = I2CState.DISCONNECTED
|
|
|
self.log("info", "DEBUG: simulate I²C disconnect")
|
|
|
return True
|
|
|
|
|
|
try:
|
|
|
# 1) Stop logger first so no thread touches the bus.
|
|
|
self.stop_logger()
|
|
|
|
|
|
# 2) Close the bus under the same lock used for I/O.
|
|
|
with self._bus_lock:
|
|
|
if self.bus:
|
|
|
try:
|
|
|
self.bus.close()
|
|
|
except Exception:
|
|
|
pass
|
|
|
self.bus = None
|
|
|
|
|
|
self._state = I2CState.DISCONNECTED
|
|
|
self.log("info", f"🔌 Disconnected from I²C bus {self.bus_id}")
|
|
|
return True
|
|
|
except Exception as e:
|
|
|
self.log("error", f"❌ Disconnection error: {e}")
|
|
|
return False
|
|
|
|
|
|
# ---------------------------
|
|
|
# One-shot command + capture window (UART-style)
|
|
|
# ---------------------------
|
|
|
|
|
|
def send_command(self, command, device_address=0x40):
|
|
|
"""
|
|
|
Execute a single I²C action under an exclusive capture window.
|
|
|
|
|
|
command: either
|
|
|
- str (raw): "01,02,03" (block write to reg 0x00)
|
|
|
- dict: {
|
|
|
'action'/'operation': 'read'|'write',
|
|
|
'reg'/'register': int|str,
|
|
|
'length': int (for read),
|
|
|
'value': int (single-byte write),
|
|
|
'bytes': '01,02,03' (block write string)
|
|
|
}
|
|
|
device_address: int or str ('0x40' accepted). If None, attempt to read
|
|
|
from command['address'] if present.
|
|
|
"""
|
|
|
if self._shutting_down:
|
|
|
self.log("warning", "⛔ I²C is shutting down; command ignored.")
|
|
|
return None
|
|
|
|
|
|
# must be connected in real mode
|
|
|
if not config.DEBUG_MODE and not self.bus:
|
|
|
self.log("error", "❌ | bus not connected")
|
|
|
return None
|
|
|
|
|
|
# Claim capture (exclusive) — reject if already in use
|
|
|
deadline_ns = now_ns() + 300_000_000 # ~300 ms default window
|
|
|
if not claim_i2c_capture(deadline_ns, owner="I2C_CMD"):
|
|
|
self.log("warning", "⛔ Busy: capture window active. Try again shortly.")
|
|
|
return None
|
|
|
|
|
|
prev_state = self._state
|
|
|
self._state = I2CState.CAPTURE_ACTIVE
|
|
|
try:
|
|
|
# Normalize device address
|
|
|
if device_address is not None:
|
|
|
addr = self._to_int_any(device_address, default=0x40)
|
|
|
elif isinstance(command, dict) and "address" in command:
|
|
|
addr = self._to_int_any(command["address"], default=0x40)
|
|
|
else:
|
|
|
self.log("error", "❌ | no device address provided")
|
|
|
return None
|
|
|
|
|
|
# RAW string mode (block write to reg 0x00)
|
|
|
if isinstance(command, str):
|
|
|
data = self._parse_hex_list(command)
|
|
|
self.log(
|
|
|
"info",
|
|
|
f"➡️ | addr {self._fmt_hex(addr)} | reg 0x00 | action WRITE | bytes {self._fmt_bytes(data)}",
|
|
|
)
|
|
|
with self._bus_lock:
|
|
|
if not config.DEBUG_MODE:
|
|
|
self.bus.write_i2c_block_data(addr, 0x00, data)
|
|
|
self.log(
|
|
|
"success", f"✅ | addr {self._fmt_hex(addr)} | reg 0x00 | status OK"
|
|
|
)
|
|
|
return True
|
|
|
|
|
|
# Dict mode
|
|
|
action = (
|
|
|
command.get("action") or command.get("operation") or "read"
|
|
|
).upper()
|
|
|
reg = self._to_int_any(
|
|
|
command.get("reg", command.get("register", 0)), default=0
|
|
|
)
|
|
|
|
|
|
if action == "READ":
|
|
|
length = int(command.get("length", 1))
|
|
|
self.log(
|
|
|
"info",
|
|
|
f"➡️ | addr {self._fmt_hex(addr)} | reg {self._fmt_hex(reg)} | action READ | len {length}",
|
|
|
)
|
|
|
with self._bus_lock:
|
|
|
if not config.DEBUG_MODE:
|
|
|
if length <= 1:
|
|
|
data = [self.bus.read_byte_data(addr, reg)]
|
|
|
else:
|
|
|
data = self.bus.read_i2c_block_data(addr, reg, length)
|
|
|
else:
|
|
|
# synthetic data in DEBUG
|
|
|
data = [0xBE] if length <= 1 else list(range(length))
|
|
|
self.log(
|
|
|
"success",
|
|
|
f"⬅️ | addr {self._fmt_hex(addr)} | reg {self._fmt_hex(reg)} | data {self._fmt_bytes(data)}",
|
|
|
)
|
|
|
return data if length > 1 else data[0]
|
|
|
|
|
|
elif action == "WRITE":
|
|
|
if "bytes" in command and command["bytes"]:
|
|
|
data = self._parse_hex_list(command["bytes"])
|
|
|
self.log(
|
|
|
"info",
|
|
|
f"➡️ | addr {self._fmt_hex(addr)} | reg {self._fmt_hex(reg)} | action WRITE | bytes {self._fmt_bytes(data)}",
|
|
|
)
|
|
|
with self._bus_lock:
|
|
|
if not config.DEBUG_MODE:
|
|
|
self.bus.write_i2c_block_data(addr, reg, data)
|
|
|
self.log(
|
|
|
"success",
|
|
|
f"✅ | addr {self._fmt_hex(addr)} | reg {self._fmt_hex(reg)} | status OK",
|
|
|
)
|
|
|
return True
|
|
|
else:
|
|
|
value = self._to_int_any(command.get("value", 0))
|
|
|
self.log(
|
|
|
"info",
|
|
|
f"➡️ | addr {self._fmt_hex(addr)} | reg {self._fmt_hex(reg)} | action WRITE | value {self._fmt_hex(value)}",
|
|
|
)
|
|
|
with self._bus_lock:
|
|
|
if not config.DEBUG_MODE:
|
|
|
self.bus.write_byte_data(addr, reg, value)
|
|
|
self.log(
|
|
|
"success",
|
|
|
f"✅ | addr {self._fmt_hex(addr)} | reg {self._fmt_hex(reg)} | status OK",
|
|
|
)
|
|
|
return True
|
|
|
|
|
|
else:
|
|
|
self.log(
|
|
|
"warning",
|
|
|
f"⚠️ | addr {self._fmt_hex(addr)} | reg {self._fmt_hex(reg)} | unknown action '{action}'",
|
|
|
)
|
|
|
return None
|
|
|
|
|
|
except Exception as e:
|
|
|
self.log("error", f"❌ | error: {e}")
|
|
|
return None
|
|
|
finally:
|
|
|
release_i2c_capture()
|
|
|
# restore state (back to LOGGING if logger is on)
|
|
|
self._state = (
|
|
|
I2CState.LOGGING if self._logger_running else I2CState.CONNECTED_IDLE
|
|
|
)
|
|
|
|
|
|
# ---------------------------
|
|
|
# Hot-path primitive (no logging, no waits)
|
|
|
# ---------------------------
|
|
|
def read_2_bytes(self, addr_7bit: int, reg: int) -> dict:
|
|
|
"""
|
|
|
Fast, on-demand I²C read of exactly 2 bytes from 'reg' at 7-bit address.
|
|
|
- Immediate-or-skip capture; no waits, no prints, no retries.
|
|
|
"""
|
|
|
# must be connected in real (non-debug) mode
|
|
|
if not config.DEBUG_MODE and self.bus is None:
|
|
|
return {"status": "ERR_NOT_CONNECTED", "addr": addr_7bit, "reg": reg}
|
|
|
|
|
|
# Give the coordinator a tiny future window so claim is valid
|
|
|
deadline_ns = now_ns() + 5_000
|
|
|
if not claim_i2c_capture(deadline_ns, owner="I2C_PKT"):
|
|
|
return {"status": "SKIPPED_BUSY", "addr": addr_7bit, "reg": reg}
|
|
|
|
|
|
try:
|
|
|
with self._bus_lock:
|
|
|
data = self.bus.read_i2c_block_data(addr_7bit, reg, 2)
|
|
|
if not isinstance(data, list) or len(data) != 2:
|
|
|
return {"status": "ERR", "addr": addr_7bit, "reg": reg}
|
|
|
return {
|
|
|
"status": "OK",
|
|
|
"bytes": [data[0], data[1]],
|
|
|
}
|
|
|
except Exception:
|
|
|
return {"status": "ERR", "addr": addr_7bit, "reg": reg}
|
|
|
finally:
|
|
|
release_i2c_capture()
|
|
|
|
|
|
def measure_zero_raw14(self, duration_ms: int = 200, interval_ms: int = 10):
|
|
|
"""
|
|
|
Measure zero for the I²C angle sensor.
|
|
|
Saves the result bytes into config.SESSION_ZERO = [MSB, LSB] (big-endian).
|
|
|
Returns the median raw14 (int) or None.
|
|
|
"""
|
|
|
samples = []
|
|
|
deadline = time.time() + (duration_ms / 1000.0)
|
|
|
|
|
|
try:
|
|
|
if self.connect(1):
|
|
|
self.log("info", "I²C auto-initialized on /dev/i2c-1 to set zero")
|
|
|
while time.time() < deadline:
|
|
|
rep = self.read_2_bytes(
|
|
|
0x40, 0xFE
|
|
|
) # dict: {"status":"OK","bytes":[b0,b1]}
|
|
|
if isinstance(rep, dict) and rep.get("status") == "OK":
|
|
|
by = rep.get("bytes")
|
|
|
if isinstance(by, list) and len(by) == 2:
|
|
|
raw16 = ((by[0] & 0xFF) << 8) | (by[1] & 0xFF) # big-endian
|
|
|
raw14 = raw16 & 0x3FFF
|
|
|
samples.append(raw14)
|
|
|
time.sleep(interval_ms / 1000.0)
|
|
|
|
|
|
if not samples:
|
|
|
self.log("warn", "I²C zero measurement collected no valid samples")
|
|
|
return None
|
|
|
|
|
|
med = int(statistics.median(samples))
|
|
|
msb = (med >> 8) & 0xFF
|
|
|
lsb = med & 0xFF
|
|
|
|
|
|
# Store exactly the two bytes
|
|
|
config.SESSION_ZERO = [msb, lsb]
|
|
|
|
|
|
self.log(
|
|
|
"info", f"I²C zero raw14={med} -> SESSION_ZERO={msb:02X} {lsb:02X}"
|
|
|
)
|
|
|
return med
|
|
|
|
|
|
except Exception as e:
|
|
|
self.log("error", f"I²C zero measurement exception: {e}")
|
|
|
return None
|
|
|
|
|
|
# ---------------------------
|
|
|
# Continuous Logger (thread) — UART-style
|
|
|
# ---------------------------
|
|
|
|
|
|
def start_logger(self, device_address, reg, length, interval_ms: int = 200) -> bool:
|
|
|
"""
|
|
|
Start the background I²C logger thread.
|
|
|
|
|
|
- If already running: no-op, returns True.
|
|
|
- If not connected (and not in DEBUG): returns False.
|
|
|
|
|
|
Behavior:
|
|
|
- At each interval, performs a READ of 'length' bytes at 'reg' from 'device_address'.
|
|
|
- During a capture window, emissions are throttled to at most once every _throttle_ns.
|
|
|
"""
|
|
|
if self._logger_running:
|
|
|
self.log("info", "ℹ️ I²C logger already running.")
|
|
|
return True
|
|
|
if not config.DEBUG_MODE and not self.bus:
|
|
|
self.log("error", "⚠️ Cannot start I²C logger: not connected.")
|
|
|
return False
|
|
|
|
|
|
addr = self._to_int_any(device_address)
|
|
|
r = self._to_int_any(reg)
|
|
|
ln = self._to_int_any(length, default=1)
|
|
|
if addr is None or r is None or ln is None or ln <= 0:
|
|
|
self.log("warning", "⚠️ Invalid logger params (addr/reg/length).")
|
|
|
return False
|
|
|
|
|
|
self._log_addr = addr
|
|
|
self._log_reg = r
|
|
|
self._log_len = ln
|
|
|
self._log_interval_s = max(0.001, (interval_ms or 200) / 1000.0)
|
|
|
|
|
|
self._logger_running = True
|
|
|
self._logger_thread = threading.Thread(
|
|
|
target=self._logger_loop, name="I2CLogger", daemon=True
|
|
|
)
|
|
|
self._logger_thread.start()
|
|
|
self._state = I2CState.LOGGING
|
|
|
self.log(
|
|
|
"success",
|
|
|
f"🟢 I²C logger started | addr 0x{addr:02X} | reg 0x{r:02X} | len {ln} | interval {interval_ms}ms",
|
|
|
)
|
|
|
return True
|
|
|
|
|
|
def stop_logger(self) -> bool:
|
|
|
"""
|
|
|
Stop the background I²C logger thread if running.
|
|
|
Joins with a short timeout so shutdown remains responsive.
|
|
|
"""
|
|
|
if not self._logger_running:
|
|
|
return True
|
|
|
self._logger_running = False
|
|
|
t = self._logger_thread
|
|
|
self._logger_thread = None
|
|
|
|
|
|
# Wait up to ~2s for a clean exit (covers a read that’s mid-call + pacing sleep)
|
|
|
if t and t.is_alive():
|
|
|
t.join(timeout=2.0)
|
|
|
# If still alive, wait a tiny bit more — but don't hang the UI
|
|
|
if t.is_alive():
|
|
|
time.sleep(0.05)
|
|
|
|
|
|
if self.bus:
|
|
|
self._state = I2CState.CONNECTED_IDLE
|
|
|
self.log("info", "🔴 I²C logger stopped.")
|
|
|
return True
|
|
|
|
|
|
def _logger_loop(self):
|
|
|
"""
|
|
|
Background poller (daemon thread):
|
|
|
- DEBUG_MODE: emits synthetic lines periodically so you can test the UI without hardware.
|
|
|
- Real bus: performs read(s) at ~_log_interval_s cadence.
|
|
|
- During capture: throttle emissions (drop extras) to one per _throttle_ns.
|
|
|
"""
|
|
|
addr = self._log_addr
|
|
|
r = self._log_reg
|
|
|
ln = self._log_len
|
|
|
pace = self._log_interval_s
|
|
|
|
|
|
last_emit_ns = now_ns()
|
|
|
try:
|
|
|
while self._logger_running:
|
|
|
try:
|
|
|
# Respect capture throttle (like UART)
|
|
|
if self._shutting_down:
|
|
|
break # exit ASAP on shutdown
|
|
|
if i2c_capture_active():
|
|
|
now = now_ns()
|
|
|
if now - last_emit_ns < self._throttle_ns:
|
|
|
time.sleep(self._logger_sleep_s)
|
|
|
continue
|
|
|
last_emit_ns = now
|
|
|
|
|
|
# TX line
|
|
|
self.log(
|
|
|
"info",
|
|
|
f"➡️ | addr 0x{addr:02X} | reg 0x{r:02X} | action READ | len {ln}",
|
|
|
)
|
|
|
|
|
|
# Perform read under the same bus lock as send_command
|
|
|
with self._bus_lock:
|
|
|
if not self.bus and not config.DEBUG_MODE:
|
|
|
break
|
|
|
if not self.bus and not config.DEBUG_MODE:
|
|
|
break
|
|
|
|
|
|
if config.DEBUG_MODE:
|
|
|
data = [0xBE] if ln <= 1 else list(range(ln))
|
|
|
else:
|
|
|
if ln <= 1:
|
|
|
data = [self.bus.read_byte_data(addr, r)]
|
|
|
else:
|
|
|
data = self.bus.read_i2c_block_data(addr, r, ln)
|
|
|
|
|
|
# RX line
|
|
|
self.log(
|
|
|
"success",
|
|
|
f"⬅️ | addr 0x{addr:02X} | reg 0x{r:02X} | data {self._fmt_bytes(data)}",
|
|
|
)
|
|
|
|
|
|
except Exception as e:
|
|
|
self.log(
|
|
|
"error", f"❌ | addr 0x{addr:02X} | reg 0x{r:02X} | error {e}"
|
|
|
)
|
|
|
|
|
|
# pacing
|
|
|
time.sleep(pace or self._logger_sleep_s)
|
|
|
finally:
|
|
|
self._logger_running = False
|
|
|
|
|
|
# ---------------------------
|
|
|
# Commands catalog passthrough
|
|
|
# ---------------------------
|
|
|
|
|
|
def get_predefined_commands(self):
|
|
|
"""
|
|
|
Load predefined I²C commands for your command table.
|
|
|
The DB returns rows mapped by components.data.db.get_i2c_commands().
|
|
|
"""
|
|
|
return get_i2c_commands()
|