Files
blackbird/pybot/pybot/hd/i2c.py
T
qing bba7ee3eb4 feat(pybot): bot4cj 的 Python 迁移版(.mt 解析 + 硬件驱动 + 插件系统)
- 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;运行时零第三方依赖
2026-08-09 18:57:03 +00:00

220 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)