KLMB100/200 开发指南:从 SDK 到生产代码
翻遍 SDK V4.0 文档,拆解 KLMB100/200 的协议结构、API 调用、缓冲区解析。不是官网参数表,是真实能跑的代码。
前七篇文章里,我们一直在用 KL9700 当主角。但很多客户实际拿到的是 KLMB100 或 KLMB200——更小巧、更便宜、直接嵌入产线的那种。
问题是:这俩玩意儿的 SDK 长什么样?怎么写代码才能稳定跑?
今天这篇文章,我把 SDK V4.0 翻了一遍,把协议结构、API 调用、缓冲区解析全拆给你看。不是官网参数表,是真实能跑的代码。
一、硬件全景:KLMB100 vs KLMB200
先说结论:KLMB100 和 KLMB200 用的是同一套 SDK,同一套协议,同一组 DLL。区别只在物理形态和天线配置。
# KLMB100/KLMB200 硬件规格(来自 SDK 文档)
hardware:
klmb100:
form_factor: "小型嵌入式模块"
antenna_ports: 1
rf_power_max: "26 dBm"
interfaces:
- "TCP/IP (RJ45)"
- "USB (HID)"
- "RS232"
use_cases:
- "产线单点采集"
- "工位打卡"
- "小型设备集成"
klmb200:
form_factor: "工业级读写器"
antenna_ports: 2
rf_power_max: "26 dBm"
interfaces:
- "TCP/IP (RJ45)"
- "USB (HID)"
- "RS232/RS485"
- "WIFI (可选)"
- "4G (可选)"
use_cases:
- "多天线覆盖"
- "仓储出入口"
- "远距离识别"
common:
protocol: "SWNetApi / SWComApi / SWHidApi"
buffer_size: 9182
default_ip: "192.168.1.250"
default_port: 60000
tag_types:
- "EPC (0x01)"
- "TID (0x02)"
frequency_bands:
- "US (920-925 MHz)"
- "EU (865-868 MHz)"
- "CN (920-925 MHz)"二、三种连接方式:TCP / COM / USB
SDK 提供了三套 DLL,对应三种物理接口。但它们的 API 命名几乎一模一样——只是前缀不同。
# 三种连接方式的 DLL 和 API 对照
"""
TCP (网线/WIFI/4G): SWNetApi.dll → SWNet_OpenDevice
COM (串口): SWComApi.dll → SWCom_OpenDevice
USB (HID): SWHidApi.dll → SWHid_OpenDevice
"""
import ctypes
from ctypes import byref, c_int
# === 方式一:TCP 连接(最常用)===
def connect_tcp(ip="192.168.1.250", port=60000):
dll = ctypes.windll.LoadLibrary("SWNetApi.dll")
ret = dll.SWNet_OpenDevice(ip.encode(), port)
if ret == 1:
print("TCP 连接成功")
return dll
else:
print("TCP 连接失败")
return None
# === 方式二:COM 串口 ===
def connect_com(com_port="COM4", baudrate=115200):
dll = ctypes.windll.LoadLibrary("SWComApi.dll")
ret = dll.SWCom_OpenDevice(com_port.encode(), baudrate)
if ret == 1:
print("COM 连接成功")
return dll
else:
print("COM 连接失败")
return None
# === 方式三:USB HID ===
def connect_usb(device_index=0):
dll = ctypes.windll.LoadLibrary("SWHidApi.dll")
# 先检测 USB 设备数量
count = dll.SWHid_GetUsbCount()
if count == 0:
print("未检测到 USB 设备")
return None
ret = dll.SWHid_OpenDevice(device_index)
if ret == 1:
print("USB 连接成功")
return dll
else:
print("USB 连接失败")
return None三、缓冲区解析:9182 字节里的秘密
不管用哪种连接方式,读到的数据都是同一个格式:一个 9182 字节的缓冲区,里面塞着若干条标签记录。
每条记录的结构是这样的:
# 缓冲区结构(每条标签记录)
┌─────────────┬──────────┬─────────┬──────────────┬──────────┐
│ PackLength │ Type │ Ant │ TagID... │ RSSI │
│ (1 byte) │ (1 byte) │(1 byte)│ (N bytes) │ (1 byte) │
└─────────────┴──────────┴─────────┴──────────────┴──────────┘
字段说明:
- PackLength: 本条记录的总长度(不含自身)
- Type: 标签类型(0x01=EPC, 0x02=TID, 0x81=EPC+时间戳)
- Ant: 天线端口号(1-4)
- TagID: 标签内容(EPC 或 TID,长度可变)
- RSSI: 信号强度(负值,需转换)# 完整的缓冲区解析代码
import ctypes
from ctypes import byref, c_int
import time
def read_tags(dll, duration=10):
"""
持续读取标签,直到超时
Args:
dll: 已加载的 DLL 对象
duration: 读取时长(秒)
"""
# 清空缓冲区
dll.SWNet_ClearTagBuf()
start_time = time.time()
tag_count = 0
while time.time() - start_time < duration:
# 分配 9182 字节缓冲区
arrBuffer = bytes(9182)
iTagLength = c_int(0)
iTagNumber = c_int(0)
# 读取缓冲区
ret = dll.SWNet_GetTagBuf(arrBuffer, byref(iTagLength), byref(iTagNumber))
if iTagNumber.value > 0:
iLength = 0
# 遍历每条标签记录
for i in range(iTagNumber.value):
# 读取 PackLength
bPackLength = arrBuffer[iLength]
# 读取 Type 和 Ant
tag_type = arrBuffer[1 + iLength]
ant_num = arrBuffer[1 + iLength + 1]
# 读取 TagID(可变长度)
tag_id_bytes = []
for j in range(2, bPackLength - 1):
tag_id_bytes.append(arrBuffer[1 + iLength + j])
tag_id = ''.join(f'{b:02X}' for b in tag_id_bytes)
# 读取 RSSI
rssi_raw = arrBuffer[1 + iLength + bPackLength - 1]
rssi = rssi_raw - 256 if rssi_raw > 127 else rssi_raw
# 输出
type_str = "EPC" if tag_type == 0x01 else "TID" if tag_type == 0x02 else f"0x{tag_type:02X}"
print(f"[{type_str}] Ant:{ant_num} Tag:{tag_id} RSSI:{rssi}dBm")
tag_count += 1
# 移动到下条记录
iLength = iLength + bPackLength + 1
time.sleep(0.1) # 避免 CPU 空转
print(f"\n总计读取 {tag_count} 条标签")
return tag_count四、主动模式 vs 应答模式
KLMB100/200 有两种工作模式,决定了谁来控制"读"这个动作。
# 两种工作模式对比
work_modes:
active_mode:
description: "上电后自动连续读标签,数据主动推送到指定接口"
trigger: "设备自动"
data_flow: "设备 → 主机(推送)"
use_cases:
- "产线持续采集"
- "仓储出入口监控"
- "实时盘点"
config: "ReaderSoft → ParameterSet → WorkMode → ActiveMode"
answer_mode:
description: "上电后不读标签,等待主机发送读命令,每命令读一次"
trigger: "主机命令"
data_flow: "主机请求 → 设备响应"
use_cases:
- "按需读取"
- "功耗敏感场景"
- "精确控制采集时机"
config: "ReaderSoft → ParameterSet → WorkMode → AnswerMode"
# TCP 命令协议(直接发指令控制)
tcp_commands:
header: [0x53, 0x57] # "SW"
format: "Header + Length + CMD + Data + Checksum"
examples:
set_active: [0x53, 0x57, 0x00, 0x05, 0xFF, 0x24, 0x02, 0x01, 0x2B]
set_answer: [0x53, 0x57, 0x00, 0x05, 0xFF, 0x24, 0x02, 0x00, 0x2C]
set_power_26dbm: [0x53, 0x57, 0x00, 0x05, 0xFF, 0x24, 0x05, 0x1A, 0x24]
set_power_7dbm: [0x53, 0x57, 0x00, 0x05, 0xFF, 0x24, 0x05, 0x07, 0x22]
set_freq_us: [0x53, 0x57, 0x00, 0x05, 0xFF, 0x3F, 0x31, 0x80, 0x62]
set_freq_eu: [0x53, 0x57, 0x00, 0x05, 0xFF, 0x3F, 0x4E, 0x00, 0xC5]五、高级功能:掩码、继电器、时间戳
除了基本的读标签,KLMB100/200 还支持一些实用功能。
# 高级功能配置
"""
1. 掩码过滤:只读取特定前缀的标签
2. 继电器控制:读到标签后触发外部设备
3. 时间戳:每条标签附带读取时间
"""
# === 掩码过滤 ===
"""
配置方式:ReaderSoft → AdvanceSet → Mask
- 开启 Mask 功能
- Start(Hex): 起始地址(通常设为 0)
- MaskData(Hex): 要匹配的前缀
示例:只读取 1122 开头的标签
Start = 0
MaskData = 1122
效果:
✓ 112233445566778899AABB (匹配)
✗ 2233445566778899AABB00 (不匹配)
"""
# === 继电器控制 ===
"""
硬件接线:
COM → 公共端
KOFF → 默认断开(继电器释放时连通)
KON → 默认连通(继电器释放时断开)
自动模式:
ReaderSoft → AdvanceSet → Relay
- 开启 Relay
- ValidTime = 3(秒)
效果:每次读到标签,继电器闭合 3 秒后自动断开
SDK 控制:
dll.SWNet_RelayOn() # 闭合继电器
dll.SWNet_RelayOff() # 释放继电器
"""
# === 时间戳模式 ===
"""
当 Type = 0x81 时,TagID 后面多 6 字节时间戳
缓冲区结构变化:
正常: [PackLen][Type=0x01][Ant][TagID...][RSSI]
时间戳: [PackLen][Type=0x81][Ant][TagID...][Timestamp 6B][RSSI]
时间戳格式:Unix 时间戳(秒)
"""
def parse_tag_with_timestamp(arrBuffer, iLength, bPackLength):
"""解析带时间戳的标签"""
tag_type = arrBuffer[1 + iLength]
if tag_type == 0x81:
# 带时间戳
# TagID 长度 = bPackLength - 1(Type) - 1(Ant) - 6(Timestamp) - 1(RSSI)
tag_len = bPackLength - 9
tag_id = ''.join(f'{arrBuffer[1 + iLength + 2 + j]:02X}'
for j in range(tag_len))
# 时间戳(最后 6 字节,跳过 RSSI)
ts_bytes = arrBuffer[1 + iLength + 2 + tag_len : 1 + iLength + 2 + tag_len + 6]
timestamp = int.from_bytes(ts_bytes, 'big')
rssi = arrBuffer[1 + iLength + bPackLength - 1]
rssi = rssi - 256 if rssi > 127 else rssi
return {
'type': 'EPC+TS',
'tag_id': tag_id,
'timestamp': timestamp,
'rssi': rssi
}
return None # 非时间戳模式六、生产环境代码:断线重连 + 异常处理
SDK 文档里提到一个关键特性:TCP 断线后自动重连。但代码层面还是要做好异常处理。
# 生产级 KLMB100/200 客户端
import ctypes
from ctypes import byref, c_int
import time
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class KLMReader:
"""KLMB100/200 读写器客户端"""
def __init__(self, ip="192.168.1.250", port=60000):
self.ip = ip
self.port = port
self.dll = None
self.connected = False
def connect(self):
"""建立连接"""
try:
self.dll = ctypes.windll.LoadLibrary("SWNetApi.dll")
ret = self.dll.SWNet_OpenDevice(self.ip.encode(), self.port)
if ret == 1:
self.connected = True
logger.info(f"已连接到 {self.ip}:{self.port}")
# 清空缓冲区
self.dll.SWNet_ClearTagBuf()
return True
else:
logger.error("连接失败")
return False
except Exception as e:
logger.error(f"连接异常: {e}")
return False
def read_loop(self, callback, duration=None):
"""
持续读取标签
Args:
callback: 每读到标签时的回调函数 callback(tag_data)
duration: 读取时长(秒),None 表示无限
"""
if not self.connected:
raise RuntimeError("未连接")
start_time = time.time()
while True:
# 检查超时
if duration and (time.time() - start_time) > duration:
break
try:
arrBuffer = bytes(9182)
iTagLength = c_int(0)
iTagNumber = c_int(0)
ret = self.dll.SWNet_GetTagBuf(
arrBuffer,
byref(iTagLength),
byref(iTagNumber)
)
if iTagNumber.value > 0:
iLength = 0
for i in range(iTagNumber.value):
bPackLength = arrBuffer[iLength]
# 解析字段
tag_type = arrBuffer[1 + iLength]
ant_num = arrBuffer[1 + iLength + 1]
# TagID
tag_id_bytes = []
for j in range(2, bPackLength - 1):
tag_id_bytes.append(arrBuffer[1 + iLength + j])
tag_id = ''.join(f'{b:02X}' for b in tag_id_bytes)
# RSSI
rssi_raw = arrBuffer[1 + iLength + bPackLength - 1]
rssi = rssi_raw - 256 if rssi_raw > 127 else rssi_raw
# 构造数据
tag_data = {
'type': tag_type,
'antenna': ant_num,
'epc': tag_id,
'rssi': rssi,
'timestamp': time.time()
}
# 回调
callback(tag_data)
# 移动到下条
iLength = iLength + bPackLength + 1
time.sleep(0.05) # 50ms 轮询间隔
except Exception as e:
logger.error(f"读取异常: {e}")
# 尝试重连
self.connected = False
if not self._try_reconnect():
time.sleep(5) # 重连失败,等待 5 秒
return True
def _try_reconnect(self, max_retries=3):
"""尝试重连"""
for i in range(max_retries):
logger.info(f"尝试重连 ({i+1}/{max_retries})...")
if self.connect():
return True
time.sleep(2)
return False
def close(self):
"""关闭连接"""
if self.dll:
try:
self.dll.SWNet_CloseDevice()
except:
pass
self.connected = False
logger.info("已断开连接")
# === 使用示例 ===
def on_tag_detected(tag):
"""标签检测回调"""
print(f"[Ant{tag['antenna']}] {tag['epc']} ({tag['rssi']}dBm)")
if __name__ == "__main__":
reader = KLMReader("192.168.1.250", 60000)
if reader.connect():
try:
# 持续读取 60 秒
reader.read_loop(on_tag_detected, duration=60)
except KeyboardInterrupt:
logger.info("用户中断")
finally:
reader.close()七、KLMB100/200 vs KL9700:怎么选?
最后做个对比,帮你选型。
# KLMB100/200 vs KL9700 选型指南
comparison:
klmb100:
pros:
- "体积小,易嵌入"
- "价格低"
- "SDK 简单,上手快"
cons:
- "单天线,覆盖有限"
- "无 GPIO"
- "无 MQTT/HTTP"
best_for:
- "产线单点采集"
- "设备集成"
- "成本敏感项目"
klmb200:
pros:
- "双天线,覆盖更广"
- "支持 WIFI/4G"
- "继电器输出"
cons:
- "体积较大"
- "价格中等"
best_for:
- "仓储出入口"
- "多点位采集"
- "需要无线联网"
kl9700:
pros:
- "四天线,覆盖最大"
- "GPIO 丰富"
- "MQTT/HTTP/Modbus"
- "工业级防护"
cons:
- "体积最大"
- "价格最高"
best_for:
- "大型仓储"
- "复杂工业环境"
- "需要协议对接"
sdk_compatibility:
note: "三者 SDK 完全兼容"
shared_features:
- "相同的缓冲区结构"
- "相同的 API 命名"
- "相同的协议格式"
migration: "代码从 KLMB100 迁移到 KL9700,只需改 DLL 路径"八、结语
KLMB100/200 的 SDK 设计得很朴素——三种接口、一套协议、一个缓冲区。没有花哨的功能,但足够稳定。
如果你要做产线集成,KLMB100 足够;如果要覆盖更大区域,KLMB200 的双天线更合适;如果需要 MQTT 或更多 GPIO,上 KL9700。
代码层面,记住三点:9182 字节缓冲区、PackLength 变长解析、断线自动重连。做到这三点,就能稳定跑起来。
下一篇文章,我们讲讲怎么用 KLMB100/200 搭建一个低成本的多点位采集系统。