# -*- coding: utf-8 -*- # !/usr/bin/env python # 换电贵业务流程整理 # 1. 用户扫码,服务器开启指定端口 # 2. 用户将电池放入柜子中,同时关闭门锁,主板检测门锁关闭后,上报相应报文 # 3. 服务器收到指令之后 需要检测 电池是否已经成功连接充电器 ,避免空仓的情况发生 # 4. 检测空仓 的方法是读取该端口的电池IMEI ,主板会有延迟,所以一次没有读到需要继续再读取一次 最多三次没有读取到就算是没有放入电池 # 5. 检测到电池的情况下,发送开锁指令,打开一个有电池的柜门 # 6. 用户关闭柜门, 主板再次上报,完成一次换电操作 # 注意事项 # 1. 整个换电可拆解为两次独立的开关门事件 即 开门--关门--开门--关门 # 2. 目前并没有比较可靠的方式将两次事件关联起来,完全依赖服务器的缓存以及订单的存储信息,实际最好的方式是在发送串口数据的时候携带订单号,但是目前主板并没有做到这一点 # 3. 安骑科技会有一个单独的电池管理系统,该系统下电池编号 用户ID 代理商ID 构成唯一的一条数据,也就是说一个用户一个经销商下,同一时间只可能拥有一块电池 # 4. 初代主板问题比较大,经常会出现明明电池放进去了,但是还是没有读到电池的情况。更换主板后情况得到缓解,但需要注意,目前还有少量的初代设备正在运行 import datetime import logging from apps.web.core.adapter.base import SmartBox, fill_2_hexByte from apps.web.core.exceptions import ServiceException from apps.web.core.networking import MessageSender from apps.web.dealer.models import Dealer from apps.web.device.models import Battery, Device, Group from apps.web.user.models import MyUser from taskmanager.mediator import task_caller from apps.web.constant import MQTT_TIMEOUT, DeviceCmdCode, Const logger = logging.getLogger(__name__) class AnQiBox(SmartBox): DOOR_STATUS_MAP = { "00": "opened", "01": "closed" } DEFAULT_CAN_USE_VOLTAGE = 65 DEFAULT_MAX_VOLTAGE = 75 DEFAULT_MIN_VOLTAGE = 10 DEFAULT_VOICE_NUM = 1 DEFAULT_REPAIR_DOOR = 11 DEFAULT_REVERSE_LOCK_STATUS = False MAX_NO_LOAD_ELEC = 0.5 INVALID_BATTERY_SN_HEX = "".join(["0" for r in xrange(48)]) BATTERY_SN_ASCII = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" def __init__(self, device): super(AnQiBox, self).__init__(device) self._devNo = device.get("devNo") self._portStatus = None def _send_data(self, funCode, data = None, cmd = None, timeout = MQTT_TIMEOUT.NORMAL): """ 发送报文函数 :return: """ if cmd is None: cmd = DeviceCmdCode.OPERATE_DEV_SYNC if data is None: data = "" result = MessageSender.send(device = self.device, cmd = cmd, payload = { "IMEI": self._devNo, "funCode": funCode, "data": data }, timeout = timeout) if result.has_key('rst') and result['rst'] != 0: if result['rst'] == -1: raise ServiceException({'result': 2, 'description': u"网络故障"}) elif result['rst'] == 1: raise ServiceException({'result': 2, 'description': u'主板无响应'}) return result def _open_door(self, port): """ 发送指令, 开启柜门 :param port: :return: """ try: self._turn_off_power(port) except Exception as e: pass portHex = fill_2_hexByte(hex(int(port)), 2) result = self._send_data("82", portHex, timeout = MQTT_TIMEOUT.START_DEVICE) data = result.get("data", "") if data[14: 16] != "00" or data[18: 20] != "00": return return result def _query_door_status(self): """ 查询所有门锁的状态 :return: """ doorStatus = list() result = self._send_data("84") data = result.get("data", "") if data[12:14] != "00": return doorStatus lockNum = int(data[14: 16], 16) needData = data[16:-2] for i in xrange(lockNum): portStr = str(i + 1) doorStatus.append({ portStr: self.DOOR_STATUS_MAP.get(needData[:2]) }) needData = needData[2:] return doorStatus def _query_all_status(self): """ 查询所有的信息, 包括电压、电量、锁状态、电池SN号 :return: """ statusDict = dict() result = self._send_data("78") data = result.get("data", "") if data[14:16] != "00": return statusDict batteryNum = int(data[16:18], 16) needData = data[18: -2] for i in range(batteryNum): portStr = str(i + 1) batterySn = needData[8:56] if batterySn == self.INVALID_BATTERY_SN_HEX: batterySn = "" else: batterySn = needData[8:56 - 2].decode("hex")[8:] # 添加上电量百分比 为了不影响其他的逻辑 换个字段名称 statusDict[portStr] = { "voltage": int(needData[: 4], 16) / 100.0, "elec": int(needData[4:6], 16), "doorStatus": self.DOOR_STATUS_MAP.get(needData[6:8]), "batteryImei": batterySn, } needData = needData[56:] return statusDict def _query_battery_imei(self, port): """ 查询电池的 IMEI 号码 :return: """ imei = "" portHex = fill_2_hexByte(hex(int(port)), 2) result = self._send_data("77", portHex) data = result.get("data") if data[14:16] != "00": return imei return self.translate_battery_imei(data[20: -2].decode("hex")[8:]) def _query_elec(self): """ 查询当前的充电电流 :return: """ result = self._send_data("72") data = result["data"] if data[14: 16] != "00": # 读取电流信息失败 raise ServiceException({'result': 2, 'description': u'无响应信息'}) allPorts = int(data[16: 18], 16) statusDict = dict() for i in xrange(allPorts): portStr = str(i + 1) electric = int(data[18 + (i * 4):18 + (i * 4) + 4], 16) electric /= 1000.0 chargeStatus = u"充电中" if electric > self.MAX_NO_LOAD_ELEC else u"停止充电" statusDict[portStr] = {"outputElec": "{:.2f}".format(electric), "chargeStatus": chargeStatus} return statusDict def _pay_voice(self): """ 播报 音乐 :return: """ voiceNum = self._device.get("otherConf", dict()).get("voiceNum", self.DEFAULT_VOICE_NUM) voiceHex = fill_2_hexByte(hex(int(voiceNum)), 2) self._send_data("75", voiceHex, DeviceCmdCode.OPERATE_DEV_NO_RESPONSE) def _turn_on_power(self, port): """ 开启充电 :return: """ portHex = fill_2_hexByte(hex(int(port)), 2) timeHex = "0258" self._send_data("70", portHex + timeHex, cmd = DeviceCmdCode.OPERATE_DEV_NO_RESPONSE) def _get_charge_limit(self): """ 读取充电电流限制 :return: """ result = self._send_data("76") data = result.get("data", "") if data[14:16] != "00": raise ServiceException({"result": 2, "description": u"读取充电电流限制失败"}) maxElec = int(data[16:20], 16) minElec = int(data[20:24], 16) return { "maxElec": maxElec, "minElec": minElec } def _turn_off_power(self, port): """ 关闭充电 :return: """ portHex = fill_2_hexByte(hex(int(port)), 2) self._send_data("71", portHex, cmd = DeviceCmdCode.OPERATE_DEV_NO_RESPONSE) def _set_charge_limit(self, maxElec, minElec): """ 设置充电电流限制 :param maxElec: :param minElec: :return: """ if maxElec and int(maxElec) > 10000: raise ServiceException({"result": 2, "description": u"可设置的最大充电电流为10000mA"}) if minElec and int(minElec) < 0: raise ServiceException({"result": 2, "description": u"可设置的最小充电电流为0mA"}) data = "" data += fill_2_hexByte(hex(int(maxElec))) data += fill_2_hexByte(hex(int(minElec))) result = self._send_data("74", data) if result.get("data", "")[14:16] != "00": raise ServiceException({"result": 2, "description": u"设置充电电流限制失败"}) def _query_all_battery_imei(self): """ 暂定 batteryImei 是 15位 :return: """ data = self._query_all_status() batteryInfo = dict() for port, item in data.items(): batteryImei = item.get("batteryImei") if Battery.is_battery_sn_format(batteryImei): batteryInfo[port] = batteryImei return batteryInfo def _check_port_status(self, portStr, portInfo): """ 根据 现在端口的内部信息(有无电池) 判定当前的端口状态 有电池为繁忙/无电池为空闲 :param portStr: :param portInfo: :return: """ # 先从设备缓存中读取被锁的设备端口列表 lockPorts = self.device.get("otherConf", dict()).get("lockPorts", list()) if portStr in lockPorts: return Const.DEV_WORK_STATUS_FAULT # 有电池的判定为繁忙 没有电池的判定为空闲 if portInfo.get("batteryImei"): return Const.DEV_WORK_STATUS_WORKING else: return Const.DEV_WORK_STATUS_IDLE @staticmethod def _is_no_elec_port(portInfo): """ 根据门锁状态以及电池状态 获取 可开启的仓门 :param portInfo: :return: """ doorStatus = portInfo.get("doorStatus") batteryImei = portInfo.get("batteryImei") if doorStatus == "closed" and not batteryImei: return True else: return False def _is_can_use_port(self, portStr, portInfo): """ 检测 端口的电压是否可用 :param portStr: :param portInfo: :return: """ otherConf = self.device.get("otherConf", dict()) lockPorts = otherConf.get("lockPorts", list()) if portStr in lockPorts: return False, "", 0 canUseVoltage = otherConf.get("canUseVoltage", self.DEFAULT_CAN_USE_VOLTAGE) voltage = portInfo.get("voltage") batteryImei = portInfo.get("batteryImei") # 用户拿电池的时候判断一下看是否被禁用 被禁用的电池直接跳过 battery = Battery.get_one(self.device.ownerId, batteryImei) if battery and battery.disable: return False, "", voltage if all([voltage, batteryImei]) and voltage > float(canUseVoltage): return True, batteryImei, voltage else: return False, "", voltage def _on_point_start(self, chargeIndex): """ 经销商远程上分 :return: """ otherConf = self._device.get("otherConf", dict()) devCache = Device.get_dev_control_cache(self._devNo) currentUseInfo = devCache.get("currentUseInfo") if currentUseInfo and "orderNo" in currentUseInfo: raise ServiceException({'result': 2, 'description': u"用户正在使用, 请等待用户使用完成之后再远程上分"}) result = self._open_door(chargeIndex) if not result: raise ServiceException({'result': 2, 'description': u'柜门开启失败,请重新试试'}) # 经销商远程上分之后 也把设备锁死,需要清空一次信息 currentUseInfo = { "openId": "dealer", "chargeIndex1": chargeIndex } Device.update_dev_control_cache(self._devNo, {"currentUseInfo": currentUseInfo}) otherConf.update({"nextPort": chargeIndex}) Device.objects.filter(devNo = self._devNo).update(otherConf = otherConf) Device.invalid_device_cache(self._devNo) return def _battery_voltage_warning(self, port, voltage, minVoltage, maxVoltage, batteryImei): """ 电池电压故障报警给经销商 :param port: 端口号 :param voltage: 电池当前电压 :param minVoltage: 允许最大电压 :param maxVoltage: 允许最小电压 :param batteryImei: 电池IMEI :return: """ dealer = Dealer.objects.get(id = self._device["ownerId"]) if not dealer or not dealer.managerialOpenId: return group = Group.get_group(self._device["groupId"]) notifyData = { "title": "设备当前电池<{}>电压不正常".format(batteryImei), "device": u"{gNum}组-{lc}-{port}端口".format(gNum = self._device["groupNumber"], lc = self._device["logicalCode"], port = port), "location": u"{address}-{groupName}".format(address = group["address"], groupName = group["groupName"]), "fault": u"当前电压:{},允许最大电压:{},允许最小电压:{}".format(voltage, maxVoltage, minVoltage), "notifyTime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") } task_caller( func_name = 'report_to_dealer_via_wechat', openId = dealer.managerialOpenId, dealerId = str(dealer.id), templateName = "device_fault", **notifyData ) @property def no_elec_door_port(self): """ 一定是直接从设备设置里面读取, 经销商远程上分的时候会重置下一个端口, 第一次一定是经销商上分 :return: """ portStatus = self.get_port_status_from_dev() # 这个地方先存入缓存, 鉴别电池电压之后立即删除 self._portStatus = portStatus otherConf = self._device.get("otherConf", dict()) nextPort = otherConf.get("nextPort") lockPorts = self.device.get("otherConf", dict()).get("lockPorts", list()) # 双重校验 防止出错 if nextPort not in lockPorts: return nextPort @property def can_use_voltage_port(self): """ 获取 可用电压的端口号 以及相应的电池的IMEI 如果此次操作中 之前从设备上获取过端口的信息 就直接使用该信息 否则再从设备端请求一次 :return: """ if self._portStatus is None: self._portStatus = self.get_port_status_from_dev() portStatus = self._portStatus canUseList = list() for portStr, portInfo in portStatus.items(): # elec in portInfo 是为了确认这个port表示的是端口信息 if isinstance(portInfo, dict) and "elec" in portInfo: canUse, batteryImei, voltage = self._is_can_use_port(portStr, portInfo) if canUse: canUseList.append((portStr, batteryImei, voltage)) Device.update_dev_control_cache(self.device.devNo, {"canUseBattery": len(canUseList)}) if not canUseList: return None, "" canUseList.sort(key = lambda x: x[2], reverse = True) # 取第一个元素的前两个元素 return canUseList[0][:2] @staticmethod def translate_door_status_to_unicode(doorStatus): """ 将柜门状态更改为 汉字表示 :param doorStatus: :return: """ if doorStatus == "opened": return u"开启" elif doorStatus == "closed": return u"关闭" else: return u"读取失败,状态未知" @staticmethod def translate_battery_imei(battery): """ 去掉battery中的非法字符 :param battery: :return: """ newBatterySn = "" for ch in battery: if ch in AnQiBox.BATTERY_SN_ASCII: newBatterySn += ch return newBatterySn def get_port_status_from_dev(self): """ 从设备方获取 换电柜的基本信息 电量百分比 电压 电池IMEI 以及 门锁状态 每次获取的时候检测 电池的电压是否正常 非正常的电池电压上报经销商 :return: """ portStatus = self._query_all_status() otherConf = self._device.get("otherConf", dict()) maxVoltage = float(otherConf.get("maxVoltage", self.DEFAULT_MAX_VOLTAGE)) minVoltage = float(otherConf.get("minVoltage", self.DEFAULT_MIN_VOLTAGE)) for portStr, portInfo in portStatus.items(): voltage = portInfo.get("voltage") batteryImei = portInfo.get("batteryImei") if voltage and batteryImei and (voltage > maxVoltage or voltage < minVoltage): self._battery_voltage_warning(portStr, voltage, minVoltage, maxVoltage, batteryImei) return portStatus def dealer_get_port_status(self): """ 经销商 上分的时候获取端口状态 :return: """ portStatus = self.get_port_status_from_dev() resultDict = dict() for portStr, portInfo in portStatus.items(): tempStatus = self._check_port_status(portStr, portInfo) portInfo.update({"status": tempStatus}) resultDict[portStr] = portInfo return resultDict def start_device(self, package, openId, attachParas): """ 换电柜开门的时候,不需要用户选择几号门打开,系统自动选定空仓打开(开门规则在最下注释) 但是经销商上分的时候是一定会选择仓门进行打开的 除此之外,应当在每次设备启动的时候记录一次缓存信息,最终整个换电结束的时候将此次的信息清空掉,这个缓存信息伴随整个换电过程而生 currentUseInfo :param package: :param openId: :param attachParas: :return: """ # 首先获取chargeIndex 如果没有chargeIndex 说明是终端用户使用 有chargeIndex没有openId的说明是经销商的远程上分 chargeIndex = attachParas.get("chargeIndex") if chargeIndex and not openId: return self._on_point_start(chargeIndex) # 用户启动的,现在可以获取订单号 这个订单号伴随整个换电的过程直到结束 orderNo = attachParas.get("orderNo") # 对于终端用户,需要校验身份认证是否通过 user = MyUser.objects(openId = openId, groupId = self._device["groupId"]).first() if not user: raise ServiceException({'result': 2, 'description': u"无效的用户"}) groupIds = Group.get_group_ids_of_dealer(str(self.device.ownerId)) activeInfo = MyUser.get_active_info(openId, agentId = user.agentId, groupId__in = groupIds) if not activeInfo or not activeInfo.get("isMember"): raise ServiceException({'result': 2, 'description': u"请先完成身份激活信息"}) # 查询缓存,如果当前正在使用的缓存存在,则说明上次的消费尚未结束 devCache = Device.get_dev_control_cache(self._devNo) currentUseInfo = devCache.get("currentUseInfo", dict()) if currentUseInfo: raise ServiceException({'result': 2, 'description': u'上一个客户尚未使用完成,或者尚未关闭柜门,暂时无法使用设备, 请检查是否有柜门尚未关闭。'}) # 获取即将打开的空仓的信息 chargeIndex = self.no_elec_door_port if not chargeIndex: raise ServiceException({'result': 2, 'description': u'当前设备没有可放置电池的柜门,请换个设备试试或联系经销商'}) # 获取可以开的port以及Imei, 没有可用电池的情况下直接结束当前的服务 toOpenChargeIndex, toOpenBattery = self.can_use_voltage_port if not toOpenChargeIndex: raise ServiceException({'result': 2, 'description': u'当前设备无可用电池,请换个设备试试'}) # 记录下此次换电要开启的有电池的信息 currentUseInfo.update({ "toOpenChargeIndex": toOpenChargeIndex, "toOpenBattery": toOpenBattery }) # 检验工作通过之后,生下来的就是记录缓存, 开启柜门 播放音乐等行为 result = self._open_door(chargeIndex) if not result: raise ServiceException({'result': 2, 'description': u'柜门开启失败,请联系相应经销商解决'}) self._pay_voice() # 记录下此次换电过程的当前信息,换电结束的时候清空掉 currentUseInfo.update({ "openId": openId, "chargeIndex1": chargeIndex, "orderNo": orderNo }) Device.update_dev_control_cache(self._devNo, {"currentUseInfo": currentUseInfo}) return result def analyze_event_data(self, data): """ 换电柜的上报信息是只要门锁状态发生了变化就会上报,这个地方直接将信息透传给eventer,交由eventer处理 :param data: :return: """ cmd = data[12: 14] # 主动上传门锁状态变化 当门锁内部有电池的时候 会将电池编号一同上传 if cmd == "85": try: batterySn = self.translate_battery_imei(data[20:-2].decode('hex')[8:]) except Exception as e: batterySn = None result = { "portStr": str(int(data[14:16], 16)), "doorStatus": self.DOOR_STATUS_MAP.get(data[16:18]) } if batterySn: result.update({"batteryImei": batterySn}) return result else: logger.error("no use event cmd, cmd is {}".format(cmd)) def get_dev_setting(self): """ 返回设备设置的一些常量参数 :return: """ elecInfo = self._get_charge_limit() otherConf = self._device.get("otherConf", dict()) repairDoorPort = otherConf.get("repairDoorPort", self.DEFAULT_REPAIR_DOOR) maxVoltage = otherConf.get("maxVoltage", self.DEFAULT_MAX_VOLTAGE) minVoltage = otherConf.get("minVoltage", self.DEFAULT_MIN_VOLTAGE) canUseVoltage = otherConf.get("canUseVoltage", self.DEFAULT_CAN_USE_VOLTAGE) voiceNum = otherConf.get("voiceNum", self.DEFAULT_VOICE_NUM) reverseLockStatus = otherConf.get("reverseLockStatus", self.DEFAULT_REVERSE_LOCK_STATUS) devCache = Device.get_dev_control_cache(self._devNo) currentUseInfo = devCache.get("currentUseInfo", dict()) data = { "repairDoorPort": repairDoorPort, "maxVoltage": maxVoltage, "minVoltage": minVoltage, "canUseVoltage": canUseVoltage, "voiceNum": voiceNum, "reverseLockStatus": reverseLockStatus, } openId = currentUseInfo.get("openId", "") if openId == "invalid user": currentUser = u"非法开门" elif openId == "dealer": currentUser = u"经销商远程上分" else: user = MyUser.objects.filter(openId = openId).first() if openId and user: currentUser = user.nickname else: currentUser = u"无用户使用" data.update({"currentUser": currentUser}) currentPort1 = currentUseInfo.get("chargeIndex1") currentPort2 = currentUseInfo.get("chargeIndex2") currentBatterySn1 = currentUseInfo.get("oldBatteryImei") currentBatterySn2 = currentUseInfo.get("newBatteryImei") if currentPort1: data.update({"currentPort1": currentPort1}) if currentPort2: data.update({"currentPort2": currentPort2}) if currentBatterySn1: data.update({"currentBatterySn1": currentBatterySn1}) if currentBatterySn2: data.update({"currentBatterySn2": currentBatterySn2}) data.update(elecInfo) return data def set_device_function_param(self, request, lastSetConf): """ 设置设备参数 :param request: :param lastSetConf: :return: """ repairDoorPort = request.POST.get("repairDoorPort") maxVoltage = request.POST.get("maxVoltage") minVoltage = request.POST.get("minVoltage") canUseVoltage = request.POST.get("canUseVoltage") voiceNum = request.POST.get("voiceNum") maxElec = request.POST.get("maxElec") minElec = request.POST.get("minElec") otherConf = self._device.get("otherConf", dict()) if repairDoorPort is not None: otherConf.update({"repairDoorPort": repairDoorPort}) if maxVoltage is not None: otherConf.update({"maxVoltage": maxVoltage}) if minVoltage is not None: otherConf.update({"minVoltage": minVoltage}) if canUseVoltage is not None: otherConf.update({"canUseVoltage": canUseVoltage}) if voiceNum is not None: otherConf.update({"voiceNum": voiceNum}) if maxElec and minElec: otherConf.update({"maxElec": maxElec, "minElec": minElec}) self._set_charge_limit(maxElec = maxElec, minElec = minElec) Device.objects.filter(devNo = self._devNo).update(otherConf = otherConf) Device.invalid_device_cache(self._devNo) def set_device_function(self, request, lastSetConf): """ 设置设备功能参数 :param request: :param lastSetConf: :return: """ otherConf = self._device.get("otherConf", dict()) repairDoorPort = otherConf.get("repairDoorPort", self.DEFAULT_REPAIR_DOOR) repairDoor = request.POST.get("repairDoor", False) reverseLockStatus = request.POST.get("reverseLockStatus", None) clearCurUse = request.POST.get("clearCurUse", False) if repairDoor: self._open_door(repairDoorPort) if reverseLockStatus is not None: otherConf.update({"reverseLockStatus": reverseLockStatus}) Device.objects.filter(otherConf = otherConf) Device.invalid_device_cache(self._devNo) # 经销商清除当前使用的信息 if clearCurUse: Device.clear_port_control_cache(self._devNo, "currentUseInfo") def lock_unlock_port(self, port, lock = True): """ 禁用端口 换电柜禁用端口的时候就是做个标记,设备主板本身不支持禁用 :param port: :param lock: :return: """ otherConf = self._device.get("otherConf") lockPorts = otherConf.get("lockPorts", list()) port = str(port) try: if lock: if port not in lockPorts: lockPorts.append(port) else: lockPorts.remove(port) except Exception as e: logger.error("lock or unlock port error: %s" % e) raise ServiceException({'result': 2, 'description': u'操作失败,请重新试试'}) otherConf.update({"lockPorts": lockPorts}) try: Device.objects(devNo = self._device["devNo"]).update(otherConf = otherConf) Device.invalid_device_cache(self._device["devNo"]) except Exception as e: logger.error("update device %s lockPorts error the reason is %s " % (self._device["devNo"], e)) raise ServiceException({'result': 2, 'description': u'操作失败,请重新试试'}) def async_update_portinfo_from_dev(self): """ 继承重写,换电柜每次都是获取的最新的端口信息,不再需要这个 :return: """ pass def active_deactive_port(self, port, charge): if charge: self._turn_on_power(port) else: self._turn_off_power(port) """ 换电情况有以下种: 正常情况 用户扫码支付, 打开空仓, 放入电池, 关闭空仓, 取走电池 此时取走的端口号即为下一个开门的仓号 会重置下一个开门端口的情况: 1. 柜门被非法打开,不知道是什么原因柜门被打开,也不知道柜门被非法打开之后会发生什么情况,这个地方需要重置下一个端口,从主板读取 2. 经销商远程上分,同样不知道是远程上分打开的是什么柜子,到底有没有放入电池,这个地方需要重置下一个端口,从主板读取 3. 用户第二次没有拿走电池,或者用户第二次拿走了电池又放入了电池,必需重置下一个端口,否则会打开有电池的门 4. 经销商清除了正在使用的信息 5. 用户扫码放入电池后,有电池的没有打开,必须重置,否则会打开上一个用户扫码放入的电池的柜门 6. 经销商检修之后 7. 禁用某个端口之后 """