IXNORFID Insight
← 返回洞察
工程实战2026-09-22 · 12 min

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 搭建一个低成本的多点位采集系统。

继续阅读