- parser: FSM 解析器与仓颉版逐语义对齐,修 S01/S02/S04/Q05 - robot/hardvars: 六类硬件变量工厂,修 r.cj:911 重名 FIXME、Q07 倒序释放 - plugin: Python 插件替代 Lua 桥接,修 S07 三级路径查找 - hd: i2c(每fd锁 Q03/S06)/spi/servo/kalman/mpu6050(S05/Q06)/speaker(S03 实声) - modules: PCA9685 与 Lua 版逐字节对齐;ADS1115 重写(experimental);ADS1256 空桩 - tests: 42 项 pytest 全绿(Mock 总线 journal 断言) - CLI: --dry-run / --test roundtrip / --verbose;运行时零第三方依赖
220 lines
6.4 KiB
Python
220 lines
6.4 KiB
Python
"""Linux I2C 封装 —— 移植自 bot4cj/src/hd/I2C.cj + src/cpp/I2C.c
|
||
|
||
纯标准库实现(fcntl.ioctl + os.read/write),零第三方依赖。
|
||
|
||
与仓颉版的差异(审计修复,语义不变):
|
||
S06: read_reg 返回 0..255 无符号 int(仓颉版返回有符号 Int8,
|
||
16 位拼接时符号扩展破坏低字节);
|
||
Q03: 每 fd 锁用 dict[fd → threading.Lock](仓颉版 20 槽数组 O(n) 扫描且容量受限)。
|
||
|
||
后端可切换:默认真实总线;use_mock() 切换为记录型 Mock(--dry-run 与单测共用),
|
||
Mock 的写序列 journal 是与仓颉版做逐字节 diff 的等价性验证工件(方案 §6.3)。
|
||
"""
|
||
|
||
import fcntl
|
||
import logging
|
||
import os
|
||
import threading
|
||
from contextlib import contextmanager
|
||
|
||
log = logging.getLogger("pybot.i2c")
|
||
|
||
I2C_SLAVE = 0x0703 # at least on arm64(与仓颉版一致)
|
||
|
||
|
||
class I2CError(OSError):
|
||
"""I2C 设备异常(对应仓颉版 I2cExcept 系列)"""
|
||
|
||
|
||
# ---------------- 后端(真实总线 / Mock) ----------------
|
||
|
||
class _RealBackend:
|
||
"""真实 I2C 总线:glibc open/ioctl/read/write 一层到底"""
|
||
|
||
def open(self, path):
|
||
return os.open(path, os.O_RDWR)
|
||
|
||
def close(self, fd):
|
||
os.close(fd)
|
||
|
||
def set_addr(self, fd, addr):
|
||
fcntl.ioctl(fd, I2C_SLAVE, addr)
|
||
|
||
def write_reg(self, fd, reg, val):
|
||
if os.write(fd, bytes([reg & 0xFF, val & 0xFF])) != 2:
|
||
raise I2CError(f"failed to write i2c reg 0x{reg:02X} (fd={fd})")
|
||
|
||
def read_reg(self, fd, reg):
|
||
# 读取寄存器之前要先写寄存器地址
|
||
if os.write(fd, bytes([reg & 0xFF])) != 1:
|
||
raise I2CError(f"failed to select i2c reg 0x{reg:02X} (fd={fd})")
|
||
data = os.read(fd, 1)
|
||
if len(data) != 1:
|
||
raise I2CError(f"failed to read i2c reg 0x{reg:02X} (fd={fd})")
|
||
return data[0] # 0..255 无符号(修复 S06)
|
||
|
||
def write_block(self, fd, reg, data):
|
||
"""向 reg 起连写多字节(ADS1115 等 16 位寄存器设备)"""
|
||
buf = bytes([reg & 0xFF]) + bytes(data)
|
||
if os.write(fd, buf) != len(buf):
|
||
raise I2CError(f"failed to write i2c block at reg 0x{reg:02X} (fd={fd})")
|
||
|
||
def read_block(self, fd, reg, n):
|
||
"""从 reg 起连读 n 字节(自动递增寄存器,MPU6050 一次读 14 字节)"""
|
||
if os.write(fd, bytes([reg & 0xFF])) != 1:
|
||
raise I2CError(f"failed to select i2c reg 0x{reg:02X} (fd={fd})")
|
||
data = os.read(fd, n)
|
||
if len(data) != n:
|
||
raise I2CError(f"short read at i2c reg 0x{reg:02X}: "
|
||
f"{len(data)}/{n} bytes (fd={fd})")
|
||
return data
|
||
|
||
|
||
class MockBackend:
|
||
"""记录型 Mock 总线:不碰硬件,记录全部操作序列供断言/diff"""
|
||
|
||
def __init__(self):
|
||
self.journal = [] # [(op, ...), ...] 全部操作按序记录
|
||
self.reg_values = {} # {(fd, reg): int} 单寄存器读注入
|
||
self.block_values = {} # {(fd, reg): bytes} 块读注入
|
||
self._next_fd = 1000
|
||
|
||
def open(self, path):
|
||
fd = self._next_fd
|
||
self._next_fd += 1
|
||
self.journal.append(("open", path, fd))
|
||
return fd
|
||
|
||
def close(self, fd):
|
||
self.journal.append(("close", fd))
|
||
|
||
def set_addr(self, fd, addr):
|
||
self.journal.append(("set_addr", fd, addr))
|
||
|
||
def write_reg(self, fd, reg, val):
|
||
self.journal.append(("write_reg", fd, reg & 0xFF, val & 0xFF))
|
||
|
||
def read_reg(self, fd, reg):
|
||
self.journal.append(("read_reg", fd, reg))
|
||
return self.reg_values.get((fd, reg), 0) & 0xFF
|
||
|
||
def write_block(self, fd, reg, data):
|
||
self.journal.append(("write_block", fd, reg & 0xFF, bytes(data)))
|
||
|
||
def read_block(self, fd, reg, n):
|
||
self.journal.append(("read_block", fd, reg, n))
|
||
return self.block_values.get((fd, reg), bytes(n))
|
||
|
||
|
||
_backend = _RealBackend()
|
||
|
||
|
||
def use_mock():
|
||
"""切换到 Mock 总线(--dry-run / 单测),返回 MockBackend 以便读取 journal"""
|
||
global _backend
|
||
_backend = MockBackend()
|
||
return _backend
|
||
|
||
|
||
def use_real():
|
||
"""切换回真实总线"""
|
||
global _backend
|
||
_backend = _RealBackend()
|
||
|
||
|
||
# ---------------- I2C 锁具(每 fd 一把,修复 Q03) ----------------
|
||
# 同一 fd 上"设地址 + 读/写寄存器"必须是原子序列(多设备共享总线)
|
||
|
||
_MAIN_LOCK = threading.Lock() # 主锁,操作锁具注册表时使用
|
||
_LOCKS = {} # {fd: threading.Lock}
|
||
|
||
|
||
def _register_lock(fd):
|
||
with _MAIN_LOCK:
|
||
_LOCKS[fd] = threading.Lock()
|
||
|
||
|
||
def _unregister_lock(fd):
|
||
with _MAIN_LOCK:
|
||
_LOCKS.pop(fd, None)
|
||
|
||
|
||
@contextmanager
|
||
def transaction(fd):
|
||
"""with transaction(fd): 持有该 fd 的总线锁,保证 I2C 操作序列原子"""
|
||
with _MAIN_LOCK:
|
||
lock = _LOCKS.get(fd)
|
||
if lock is None:
|
||
raise I2CError(f"no i2c lock for fd {fd} (device not opened by pybot?)")
|
||
with lock:
|
||
yield
|
||
|
||
|
||
# ---------------- 模块级函数(供插件 modules/*.py 直接调用) ----------------
|
||
# 对应仓颉版注册给 Lua 的 i2c_open/i2c_close/i2c_set_addr/i2c_write_reg/i2c_read_reg
|
||
|
||
def open_dev(path):
|
||
fd = _backend.open(path)
|
||
_register_lock(fd)
|
||
log.debug("i2c open %s -> fd=%d", path, fd)
|
||
return fd
|
||
|
||
|
||
def close_dev(fd):
|
||
_unregister_lock(fd)
|
||
_backend.close(fd)
|
||
log.debug("i2c close fd=%d", fd)
|
||
|
||
|
||
def set_addr(fd, addr):
|
||
_backend.set_addr(fd, addr)
|
||
|
||
|
||
def write_reg(fd, reg, val):
|
||
_backend.write_reg(fd, reg, val)
|
||
|
||
|
||
def read_reg(fd, reg):
|
||
return _backend.read_reg(fd, reg)
|
||
|
||
|
||
def write_block(fd, reg, data):
|
||
_backend.write_block(fd, reg, data)
|
||
|
||
|
||
def read_block(fd, reg, n):
|
||
return _backend.read_block(fd, reg, n)
|
||
|
||
|
||
# ---------------- I2C 总线对象(对应仓颉版 I2C_Orig) ----------------
|
||
|
||
class I2CBus:
|
||
"""持有一个 I2C 设备文件句柄;close() 幂等"""
|
||
|
||
def __init__(self, path):
|
||
self.path = path
|
||
self.fd = open_dev(path)
|
||
|
||
def close(self):
|
||
if self.fd >= 0:
|
||
close_dev(self.fd)
|
||
self.fd = -1
|
||
|
||
def set_addr(self, addr):
|
||
set_addr(self.fd, addr)
|
||
|
||
def write_reg(self, reg, val):
|
||
write_reg(self.fd, reg, val)
|
||
|
||
def read_reg(self, reg):
|
||
return read_reg(self.fd, reg)
|
||
|
||
def write_block(self, reg, data):
|
||
write_block(self.fd, reg, data)
|
||
|
||
def read_block(self, reg, n):
|
||
return read_block(self.fd, reg, n)
|
||
|
||
def transaction(self):
|
||
return transaction(self.fd)
|