From 6b99cc391064cb38c706979fafcecb9f50ea1587 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期日, 11 十月 2026 02:07:44 +0800
Subject: [PATCH] 新威直连补上工步层与循环汇总(downloadStepLayer 实机验证后落地)
---
battery-device-bts/src/main/java/com/battery/device/bts/BtsXmlCodec.java | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 107 insertions(+), 0 deletions(-)
diff --git a/battery-device-bts/src/main/java/com/battery/device/bts/BtsXmlCodec.java b/battery-device-bts/src/main/java/com/battery/device/bts/BtsXmlCodec.java
index 9ae7c8f..780799d 100644
--- a/battery-device-bts/src/main/java/com/battery/device/bts/BtsXmlCodec.java
+++ b/battery-device-bts/src/main/java/com/battery/device/bts/BtsXmlCodec.java
@@ -26,6 +26,7 @@
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;
@@ -190,6 +191,112 @@
/** 该通道的数据文件状态:测试号、已落盘行数、厂商自报的数据是否完整。 */
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},按本实例的适配器编码生成检测柜通道清单。 */
--
Gitblit v1.7.1