
简介本资源是IEC 60870-5-102电力系统电能量采集传输规约的完整Python实现方案面向电力自动化、智能电表通信及工业协议开发领域的工程师与高校研究者用于快速构建电能量数据解析、主站模拟或终端设备对接原型。压缩包共30个文件含14个核心Python脚本如processa.py、pregunta.py、historic.py等覆盖报文解析、会话管理、历史数据读取与实时采集逻辑、6个配置与说明文本、4个厂商设备实物图Landis Gyr、Circutor、Actaris等主流电表以及测试脚本、启动脚本inici.sh、许可证与README文档整体仅705KB轻量易集成。已有624人学习下载提供即开即用的协议栈基础框架包含典型应用场景下的完整调用链路、会话生命周期处理、时间同步与数据块解析逻辑并附带实测GIF动图与多轮测试用例proves.py、test.py便于理解协议交互细节与快速验证功能正确性。1. 项目概述用Python实现IEC 60870-5-102规约不是写个demo是真能进现场跑起来的通信模块IEC 60870-5-102这个编号在电力自动化圈子里一提老工程师会下意识摸摸工装口袋里的万用表——它不是实验室里摆着看的协议而是实实在在跑在水电站、变电站、配网终端设备之间的“电能量数据专用车道”。标题里那个长长的字符串“icra-tarifes_Tarifes_102_IEC60870-5_iec102协议python版_IEC60870-5-5”乍一看像一串乱码其实每个词都是关键坐标“icra-tarifes”指向土耳其ICRA公司发布的Tarifes系列电能量采集系统“102”是规约代号“IEC60870-5”是整个家族标准“python版”则点明了这次落地的技术路径。这不是用Python调个API那么简单而是要把一个严格定义帧结构、校验规则、超时机制、重传逻辑、链路管理的工业级通信协议从纸面标准变成可嵌入、可调试、可长期稳定运行的代码模块。我做过三个省级电网的电能量采集系统集成最深的体会是IEC 102协议的难点从来不在“能不能发出去”而在于“对方认不认、回不回、回得对不对、断了之后怎么续”。很多开源库只实现了基础帧格式但现场设备厂商的私有扩展、非标响应、异常报文处理、长连接保活这些“脏活累活”才是决定项目成败的关键。比如某次在云南某水电站主站发了一个读取日电量的命令从站回了个带扩展地址字段的响应标准库直接抛异常退出结果整个采集链路中断两小时——后来发现是厂家在IEC 102基础上加了两个字节的自定义标识位。这种坑光看标准文档根本找不到答案必须靠实测、抓包、逆向分析。所以这篇内容不讲协议理论不列标准条款只说我在真实项目里怎么用Python把IEC 102协议跑通、跑稳、跑久。适合两类人一是正在做电能量采集系统开发的工程师需要一个可参考的、经受过现场考验的Python实现框架二是刚接触工业协议的Python开发者想明白为什么一个“简单”的串口通信会比写个Web API复杂十倍。核心关键词就五个ICRA、IEC60870-5、IEC102、python、协议——它们不是标签而是你打开这个领域的五把钥匙。2. 协议本质与设计思路为什么不能照搬Modbus或MQTT那一套2.1 IEC 102到底是什么它和IEC 101、104有什么血缘关系先破除一个常见误解IEC 60870-5不是一个协议而是一个标准族就像“TCP/IP”是个协议栈下面包含IP、ICMP、TCP、UDP等。IEC 60870-5-101是远动控制遥控、遥信、遥测IEC 60870-5-104是它的网络版TCP/IP承载而IEC 60870-5-102是专门给“电能量计量”开的绿色通道。它的核心使命非常明确高效、可靠、无歧义地传输电度量数据有功/无功电量、最大需量、费率时段等而不是实时状态监控。这就决定了它的基因和101/104完全不同。举个生活化的例子IEC 101就像快递小哥随时待命你喊一声“送个开关命令”他立刻出发路上还不断发“已取件”、“已派件”状态IEC 104是快递小哥坐高铁速度更快但路线更固定走TCP通道而IEC 102更像是定期定点的邮政车——它不追求秒级响应但要求每天凌晨2点准时把昨天的电费账单成百上千个电表的累计电量完整、准确、不丢不乱地送到主站。所以它的帧结构设计全是围绕“批量、可靠、可追溯”展开的起始符68H固定不变长度域精确到字节控制域里有专门的“启动标志”和“结束标志”信息体地址是3字节支持65535个测量点更重要的是它强制要求“确认应答”——你发一个读命令对方必须回一个带相同地址和类型标识的响应帧否则就超时重发。这和Modbus的“发完就不管”、MQTT的“发布即成功”有本质区别。再看标题里的“icra-tarifes”这是关键线索。ICRA是土耳其一家老牌电力自动化厂商其Tarifes系列电能量采集终端在东欧、中东、北非市场占有率很高。他们的102实现并非完全遵循IEC 60870-5-102:2002标准而是做了大量工程化适配比如将标准中可选的“可变帧长”改为固定长度以简化解析在信息体中嵌入了费率标识Tariff ID用于区分峰、平、谷时段电量甚至在控制域的保留位上定义了自定义功能码。这意味着如果你只按标准文档写代码大概率连握手都失败。我第一次对接ICRA设备时就卡在控制域第4字节的“启动标志”上——标准规定是0x08但ICRA设备要求是0x0A差这两位设备直接静默。2.2 为什么选Python它真能扛住工业现场的“七十二变”看到“python”出现在工业协议标题里很多老同事第一反应是摇头“Python慢、GIL锁、实时性差搞什么搞”这话放在十年前没错但现在必须更新认知。Python在这里不是替代PLC或嵌入式C而是作为主站侧的数据汇聚、协议转换、业务逻辑中枢。它的优势恰恰在于“非实时”带来的巨大灵活性快速迭代业务规则比如电价策略变更、无缝集成AI模型预测负荷、对接各类数据库MySQL、InfluxDB、生成可视化报表Matplotlib、Plotly。一个典型的电能量采集系统架构是现场终端ICRA Tarifes→ 通信管理机串口/485→ 主站服务器Python服务→ Web前端。Python就站在这个承上启下的关键位置。但“能用”不等于“好用”。我见过太多用Python写的102模块上线三天就内存泄漏原因很简单没处理好帧缓冲区的生命周期。IEC 102通信是典型的“流式数据不定长帧”串口源源不断吐字节你的代码必须能从字节流里精准切出一帧完整的68H...68H...结尾的报文。如果用简单的readline()遇到帧头68H恰好被拆到两包中间就永远卡死。正确的做法是维护一个滚动字节缓冲区每次新数据进来就扫描缓冲区找68H起始再根据长度域判断帧是否收全。这个逻辑看似简单但涉及边界条件极多缓冲区溢出怎么办连续多个68H怎么识别有效帧头校验失败的帧如何丢弃而不影响后续这些细节直接决定了模块的健壮性。我最终采用的方案是用collections.deque做环形缓冲区最大容量设为2048字节远大于最大帧长128字节每次append新字节后触发一次find_frame()函数该函数从左到右扫描找到第一个68H读取其后第2、3字节得到长度L再检查缓冲区长度是否≥L4帧头2字节长度2字节信息体校验帧尾2字节满足则切出帧否则等待。这个设计让模块在连续高负载每秒30帧下稳定运行超过18个月零丢帧。2.3 整体架构设计分层解耦让每一层都专注自己的事一个能进现场的IEC 102 Python模块绝不能是“一个.py文件搞定所有”。我把它拆成四层每层职责清晰接口契约明确物理层Physical Layer只负责和硬件打交道。封装串口pyserial、TCP socket、甚至未来可能的光纤模块。它暴露两个方法send_bytes(data: bytes)和receive_bytes(timeout: float) - bytes。绝不碰协议字节含义只管“发出去”和“收回来”。这一层要处理底层异常串口断开自动重连、TCP连接超时重试、接收缓冲区满时的丢弃策略。链路层Link Layer这是协议的“心脏”。它消费物理层的原始字节执行帧同步、CRC16校验、地址过滤、超时重传。它向上提供send_apdu(apdu: bytes)和receive_apdu(timeout: float) - bytesAPDUApplication Protocol Data Unit就是去掉帧头帧尾、校验后的纯应用数据。这一层必须实现标准要求的“发送序号SN、接收序号RN”机制确保数据不乱序、不重复。ICRA设备特别依赖这个如果SN错一位它会直接拒绝响应。应用层Application Layer这才是真正的IEC 102逻辑。它定义所有功能码如0x01读单个电能量值0x03读日冻结电量、信息体地址编码规则、时间戳格式BCD码、数据类型32位整数、浮点数。它把业务请求如“读取ID为1001的电表昨日总电量”翻译成标准APDU再把收到的APDU解析成Python字典。这里要重点处理ICRA的扩展比如他们用信息体地址的最高字节表示费率组0x01峰0x02平0x03谷标准里没有这定义必须在这里硬编码。业务层Business Layer面向用户。提供简洁API如get_daily_energy(meter_id: int, date: str) - dict。它组合应用层的调用处理重试逻辑失败后间隔1s、2s、4s重试三次做数据缓存避免频繁读同一电表记录操作日志谁、何时、读了哪个表、耗时多少。这一层可以轻松接入Django或FastAPI对外提供HTTP接口。这种分层最大的好处是隔离变化。去年客户要求把串口升级为4G模块我只改了物理层的实现其他三层代码一行没动。今年ICRA发布了新固件修改了某个扩展功能码我只更新了应用层的APDU构造逻辑。没有这种设计每次变更都是牵一发而动全身。3. 核心细节解析与实操要点从字节到业务的每一处陷阱3.1 帧结构精解68H开头的“密码本”一个字节都不能错IEC 102的帧结构是理解一切的基础。它不像HTTP有明文头全部是二进制字节必须逐字节抠。标准帧格式如下以最常用的固定长度帧为例字段长度值说明起始符1字节0x68固定永不改变长度域L1字节N后续所有字节总数含此字节即帧长 L 4起始符重复1字节0x68再次确认防误判控制域1字节见下文关键含启动标志、功能码等地址域3字节0x00 0x00 0x01设备地址大端序ICRA常用0x000001~0x0000FF信息体可变...APDU含功能码、信息体地址、数据CRC16校验2字节...XMODEM CRC多项式0x1021初始值0x0000结束符1字节0x16固定提示ICRA设备的“长度域L”计算方式与标准略有不同。标准要求L (控制域 地址域 信息体) 字节数但ICRA的固件手册注明“L 信息体长度 5”即把控制域、地址域、校验、结束符都算进去。我第一次抓包时发现设备回的帧L12但按标准算只有9字节死活对不上翻遍文档才在附录里找到这句小字。这种细节不实测根本不知道。控制域是灵魂它1个字节8位每一位都有定义Bit7 (0x80): 启动标志Start FlagICRA要求为1Bit6 (0x40): 保留位ICRA定义为“扩展标识”1表示启用费率扩展Bit5-Bit0 (0x3F): 功能码Function Code0x01读单个值0x03读日冻结0x05读月冻结0x07读历史冻结注意ICRA的“扩展标识”位是关键开关。如果读费率电量必须置位Bit6否则设备返回空数据。这个位在标准里是保留位ICRA把它激活了这就是“iec102扩展规约”的由来。地址域3字节大端序。ICRA设备地址通常从1开始但地址0x000000有特殊含义——广播地址。发给0x000000的命令所有在线设备都会响应但主站必须能处理多个响应帧的并发解析这增加了链路层复杂度。我在一个配电房项目里就用到了广播读一次性获取20台表计的当前电量效率提升5倍但代价是链路层必须支持多帧并发处理和去重。信息体部分以读日冻结FC0x03为例字节0: 功能码 0x03字节1-3: 信息体地址3字节如0x00 0x00 0x01 表示1号表字节4-7: 日期BCD码如0x23 0x12 0x31 表示2023年12月31日字节8-11: 时分秒BCD码通常填0x00 0x00 0x00数据部分标准规定电能量值为4字节BCD码压缩十进制但ICRA部分型号支持32位整数IEEE 754 float。这就要在应用层做设备能力协商——首次连接时先发一个“读设备参数”命令FC0x00解析响应里的“数据格式标识”再决定后续读取用BCD还是INT。这个协商过程是保证兼容性的前提。3.2 CRC16校验XMODEM算法的Python实现与验证技巧CRC校验是协议可靠性的最后防线。IEC 102采用XMODEM CRC多项式0x1021初始值0x0000无反转无异或输出。网上很多Python CRC实现是错的要么多项式弄错要么初始值设成0xFFFF那是CCITT标准。正确实现如下def crc16_xmodem(data: bytes) - int: XMODEM CRC16, polynomial 0x1021, init 0x0000, no reverse, no xor crc 0x0000 for byte in data: crc ^ byte 8 for _ in range(8): if crc 0x8000: crc (crc 1) ^ 0x1021 else: crc 1 crc 0xFFFF # 保持16位 return crc但光有函数不够必须验证。我的验证方法是用ICRA设备发一个已知帧用Wireshark抓包导出原始字节然后用Python计算CRC对比抓包显示的校验值。第一次测试我的计算结果和抓包差1排查半小时才发现Wireshark默认显示的是“校验域”而XMODEM CRC计算范围是“从控制域开始到信息体结束”不包括帧头68H、长度域、帧尾16H。标准原文写得很清楚“The CRC is calculated over the Control Field, Address Field and ASDU.”但很多人误以为是整个帧。这个细节决定了你的校验是“看起来对”还是“真正对”。实操心得在链路层我加了一行日志logger.debug(fCRC calc on {data.hex()}: {crc16_xmodem(data):04x})。当模块异常时立刻能比对日志里的计算值和抓包值5分钟内定位是计算逻辑错还是数据截取范围错。这个习惯帮我节省了上百小时的调试时间。3.3 链路管理超时、重传、序号工业通信的“心跳”IEC 102的链路管理是区分“玩具代码”和“生产代码”的分水岭。它不像HTTP有Connection: keep-alive而是靠一套精巧的“发送序号SN、接收序号RN”机制维持连接。规则很简单主站发帧SN递增初始0RN填上次从站回的SN从站回帧SN填主站发来的RNRN填自己上次发的SN主站收到响应检查RN是否等于自己上次发的SN对则确认错则丢弃这套机制保证了数据不乱序、不重复。但问题来了如果从站没回呢必须超时重传。超时时间怎么定太短网络抖动就重发浪费带宽太长业务感知延迟。ICRA设备手册建议串口通信超时设为1.5秒TCP通信设为3秒。我实际项目中根据现场环境动态调整在干扰大的变电站设为2秒在光纤直连的调度中心设为1秒。重传次数也关键。标准允许最多3次。但我的经验是重传3次后仍失败大概率是设备离线或地址错误再重传只是徒劳。此时应切换策略发一个“读设备状态”命令FC0x00如果连这个都超时就标记该设备为“离线”并触发告警而不是无休止重试。这个逻辑写在业务层的重试装饰器里def retry_on_timeout(max_retries3, base_delay1.0): def decorator(func): def wrapper(*args, **kwargs): for i in range(max_retries 1): try: return func(*args, **kwargs) except TimeoutError as e: if i max_retries: # 最后一次失败查设备状态 status check_device_status(args[0]) # args[0]是设备ID if not status[online]: raise DeviceOfflineError(fDevice {args[0]} offline) else: raise e time.sleep(base_delay * (2 ** i)) # 指数退避 return None return wrapper return decorator注意指数退避Exponential Backoff是工业协议重传的黄金法则。第一次失败等1秒第二次等2秒第三次等4秒避免网络雪崩。我见过一个项目所有设备同时重传导致485总线瘫痪就是因为用了固定1秒重试。4. 实操过程与核心环节实现从零开始搭建一个可运行的模块4.1 环境准备与依赖安装避开Python生态的“经典坑”Python环境看似简单实则暗礁密布。我推荐的最小可行环境是Python 3.83.8是最后一个支持Windows XP的版本很多老旧工控机还在用pyserial 3.5串口通信注意3.4有缓冲区bugpycryptodome如果后续要加AES加密ICRA新固件支持pytest单元测试必须安装命令pip install pyserial pycryptodome pytest提示绝对不要用pip install serial这是个早已废弃的包和pyserial冲突。我曾在一个客户的Linux服务器上因为之前有人误装了serial导致import serial报错折腾半天才发现是包名冲突。正确导入永远是import serial但安装必须是pyserial。虚拟环境是生命线。工业项目常需多个Python版本共存如旧系统用3.6新模块用3.9。用venv创建隔离环境python -m venv iec102_env source iec102_env/bin/activate # Linux/Mac # iec102_env\Scripts\activate # Windows pip install --upgrade pip pip install -r requirements.txtrequirements.txt内容精简为pyserial3.5 pycryptodome3.18.0版本锁定至关重要。pyserial3.6版引入了新的异步API但破坏了旧版read()行为导致我们一个运行3年的服务突然丢帧。从此所有生产环境都严格锁定版本。4.2 物理层实现串口与TCP的统一抽象物理层的目标是让上层代码不用关心是串口还是网络。核心是定义一个抽象基类from abc import ABC, abstractmethod class PhysicalLayer(ABC): abstractmethod def send_bytes(self, data: bytes) - None: pass abstractmethod def receive_bytes(self, timeout: float 1.0) - bytes: pass abstractmethod def close(self) - None: pass串口实现serial_layer.pyimport serial import time from .base import PhysicalLayer class SerialLayer(PhysicalLayer): def __init__(self, port: str, baudrate: int 9600, timeout: float 1.0): self.port port self.baudrate baudrate self.timeout timeout self._ser None self._connect() def _connect(self): while True: try: self._ser serial.Serial( portself.port, baudrateself.baudrate, bytesizeserial.EIGHTBITS, parityserial.PARITY_NONE, stopbitsserial.STOPBITS_ONE, timeoutself.timeout, write_timeout1.0 ) break except serial.SerialException as e: logger.warning(fSerial connect failed: {e}, retry in 5s...) time.sleep(5) def send_bytes(self, data: bytes) - None: try: self._ser.write(data) except serial.SerialException as e: logger.error(fSerial write error: {e}) self._connect() # 自动重连 raise def receive_bytes(self, timeout: float 1.0) - bytes: # 重置超时避免阻塞 self._ser.timeout timeout try: # 先读1字节避免空读 first self._ser.read(1) if not first: return b # 再读剩余基于缓冲区大小 remaining self._ser.in_waiting rest self._ser.read(remaining) if remaining 0 else b return first rest except serial.SerialException as e: logger.error(fSerial read error: {e}) self._connect() raise def close(self) - None: if self._ser and self._ser.is_open: self._ser.close()TCP实现tcp_layer.pyimport socket from .base import PhysicalLayer class TCPLayer(PhysicalLayer): def __init__(self, host: str, port: int, timeout: float 3.0): self.host host self.port port self.timeout timeout self._sock None self._connect() def _connect(self): while True: try: self._sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._sock.settimeout(self.timeout) self._sock.connect((self.host, self.port)) break except (socket.timeout, socket.error) as e: logger.warning(fTCP connect failed: {e}, retry in 5s...) time.sleep(5) def send_bytes(self, data: bytes) - None: try: self._sock.sendall(data) except (socket.timeout, socket.error) as e: logger.error(fTCP send error: {e}) self._connect() raise def receive_bytes(self, timeout: float 1.0) - bytes: self._sock.settimeout(timeout) try: # 先读2字节获取长度域 len_bytes self._sock.recv(2) if len(len_bytes) 2: return b length len_bytes[1] # IEC 102长度域是第二个字节 # 再读剩余长度 remaining length 2 # 2 是帧尾16H和校验 data len_bytes self._sock.recv(remaining) return data except (socket.timeout, socket.error) as e: logger.error(fTCP receive error: {e}) self._connect() raise def close(self) - None: if self._sock: self._sock.close()实操心得串口的in_waiting属性是救命稻草。它返回接收缓冲区中未读字节数让你能“预判”后面有多少数据避免盲目read(1024)导致阻塞。TCP的recv()必须分步先读长度域再读指定长度否则网络抖动时recv(1024)可能只收到半帧解析必然失败。4.3 链路层实现帧同步与序号管理的硬核代码链路层是整个模块的“中枢神经”。link_layer.pyimport time import logging from collections import deque from typing import Optional, Tuple from .base import PhysicalLayer from .utils import crc16_xmodem logger logging.getLogger(__name__) class LinkLayer: def __init__(self, physical: PhysicalLayer, address: int 1): self.physical physical self.address address self.sn 0 # 发送序号 self.rn 0 # 接收序号 self.buffer deque(maxlen2048) # 环形缓冲区 self.timeout 1.5 # 秒 def send_apdu(self, apdu: bytes) - None: 发送APDU封装成完整帧 # 构造帧68H L 68H 控制域 地址域 APDU CRC 16H control_byte 0x80 | 0x40 | 0x01 # 启动扩展FC01 addr_bytes self._int_to_3bytes(self.address) frame_body bytes([control_byte]) addr_bytes apdu length len(frame_body) 4 # 4: CRC2 16H1 frame bytes([0x68, length, 0x68]) frame_body crc crc16_xmodem(frame_body) frame crc.to_bytes(2, little) bytes([0x16]) self.physical.send_bytes(frame) self.sn (self.sn 1) % 128 # SN 0-127循环 def receive_apdu(self, timeout: float None) - bytes: 接收APDU从字节流中解析完整帧 start_time time.time() timeout timeout or self.timeout while time.time() - start_time timeout: # 从物理层读字节追加到缓冲区 try: new_bytes self.physical.receive_bytes(0.1) # 短超时避免阻塞 if new_bytes: for b in new_bytes: self.buffer.append(b) except Exception as e: logger.debug(fPhysical read exception: {e}) continue # 扫描缓冲区找帧 frame self._find_complete_frame() if frame: # 校验 if self._validate_frame(frame): # 提取APDU去掉帧头帧尾和校验 apdu frame[6:-3] # 68HL68H控制地址 6字节CRC216H 3字节 self.rn (self.rn 1) % 128 return apdu else: logger.warning(Frame CRC invalid, drop) # 丢弃无效帧但不清空缓冲区继续找下一个68H self._skip_invalid_frame() raise TimeoutError(No valid frame received) def _find_complete_frame(self) - Optional[bytes]: 在缓冲区中找一个完整帧 buf_list list(self.buffer) for i in range(len(buf_list)): if buf_list[i] 0x68: # 找到帧头读长度域 if i 2 len(buf_list): break # 不够读长度域 length buf_list[i 1] # 检查帧长是否足够 if i length 4 len(buf_list): # 4: 68HL68H CRC216H if buf_list[i length 3] 0x16: # 结束符 return bytes(buf_list[i:i length 4]) return None def _validate_frame(self, frame: bytes) - bool: 校验帧CRC 结束符 if len(frame) 8: return False if frame[-1] ! 0x16: return False # CRC计算范围控制域到信息体结束 body frame[3:-3] # 跳过68HL68H 和 CRC216H expected_crc int.from_bytes(frame[-3:-1], little) return crc16_xmodem(body) expected_crc def _skip_invalid_frame(self): 跳过一个无效帧从下一个68H开始 buf_list list(self.buffer) for i, b in enumerate(buf_list): if b 0x68: # 从i开始清空前面的 for _ in range(i): self.buffer.popleft() break def _int_to_3bytes(self, val: int) - bytes: 整数转3字节大端序 return val.to_bytes(3, big)这段代码的核心价值在于_find_complete_frame()和_validate_frame()。前者用纯Python扫描避免正则表达式的性能开销后者严格按标准计算CRC范围。我特意把_skip_invalid_frame()单独抽出因为现场最常见的错误就是“帧错位”——由于干扰一个帧被切成两半前半截和后半截混在缓冲区里。这个函数能智能地找到下一个68H而不是暴力清空整个缓冲区保证了数据连续性。4.4 应用层与业务层让协议“活”起来的业务逻辑应用层application_layer.py定义所有功能码和数据解析from datetime import datetime from typing import Dict, Any from .link_layer import LinkLayer class ApplicationLayer: def __init__(self, link: LinkLayer): self.link link def read_daily_energy(self, meter_id: int, date: str) - Dict[str, Any]: 读取日冻结电量date格式: YYYYMMDD # 构造APDU: FC03 地址 日期(BCD) apdu bytes([0x03]) apdu self._int_to_3bytes(meter_id) # 日期BCD: 20231231 - 0x23 0x12 0x31 ymd [int(date[0:2]), int(date[2:4]), int(date[4:6])] apdu bytes([ymd[0], ymd[1], ymd[2]]) apdu bytes([0x00, 0x00, 0x00]) # 时分秒填0 # 发送 self.link.send_apdu(apdu) # 接收 resp self.link.receive_apdu() return self._parse_daily_response(resp, meter_id, date) def _parse_daily_response(self, data: bytes, meter_id: int, date: str) - Dict[str, Any]: 解析日冻结响应 if len(data) 12: raise ValueError(Response too short) # 响应APDU结构: FC 地址 日期 电量(4字节BCD) fc data[0] if fc ! 0x83: # 响应FC 命令FC 0x80 raise ValueError(fUnexpected function code: {fc: p a hrefhttps://download.csdn.net/download/weixin_42676876/26570549 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p