You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
685 lines
18 KiB
685 lines
18 KiB
#!/usr/bin/env python3
|
|
"""
|
|
I2C Core - vzug-e-hinge
|
|
=======================
|
|
Generic I2C bus management with reading/writing and continuous logging.
|
|
|
|
Features:
|
|
- Bus scanning (detect available /dev/i2c-*)
|
|
- Device scanning (like i2cdetect)
|
|
- Read/write operations (byte or block)
|
|
- Continuous logger thread (background polling)
|
|
- Circular buffer for logger data
|
|
- Timestamp synchronization (inject clock)
|
|
- Thread-safe operations
|
|
- Sensor-agnostic (generic register access)
|
|
|
|
Author: Kynsight
|
|
Version: 1.0.0
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
import os
|
|
import threading
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from typing import Optional, Callable, Tuple, List
|
|
|
|
try:
|
|
import smbus2
|
|
except ImportError:
|
|
smbus2 = None
|
|
|
|
from buffer_kit.circular_buffer import (
|
|
Status as BufferStatus,
|
|
CircularBufferHandle,
|
|
cb_init,
|
|
cb_write,
|
|
cb_capacity,
|
|
cb_fill_bytes,
|
|
cb_fill_pct,
|
|
cb_overflows,
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Status & Configuration
|
|
# =============================================================================
|
|
|
|
class Status(Enum):
|
|
"""Operation status codes."""
|
|
OK = 0
|
|
ERROR = 1
|
|
BUS_NOT_FOUND = 2
|
|
DEVICE_NOT_FOUND = 3
|
|
ALREADY_OPEN = 4
|
|
NOT_OPEN = 5
|
|
LOGGER_RUNNING = 6
|
|
NACK = 7 # Device did not acknowledge
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class I2CConfig:
|
|
"""
|
|
I2C bus configuration.
|
|
|
|
bus_id: Bus number (e.g., 1 for /dev/i2c-1)
|
|
buffer_size: Logger circular buffer capacity
|
|
timestamp_source: Optional external time function (for sync with UART)
|
|
"""
|
|
bus_id: int
|
|
buffer_size: int = 4096
|
|
timestamp_source: Optional[Callable[[], float]] = None
|
|
|
|
|
|
@dataclass
|
|
class I2CReading:
|
|
"""
|
|
Single I2C read/write operation record.
|
|
|
|
reading_id: Sequential ID (per session)
|
|
timestamp: When operation occurred
|
|
address: Device address (7-bit)
|
|
register: Register address
|
|
data: Raw bytes read/written
|
|
operation: "READ" or "WRITE"
|
|
"""
|
|
reading_id: int
|
|
timestamp: float
|
|
address: int
|
|
register: int
|
|
data: bytes
|
|
operation: str # "READ" or "WRITE"
|
|
|
|
|
|
@dataclass
|
|
class BusInfo:
|
|
"""
|
|
Information about I2C bus status.
|
|
|
|
is_open: Bus is opened
|
|
is_logger_running: Logger thread active
|
|
buffer_capacity: Buffer size
|
|
buffer_fill_bytes: Current buffer usage
|
|
buffer_fill_percent: Fill percentage
|
|
buffer_overflows: Overflow count
|
|
total_reads: Total read operations
|
|
total_writes: Total write operations
|
|
total_errors: Total errors encountered
|
|
"""
|
|
is_open: bool
|
|
is_logger_running: bool
|
|
buffer_capacity: int
|
|
buffer_fill_bytes: int
|
|
buffer_fill_percent: int
|
|
buffer_overflows: int
|
|
total_reads: int
|
|
total_writes: int
|
|
total_errors: int
|
|
|
|
|
|
# =============================================================================
|
|
# I2C Handle
|
|
# =============================================================================
|
|
|
|
@dataclass
|
|
class I2CHandle:
|
|
"""
|
|
I2C bus handle (internal state - use API functions).
|
|
|
|
Do not access fields directly - use i2c_* functions.
|
|
"""
|
|
config: I2CConfig
|
|
|
|
# SMBus
|
|
_bus: Optional[smbus2.SMBus] = None
|
|
_is_open: bool = False
|
|
|
|
# Circular buffer for logger
|
|
_buffer: Optional[CircularBufferHandle] = None
|
|
|
|
# Logger thread
|
|
_logger_thread: Optional[threading.Thread] = None
|
|
_logger_running: bool = False
|
|
_stop_logger_event: threading.Event = field(default_factory=threading.Event)
|
|
|
|
# Logger configuration
|
|
_logger_addr: int = 0
|
|
_logger_reg: int = 0
|
|
_logger_len: int = 1
|
|
_logger_interval_ms: int = 100
|
|
|
|
# Thread safety
|
|
_lock: threading.RLock = field(default_factory=threading.RLock)
|
|
|
|
# Statistics
|
|
_total_reads: int = 0
|
|
_total_writes: int = 0
|
|
_total_errors: int = 0
|
|
_reading_counter: int = 0
|
|
|
|
# Timestamp function
|
|
_get_timestamp: Callable[[], float] = field(default=time.perf_counter)
|
|
|
|
def __post_init__(self):
|
|
"""Initialize timestamp source."""
|
|
if self.config.timestamp_source:
|
|
self._get_timestamp = self.config.timestamp_source
|
|
|
|
|
|
# =============================================================================
|
|
# Bus Discovery
|
|
# =============================================================================
|
|
|
|
def i2c_scan_buses() -> List[int]:
|
|
"""
|
|
Scan for available I2C buses.
|
|
|
|
Returns:
|
|
List of bus IDs (e.g., [0, 1, 2] for /dev/i2c-0, /dev/i2c-1, etc.)
|
|
"""
|
|
buses = []
|
|
try:
|
|
for entry in os.listdir("/dev"):
|
|
if entry.startswith("i2c-"):
|
|
try:
|
|
bus_id = int(entry.split("-")[1])
|
|
buses.append(bus_id)
|
|
except ValueError:
|
|
continue
|
|
except Exception:
|
|
pass
|
|
|
|
return sorted(buses)
|
|
|
|
|
|
# =============================================================================
|
|
# Bus Lifecycle
|
|
# =============================================================================
|
|
|
|
def i2c_create(config: I2CConfig) -> Tuple[Status, Optional[I2CHandle]]:
|
|
"""
|
|
Create I2C handle.
|
|
|
|
Args:
|
|
config: I2C configuration
|
|
|
|
Returns:
|
|
(Status.OK, handle) on success
|
|
(Status.ERROR, None) on failure
|
|
"""
|
|
if not smbus2:
|
|
return (Status.ERROR, None)
|
|
|
|
if config.bus_id < 0:
|
|
return (Status.ERROR, None)
|
|
|
|
handle = I2CHandle(config=config)
|
|
return (Status.OK, handle)
|
|
|
|
|
|
def i2c_open(handle: I2CHandle) -> Status:
|
|
"""
|
|
Open I2C bus.
|
|
|
|
Args:
|
|
handle: I2C handle
|
|
|
|
Returns:
|
|
Status.OK on success
|
|
Status.ALREADY_OPEN if already open
|
|
Status.BUS_NOT_FOUND if bus not available
|
|
"""
|
|
if handle._is_open:
|
|
return Status.ALREADY_OPEN
|
|
|
|
try:
|
|
# Open SMBus
|
|
handle._bus = smbus2.SMBus(handle.config.bus_id)
|
|
|
|
# Create buffer for logger
|
|
status, buffer = cb_init(handle.config.buffer_size)
|
|
if status != BufferStatus.OK:
|
|
handle._bus.close()
|
|
return Status.ERROR
|
|
|
|
handle._buffer = buffer
|
|
handle._is_open = True
|
|
|
|
# Reset statistics
|
|
with handle._lock:
|
|
handle._total_reads = 0
|
|
handle._total_writes = 0
|
|
handle._total_errors = 0
|
|
handle._reading_counter = 0
|
|
|
|
return Status.OK
|
|
|
|
except FileNotFoundError:
|
|
return Status.BUS_NOT_FOUND
|
|
except Exception:
|
|
return Status.ERROR
|
|
|
|
|
|
def i2c_close(handle: I2CHandle) -> Status:
|
|
"""
|
|
Close I2C bus and cleanup.
|
|
|
|
Stops logger if running.
|
|
|
|
Args:
|
|
handle: I2C handle
|
|
|
|
Returns:
|
|
Status.OK
|
|
"""
|
|
if not handle._is_open:
|
|
return Status.OK
|
|
|
|
# Stop logger first
|
|
if handle._logger_running:
|
|
i2c_stop_logger(handle)
|
|
|
|
# Close bus
|
|
with handle._lock:
|
|
if handle._bus:
|
|
try:
|
|
handle._bus.close()
|
|
except:
|
|
pass
|
|
handle._bus = None
|
|
|
|
handle._buffer = None
|
|
handle._is_open = False
|
|
|
|
return Status.OK
|
|
|
|
|
|
# =============================================================================
|
|
# Device Scanning
|
|
# =============================================================================
|
|
|
|
def i2c_scan_devices(handle: I2CHandle) -> Tuple[Status, List[int]]:
|
|
"""
|
|
Scan bus for devices (like i2cdetect).
|
|
|
|
Probes addresses 0x03 to 0x77 (valid 7-bit range).
|
|
|
|
Args:
|
|
handle: I2C handle (must be open)
|
|
|
|
Returns:
|
|
(Status.OK, [addresses]) on success
|
|
(Status.NOT_OPEN, []) if bus not open
|
|
"""
|
|
if not handle._is_open or not handle._bus:
|
|
return (Status.NOT_OPEN, [])
|
|
|
|
devices = []
|
|
|
|
with handle._lock:
|
|
for addr in range(0x03, 0x78): # Valid 7-bit address range
|
|
try:
|
|
# Quick write to probe device
|
|
handle._bus.write_quick(addr)
|
|
devices.append(addr)
|
|
except OSError:
|
|
# Device not present (NACK)
|
|
continue
|
|
except Exception:
|
|
# Other error - skip
|
|
continue
|
|
|
|
return (Status.OK, devices)
|
|
|
|
|
|
# =============================================================================
|
|
# Read Operations
|
|
# =============================================================================
|
|
|
|
def i2c_read_byte(handle: I2CHandle, addr: int, reg: int) -> Tuple[Status, int]:
|
|
"""
|
|
Read single byte from register.
|
|
|
|
Args:
|
|
handle: I2C handle
|
|
addr: Device address (7-bit)
|
|
reg: Register address
|
|
|
|
Returns:
|
|
(Status.OK, value) on success
|
|
(Status.NOT_OPEN, 0) if bus not open
|
|
(Status.NACK, 0) if device did not respond
|
|
(Status.ERROR, 0) on other error
|
|
"""
|
|
if not handle._is_open or not handle._bus:
|
|
return (Status.NOT_OPEN, 0)
|
|
|
|
try:
|
|
with handle._lock:
|
|
value = handle._bus.read_byte_data(addr, reg)
|
|
handle._total_reads += 1
|
|
|
|
return (Status.OK, value)
|
|
|
|
except OSError as e:
|
|
with handle._lock:
|
|
handle._total_errors += 1
|
|
# errno 121 = Remote I/O error (NACK)
|
|
if e.errno == 121:
|
|
return (Status.NACK, 0)
|
|
return (Status.ERROR, 0)
|
|
|
|
except Exception:
|
|
with handle._lock:
|
|
handle._total_errors += 1
|
|
return (Status.ERROR, 0)
|
|
|
|
|
|
def i2c_read_block(handle: I2CHandle, addr: int, reg: int, length: int) -> Tuple[Status, bytes]:
|
|
"""
|
|
Read block of bytes from register.
|
|
|
|
Args:
|
|
handle: I2C handle
|
|
addr: Device address (7-bit)
|
|
reg: Register address
|
|
length: Number of bytes to read (1-32)
|
|
|
|
Returns:
|
|
(Status.OK, data) on success
|
|
(Status.NOT_OPEN, b"") if bus not open
|
|
(Status.NACK, b"") if device did not respond
|
|
(Status.ERROR, b"") on other error
|
|
"""
|
|
if not handle._is_open or not handle._bus:
|
|
return (Status.NOT_OPEN, b"")
|
|
|
|
if length < 1 or length > 32:
|
|
return (Status.ERROR, b"")
|
|
|
|
try:
|
|
with handle._lock:
|
|
data = handle._bus.read_i2c_block_data(addr, reg, length)
|
|
handle._total_reads += 1
|
|
|
|
return (Status.OK, bytes(data))
|
|
|
|
except OSError as e:
|
|
with handle._lock:
|
|
handle._total_errors += 1
|
|
if e.errno == 121:
|
|
return (Status.NACK, b"")
|
|
return (Status.ERROR, b"")
|
|
|
|
except Exception:
|
|
with handle._lock:
|
|
handle._total_errors += 1
|
|
return (Status.ERROR, b"")
|
|
|
|
|
|
# =============================================================================
|
|
# Write Operations
|
|
# =============================================================================
|
|
|
|
def i2c_write_byte(handle: I2CHandle, addr: int, reg: int, value: int) -> Status:
|
|
"""
|
|
Write single byte to register.
|
|
|
|
Args:
|
|
handle: I2C handle
|
|
addr: Device address (7-bit)
|
|
reg: Register address
|
|
value: Byte value (0-255)
|
|
|
|
Returns:
|
|
Status.OK on success
|
|
Status.NOT_OPEN if bus not open
|
|
Status.NACK if device did not respond
|
|
Status.ERROR on other error
|
|
"""
|
|
if not handle._is_open or not handle._bus:
|
|
return Status.NOT_OPEN
|
|
|
|
if value < 0 or value > 255:
|
|
return Status.ERROR
|
|
|
|
try:
|
|
with handle._lock:
|
|
handle._bus.write_byte_data(addr, reg, value)
|
|
handle._total_writes += 1
|
|
|
|
return Status.OK
|
|
|
|
except OSError as e:
|
|
with handle._lock:
|
|
handle._total_errors += 1
|
|
if e.errno == 121:
|
|
return Status.NACK
|
|
return Status.ERROR
|
|
|
|
except Exception:
|
|
with handle._lock:
|
|
handle._total_errors += 1
|
|
return Status.ERROR
|
|
|
|
|
|
def i2c_write_block(handle: I2CHandle, addr: int, reg: int, data: bytes) -> Status:
|
|
"""
|
|
Write block of bytes to register.
|
|
|
|
Args:
|
|
handle: I2C handle
|
|
addr: Device address (7-bit)
|
|
reg: Register address
|
|
data: Bytes to write (1-32 bytes)
|
|
|
|
Returns:
|
|
Status.OK on success
|
|
Status.NOT_OPEN if bus not open
|
|
Status.NACK if device did not respond
|
|
Status.ERROR on other error
|
|
"""
|
|
if not handle._is_open or not handle._bus:
|
|
return Status.NOT_OPEN
|
|
|
|
if len(data) < 1 or len(data) > 32:
|
|
return Status.ERROR
|
|
|
|
try:
|
|
with handle._lock:
|
|
handle._bus.write_i2c_block_data(addr, reg, list(data))
|
|
handle._total_writes += 1
|
|
|
|
return Status.OK
|
|
|
|
except OSError as e:
|
|
with handle._lock:
|
|
handle._total_errors += 1
|
|
if e.errno == 121:
|
|
return Status.NACK
|
|
return Status.ERROR
|
|
|
|
except Exception:
|
|
with handle._lock:
|
|
handle._total_errors += 1
|
|
return Status.ERROR
|
|
|
|
|
|
# =============================================================================
|
|
# Logger (Background Polling)
|
|
# =============================================================================
|
|
|
|
def i2c_start_logger(handle: I2CHandle, addr: int, reg: int, length: int,
|
|
interval_ms: int) -> Status:
|
|
"""
|
|
Start background logger thread.
|
|
|
|
Continuously polls register at specified rate and stores in buffer.
|
|
|
|
Args:
|
|
handle: I2C handle
|
|
addr: Device address (7-bit)
|
|
reg: Register address
|
|
length: Bytes to read (1-32)
|
|
interval_ms: Polling interval in milliseconds
|
|
|
|
Returns:
|
|
Status.OK on success
|
|
Status.NOT_OPEN if bus not open
|
|
Status.LOGGER_RUNNING if already running
|
|
"""
|
|
if not handle._is_open:
|
|
return Status.NOT_OPEN
|
|
|
|
if handle._logger_running:
|
|
return Status.LOGGER_RUNNING
|
|
|
|
if length < 1 or length > 32 or interval_ms < 1:
|
|
return Status.ERROR
|
|
|
|
# Configure logger
|
|
handle._logger_addr = addr
|
|
handle._logger_reg = reg
|
|
handle._logger_len = length
|
|
handle._logger_interval_ms = interval_ms
|
|
|
|
# Start thread
|
|
handle._stop_logger_event.clear()
|
|
handle._logger_running = True
|
|
|
|
handle._logger_thread = threading.Thread(
|
|
target=_logger_thread_func,
|
|
args=(handle,),
|
|
daemon=True,
|
|
name=f"I2C-Logger-{handle.config.bus_id}"
|
|
)
|
|
handle._logger_thread.start()
|
|
|
|
return Status.OK
|
|
|
|
|
|
def i2c_stop_logger(handle: I2CHandle) -> Status:
|
|
"""
|
|
Stop background logger thread.
|
|
|
|
Blocks until thread exits (with timeout).
|
|
|
|
Args:
|
|
handle: I2C handle
|
|
|
|
Returns:
|
|
Status.OK
|
|
"""
|
|
if not handle._logger_running:
|
|
return Status.OK
|
|
|
|
handle._stop_logger_event.set()
|
|
|
|
if handle._logger_thread:
|
|
handle._logger_thread.join(timeout=2.0)
|
|
handle._logger_thread = None
|
|
|
|
handle._logger_running = False
|
|
|
|
return Status.OK
|
|
|
|
|
|
def _logger_thread_func(handle: I2CHandle):
|
|
"""
|
|
Logger thread implementation.
|
|
|
|
Polls register at configured interval and stores readings in buffer.
|
|
"""
|
|
addr = handle._logger_addr
|
|
reg = handle._logger_reg
|
|
length = handle._logger_len
|
|
interval_s = handle._logger_interval_ms / 1000.0
|
|
|
|
while not handle._stop_logger_event.is_set():
|
|
try:
|
|
# Read from device
|
|
with handle._lock:
|
|
if not handle._bus:
|
|
break
|
|
|
|
data = handle._bus.read_i2c_block_data(addr, reg, length)
|
|
timestamp = handle._get_timestamp()
|
|
handle._total_reads += 1
|
|
handle._reading_counter += 1
|
|
reading_id = handle._reading_counter
|
|
|
|
# Create reading record
|
|
reading = I2CReading(
|
|
reading_id=reading_id,
|
|
timestamp=timestamp,
|
|
address=addr,
|
|
register=reg,
|
|
data=bytes(data),
|
|
operation="READ"
|
|
)
|
|
|
|
# Store in buffer (serialize reading to bytes)
|
|
# Format: [timestamp(8)][addr(1)][reg(1)][len(1)][data(N)]
|
|
buf_data = (
|
|
int(timestamp * 1_000_000).to_bytes(8, 'little') +
|
|
addr.to_bytes(1, 'little') +
|
|
reg.to_bytes(1, 'little') +
|
|
len(data).to_bytes(1, 'little') +
|
|
bytes(data)
|
|
)
|
|
cb_write(handle._buffer, buf_data)
|
|
|
|
except Exception:
|
|
with handle._lock:
|
|
handle._total_errors += 1
|
|
|
|
# Sleep for interval (check stop event periodically)
|
|
handle._stop_logger_event.wait(interval_s)
|
|
|
|
|
|
# =============================================================================
|
|
# Status Query
|
|
# =============================================================================
|
|
|
|
def i2c_get_status(handle: I2CHandle) -> BusInfo:
|
|
"""
|
|
Get current bus status.
|
|
|
|
Args:
|
|
handle: I2C handle
|
|
|
|
Returns:
|
|
BusInfo with current metrics
|
|
"""
|
|
if not handle._buffer:
|
|
return BusInfo(
|
|
is_open=handle._is_open,
|
|
is_logger_running=handle._logger_running,
|
|
buffer_capacity=0,
|
|
buffer_fill_bytes=0,
|
|
buffer_fill_percent=0,
|
|
buffer_overflows=0,
|
|
total_reads=0,
|
|
total_writes=0,
|
|
total_errors=0
|
|
)
|
|
|
|
with handle._lock:
|
|
total_reads = handle._total_reads
|
|
total_writes = handle._total_writes
|
|
total_errors = handle._total_errors
|
|
|
|
return BusInfo(
|
|
is_open=handle._is_open,
|
|
is_logger_running=handle._logger_running,
|
|
buffer_capacity=cb_capacity(handle._buffer),
|
|
buffer_fill_bytes=cb_fill_bytes(handle._buffer),
|
|
buffer_fill_percent=cb_fill_pct(handle._buffer),
|
|
buffer_overflows=cb_overflows(handle._buffer),
|
|
total_reads=total_reads,
|
|
total_writes=total_writes,
|
|
total_errors=total_errors
|
|
)
|