feat: update examples and README with streaming support and uint32 force types

- Change CForce3D fx/fy/fz from int16 to uint32 to match hardware
- Add independent C++ example with command and streaming modes
- Rewrite Python example with threaded streaming (read_loop + consumer pattern)
- Add ROS2 C++ publisher/subscriber examples
- Update README with streaming APIs, ROS2 docs, data type definitions, and full FFI table
- Add CHANGELOG
This commit is contained in:
lenn
2026-05-08 17:41:46 +08:00
parent c195234771
commit 705375085f
15 changed files with 903 additions and 149 deletions

View File

@@ -1,7 +1,7 @@
import ctypes
from ctypes import (
Structure, POINTER, c_void_p, c_char, c_char_p, c_uint8, c_uint16,
c_uint32, c_uint64, c_int16, c_bool
c_uint32, c_uint64, c_bool
)
LIB_PATH = "./libeskin_finger_sdk.so"
@@ -14,6 +14,29 @@ class EskinSdkVersion(Structure):
]
class CForce3D(Structure):
_fields_ = [
("fx", c_uint32),
("fy", c_uint32),
("fz", c_uint32),
]
class CCombinedForce(Structure):
_fields_ = [
("module", c_uint32),
("force", CForce3D),
]
class CFingerSample(Structure):
_fields_ = [
("timestamp_us", c_uint64),
("sequence", c_uint32),
("combined_force", CCombinedForce),
]
class EskinDevice:
def __init__(self):
self._lib = ctypes.CDLL(LIB_PATH)
@@ -90,6 +113,22 @@ class EskinDevice:
c_void_p, c_uint8, POINTER(c_uint16)
]
# Streaming bindings
lib.eskin_start_stream.restype = c_uint32
lib.eskin_start_stream.argtypes = [c_void_p]
lib.eskin_stop_stream.restype = c_uint32
lib.eskin_stop_stream.argtypes = [c_void_p]
lib.eskin_read_sample.restype = c_uint32
lib.eskin_read_sample.argtypes = [
c_void_p, c_uint32, POINTER(CFingerSample)
]
lib.eskin_get_mode.restype = c_uint32
lib.eskin_get_mode.argtypes = [c_void_p, POINTER(c_uint32)]
def version(self) -> tuple:
v = self._lib.eskin_version()
return (v.major, v.minor, v.patch)
@@ -210,8 +249,38 @@ class EskinDevice:
raise RuntimeError(f"write_matrix_col failed: error={err}")
return ret.value
# Streaming methods
def start_stream(self):
"""启动流式采集"""
err = self._lib.eskin_start_stream(self._handle)
if err != 0:
raise RuntimeError(f"start_stream failed: error={err}")
def stop_stream(self):
"""停止流式采集"""
err = self._lib.eskin_stop_stream(self._handle)
if err != 0:
raise RuntimeError(f"stop_stream failed: error={err}")
def read_sample(self, timeout_ms: int = 200) -> CFingerSample:
"""读取一个采样数据(流模式下调用)"""
sample = CFingerSample()
err = self._lib.eskin_read_sample(self._handle, timeout_ms, ctypes.byref(sample))
if err != 0:
raise RuntimeError(f"read_sample failed: error={err}")
return sample
def get_mode(self) -> int:
"""查询当前设备模式0=Command, 1=Streaming"""
out = c_uint32(0)
err = self._lib.eskin_get_mode(self._handle, ctypes.byref(out))
if err != 0:
raise RuntimeError(f"get_mode failed: error={err}")
return out.value
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
self.close()

View File

@@ -1,51 +1,92 @@
from eskin_ffi import EskinDevice
import time
import threading
from collections import deque
from eskin_ffi import EskinDevice, CFingerSample
def demo_command_mode(dev: EskinDevice):
"""Command 模式:读取设备信息、寄存器等"""
print("=== Command Mode ===")
hdw_ver = dev.read_hdw_version()
print(f"Hardware version: {hdw_ver}")
row = dev.read_matrix_row()
col = dev.read_matrix_col()
print(f"Matrix size: {row} x {col}")
cfg1 = dev.read_device_config1()
print(f"Device config1: 0x{cfg1:02X}")
data = dev.read_register(0x1C00, 168)
print(f"Serial number: {data.hex().upper()}")
def demo_streaming(dev: EskinDevice, duration_sec: float = 5.0):
"""Streaming 模式:持续采集力数据(参考 ROS C++ publisher"""
print("\n=== Streaming Mode ===")
# 启动流式采集
dev.start_stream()
print(f"Streaming started, will run for {duration_sec}s ...")
# 线程安全的队列(参考 ROS demo 的 read_loop + publish_callback 分离模式)
queue: deque = deque(maxlen=100)
running = True
def read_loop():
"""独立读取线程:持续从设备读取 sample"""
while running:
try:
sample = dev.read_sample(timeout_ms=50)
queue.append(sample)
except RuntimeError:
# 超时等非致命错误,继续读取
pass
# 启动读取线程
reader = threading.Thread(target=read_loop, daemon=True)
reader.start()
# 主线程:从队列取数据并打印(类似 ROS 的 publish_callback
start = time.monotonic()
count = 0
while time.monotonic() - start < duration_sec:
if queue:
sample: CFingerSample = queue.popleft()
f = sample.combined_force.force
mod = sample.combined_force.module
print(
f"[{sample.sequence:5d}] "
f"module={mod} "
f"fx={f.fx} fy={f.fy} fz={f.fz}"
)
count += 1
else:
time.sleep(0.005)
running = False
reader.join(timeout=1.0)
dev.stop_stream()
print(f"Streaming stopped. Total samples: {count}")
def main():
dev = EskinDevice()
# SDK 版本
ver = dev.version()
ver = EskinDevice().version()
print(f"ESkin SDK version: {ver[0]}.{ver[1]}.{ver[2]}")
# 打开设备
dev.open("/dev/ttyUSB0")
print("Device opened")
device_path = "/dev/ttyUSB0"
try:
# 读取硬件版本
hdw_ver = dev.read_hdw_version()
print(f"Hardware version: {hdw_ver}")
with EskinDevice() as dev:
dev.open(device_path)
print(f"Device opened: {device_path}")
# 读取矩阵尺寸
row = dev.read_matrix_row()
col = dev.read_matrix_col()
print(f"Matrix size: {row} x {col}")
demo_command_mode(dev)
demo_streaming(dev, duration_sec=5.0)
# 读取设备配置
cfg1 = dev.read_device_config1()
# cfg2 = dev.read_device_config2()
print(f"Device config1: 0x{cfg1:02X}")
# print(f"Device config2: 0x{cfg2:02X}")
print("Device closed")
# 写入矩阵尺寸示例
# ret = dev.write_matrix_row(16)
# print(f"Write matrix row: returned {ret} bytes")
# ret = dev.write_matrix_col(16)
# print(f"Write matrix col: returned {ret} bytes")
# 写入设备配置示例
# ret = dev.write_device_config1(True)
# print(f"Write device config1: returned {ret} bytes")
# ret = dev.write_device_config2(False)
# print(f"Write device config2: returned {ret} bytes")
# 原始寄存器读写
data = dev.read_register(0x1C00, 168)
print(f"Serial number: {data.hex().upper()}")
finally:
dev.close()
print("Device closed")
if __name__ == "__main__":
main()