yeg
4 天以前 6b99cc391064cb38c706979fafcecb9f50ea1587
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},按本实例的适配器编码生成检测柜通道清单。 */