| | |
| | | import org.xml.sax.helpers.DefaultHandler; |
| | | import com.battery.device.api.StepDirectionResolver; |
| | | import com.battery.device.api.TestDataObservationAdapter.CabinetRealtime; |
| | | import com.battery.device.api.TestDataObservationAdapter.StepRecord; |
| | | import com.battery.device.api.model.ChannelIdentity; |
| | | import com.battery.device.api.model.DeviceConnectionStatus; |
| | | import com.battery.device.api.model.DetectedChannel; |
| | |
| | | /** 该通道的数据文件状态:测试号、已落盘行数、厂商自报的数据是否完整。 */ |
| | | public record DataFileState(long testId, long count, boolean complete) { } |
| | | |
| | | /** |
| | | * 构造工步层下载的请求报文({@code downloadStepLayer})。 |
| | | * |
| | | * <p>现场实测:只发命令与 {@code downloadStepLayer} 元素即可,文档里的 {@code V1I1}/{@code V2I2} |
| | | * 不填时按文档所说"用默认参数";{@code dcir="0"}"不计算 DCIR",我们只要工步事实、不做派生计算。 |
| | | * 与 {@code download} 一样,{@code testid="0"} 表示"该通道最近一次测试",应答会回显真实测试号。 |
| | | */ |
| | | public String downloadStepLayerRequest(ChannelIdentity channel) |
| | | { |
| | | requireOwned(channel); |
| | | return envelope("<cmd>downloadStepLayer</cmd><downloadStepLayer devtype=\"" + channel.devtype() |
| | | + "\" devid=\"" + channel.devid() + "\" subdevid=\"" + channel.subdevid() |
| | | + "\" chlid=\"" + channel.chlid() + "\" testid=\"0\" dcir=\"0\"/>"); |
| | | } |
| | | |
| | | /** |
| | | * 解析 {@code downloadStepLayer_resp} 为工步记录。 |
| | | * |
| | | * <p>每条 {@code <data/>} 是一个工步:{@code startseqid}/{@code endseqid}(该工步的首末数据序号)、 |
| | | * {@code stepindex}(测试内的工步序号)、{@code stepid}(原始工步号,循环里会重复)、 |
| | | * {@code cycleid}、{@code steptype}、{@code steptime}(工步运行时长,**毫秒**)、{@code endatime} |
| | | * (工步绝对结束时间)、{@code startvolt}/{@code endvolt}、{@code startcurr}/{@code endcurr}、 |
| | | * {@code cap}/{@code eng}、{@code dcir}(mΩ)。 |
| | | * |
| | | * <p>三处按实测钉死的口径: |
| | | * |
| | | * <ul> |
| | | * <li><b>循环号要减一。</b>这条命令的 {@code cycleid} 是 **1 起**(第一步 {@code cycleid="1"}), |
| | | * 而 {@code download} 的 {@code cycleid} 与厂商库的 {@code cycle} 都是 **0 起**。 |
| | | * 实测同一测试两处对照(工步层 {@code cycleid="1"}/逐点 {@code cycleid="0"})确认, |
| | | * 所以这里统一成 0 起,与库路线、逐点路线同口径。</li> |
| | | * <li><b>开始时间 = 结束时间 − 工步时长。</b>应答只给 {@code endatime}。按两个独立样本核对过 |
| | | * ({@code endatime=09:09:14, steptime=23100 ms} → 09:08:50.9,该测试首行 {@code atime}=09:08:51), |
| | | * 所以这个减法还原的是设备事实,不是估算。缓冲里缺行时(实测有测试只覆盖约 1 天) |
| | | * 相邻两工步的首末不会接上,那是来源的数据缺口,不是这里算错。</li> |
| | | * <li><b>{@code stepindex} 才是工步身份。</b>{@code stepid} 在循环里会重复(同一步在每轮循环再跑一遍), |
| | | * 用它做身份会撞键。</li> |
| | | * </ul> |
| | | * |
| | | * <p>方向的判定沿用既有解析器;{@code cap}/{@code eng} 在接口上是**单一值**(不分充放), |
| | | * 只在方向已确认时给出,方向未知时留空——不按数值大小猜它属于哪一侧(ADR-0069 §3)。 |
| | | * |
| | | * @param xml 应答原文 |
| | | * @param receivedAt 服务器接收时间 |
| | | * @return 按应答顺序的工步记录;该通道缓冲里没有测试时为空列表 |
| | | * @throws BtsProtocolException 应答不是 {@code downloadStepLayer_resp} |
| | | */ |
| | | public List<StepRecord> parseStepLayer(String xml, Instant receivedAt) |
| | | { |
| | | Document document = parse(xml, "downloadStepLayer_resp"); |
| | | NodeList nodes = document.getElementsByTagName("data"); |
| | | if (nodes.getLength() == 0) return List.of(); |
| | | Element header = optionalElement(document, "downloadStepLayer"); |
| | | String testId = header == null ? "" : optionalAttribute(header, "testid"); |
| | | String zoneId = deviceZone.getId(); |
| | | List<StepRecord> steps = new ArrayList<>(nodes.getLength()); |
| | | for (int i = 0; i < nodes.getLength(); i++) |
| | | { |
| | | Element data = (Element) nodes.item(i); |
| | | String stepType = optionalAttribute(data, "steptype"); |
| | | StepDirectionResolver.Direction direction = directions.resolve(stepType); |
| | | String rawTime = requiredAttribute(data, "endatime"); |
| | | Instant endedAt = parseDataTime(rawTime); |
| | | Long durationMillis = optionalDecimal(data, "steptime") == null ? null |
| | | : optionalDecimal(data, "steptime").longValue(); |
| | | int stepIndex = integer(requiredAttribute(data, "stepindex"), "stepindex"); |
| | | // 应答里的 cycleid 是 1 起,逐点与厂商库都是 0 起;统一成 0 起。 |
| | | int cycleNo = requiredInteger(data, "cycleid") - 1; |
| | | BigDecimal capacity = optionalDecimal(data, "cap"); |
| | | BigDecimal energy = optionalDecimal(data, "eng"); |
| | | boolean directionKnown = direction != StepDirectionResolver.Direction.UNKNOWN; |
| | | steps.add(new StepRecord(testId.isBlank() ? null : "neware:" + testId + ":step:" + stepIndex, cycleNo, |
| | | requiredInteger(data, "stepid"), direction.displayName(), stepType, |
| | | durationMillis == null ? endedAt : endedAt.minusMillis(durationMillis), |
| | | endedAt, doubleValue(data, "endvolt"), doubleValue(data, "endcurr"), |
| | | directionKnown && capacity != null ? capacity.doubleValue() : null, |
| | | directionKnown && energy != null ? energy.doubleValue() : null, |
| | | rawTime, zoneId, receivedAt, stepMeasurements(data, durationMillis))); |
| | | } |
| | | return List.copyOf(steps); |
| | | } |
| | | |
| | | /** 工步的设备事实明细;映射表按已有键名口径命名,值全部来自应答原文。 */ |
| | | private static Map<String, BigDecimal> stepMeasurements(Element data, Long durationMillis) |
| | | { |
| | | Map<String, BigDecimal> measurements = new LinkedHashMap<>(); |
| | | if (durationMillis != null) measurements.put("stepDuration", BigDecimal.valueOf(durationMillis).movePointLeft(3)); |
| | | put(measurements, data, "startSeqId", "startseqid"); |
| | | put(measurements, data, "endSeqId", "endseqid"); |
| | | put(measurements, data, "startVoltage", "startvolt"); |
| | | put(measurements, data, "startCurrent", "startcurr"); |
| | | put(measurements, data, "dcir", "dcir"); |
| | | return Map.copyOf(measurements); |
| | | } |
| | | |
| | | private static void put(Map<String, BigDecimal> target, Element element, String key, String attribute) |
| | | { |
| | | BigDecimal value = optionalDecimal(element, attribute); |
| | | if (value != null) target.put(key, value); |
| | | } |
| | | |
| | | private static int requiredInteger(Element element, String name) |
| | | { |
| | | return integer(requiredAttribute(element, name), name); |
| | | } |
| | | |
| | | |
| | | |
| | | /** 解析 {@code getdevinfo_resp},按本实例的适配器编码生成检测柜通道清单。 */ |