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/test/java/com/battery/device/bts/NewareBtsAdapterContractTest.java |  147 +++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 142 insertions(+), 5 deletions(-)

diff --git a/battery-device-bts/src/test/java/com/battery/device/bts/NewareBtsAdapterContractTest.java b/battery-device-bts/src/test/java/com/battery/device/bts/NewareBtsAdapterContractTest.java
index cdbdd16..d75dfc1 100644
--- a/battery-device-bts/src/test/java/com/battery/device/bts/NewareBtsAdapterContractTest.java
+++ b/battery-device-bts/src/test/java/com/battery/device/bts/NewareBtsAdapterContractTest.java
@@ -16,6 +16,7 @@
 import org.junit.jupiter.api.Test;
 import com.battery.device.api.DeviceAdapter;
 import com.battery.device.api.TestDataObservationAdapter;
+import com.battery.device.api.TestDataObservationAdapter.CycleRecord;
 import com.battery.device.api.TestDataObservationAdapter.SampleWatermark;
 import com.battery.device.api.model.ChannelIdentity;
 import com.battery.device.api.model.DeviceControlCommand;
@@ -148,14 +149,12 @@
     }
 
     @Test
-    void doesNotInventStepCyclesCollectorOrPlanFacts()
+    void doesNotInventCollectorOrPlanFacts()
     {
         ScriptedPipe pipe = new ScriptedPipe();
         try (TestDataObservationAdapter adapter = (TestDataObservationAdapter) adapter(pipe))
         {
-            // 工步层要未验证的 downloadStepLayer;采集器测量 ADR-0068 已复核不可得;协议里没有读计划的命令。
-            assertTrue(adapter.stepRecords(CHANNEL).isEmpty());
-            assertTrue(adapter.cycleRecords(CHANNEL).isEmpty());
+            // 采集器测量 ADR-0068 已复核不可得;协议里没有读计划的命令。
             assertNull(adapter.latestCollector(ADAPTER, "collector-1"));
             assertNull(adapter.testPlan(CHANNEL));
             assertEquals(NewareBtsAdapter.SAMPLE_INTERVAL_MS, adapter.persistenceIntervalMillis(CHANNEL));
@@ -178,6 +177,128 @@
                 assertThrows(UnsupportedOperationException.class, () -> adapter.control(CHANNEL, command));
             assertTrue(pipe.commands().isEmpty());
         }
+    }
+
+    @Test
+    void handsOutEveryStepAndCycleWhenTheChannelHasEnded()
+    {
+        ScriptedPipe pipe = finishedTestPipe(DeviceWorkStatus.FINISHED);
+        try (TestDataObservationAdapter adapter = (TestDataObservationAdapter) adapter(pipe))
+        {
+            var steps = adapter.stepRecords(CHANNEL);
+            var cycles = adapter.cycleRecords(CHANNEL);
+
+            assertEquals(4, steps.size());
+            assertEquals("充电", steps.get(1).stepName());
+            // 循环号已按 0 起归一到与厂商库、逐点同口径(应答里的 cycleid 是 1 起)。
+            assertEquals(0, steps.get(0).cycleNo());
+
+            assertEquals(1, cycles.size());
+            var cycle = cycles.get(0);
+            assertEquals(0, cycle.cycleNo());
+            assertEquals(2.81630277777778, cycle.chargeCapacityAh(), 1e-9);
+            assertEquals(0.00431017035590278, cycle.dischargeCapacityAh(), 1e-12);
+            assertEquals(5.58447055555556, cycle.chargeEnergyWh(), 1e-9);
+            assertEquals(0.00426197048611111, cycle.dischargeEnergyWh(), 1e-12);
+            // 效率 = 放电容量 / 充电容量 × 100;搁置工步不进任何一侧。
+            assertEquals(0.153, cycle.efficiency(), 0.0005);
+            assertEquals("neware:316:cycle:0", cycle.sourceRecordId());
+            assertEquals(CycleRecord.SOURCE_AGGREGATED, cycle.sourceKind());
+        }
+    }
+
+    @Test
+    void holdsBackTheRunningStepAndItsCycleWhileTheTestIsStillRunning()
+    {
+        // 实测这条命令会把正在进行的那一步也交出来,而它的 endseqid 恰好等于已落盘行数
+        // (四个测试都相等),所以只能靠通道工作状态判断它有没有结束。
+        ScriptedPipe pipe = finishedTestPipe(DeviceWorkStatus.WORKING);
+        try (TestDataObservationAdapter adapter = (TestDataObservationAdapter) adapter(pipe))
+        {
+            var steps = adapter.stepRecords(CHANNEL);
+            var cycles = adapter.cycleRecords(CHANNEL);
+
+            // 最后一步(stepindex=4)与它所在的循环都不交出,等它结束再交。
+            assertEquals(3, steps.size());
+            assertEquals("neware:316:step:3", steps.get(2).sourceRecordId());
+            assertTrue(cycles.isEmpty());
+        }
+    }
+
+    @Test
+    void readsTheStepLayerOnceForBothStepsAndCycles()
+    {
+        // 工步与循环必须来自同一次读取:否则一个工步边界落在两次读取之间时,
+        // 循环合计会包含工步列表里没有的那一步,使循环记录每轮内容都不同。
+        ScriptedPipe pipe = finishedTestPipe(DeviceWorkStatus.FINISHED);
+        try (TestDataObservationAdapter adapter = (TestDataObservationAdapter) adapter(pipe))
+        {
+            adapter.stepRecords(CHANNEL);
+            adapter.cycleRecords(CHANNEL);
+
+            assertEquals(1, pipe.commands().stream().filter("downloadsteplayer"::equals).count());
+        }
+    }
+
+    @Test
+    void aChannelWithNoTestInTheBufferHasNoStepsOrCycles()
+    {
+        ScriptedPipe pipe = new ScriptedPipe()
+                .on("downloadStepLayer", "<bts><cmd>downloadStepLayer_resp</cmd><list count=\"0\" /></bts>");
+        try (TestDataObservationAdapter adapter = (TestDataObservationAdapter) adapter(pipe))
+        {
+            assertTrue(adapter.stepRecords(CHANNEL).isEmpty());
+            assertTrue(adapter.cycleRecords(CHANNEL).isEmpty());
+            // 缓冲里没有测试就直接是空,不必再去问工作状态。
+            assertEquals(List.of("downloadsteplayer"), pipe.commands());
+        }
+    }
+
+    @Test
+    void aFailedStateReadSkipsTheRoundInsteadOfHandingOutTheRunningStep()
+    {
+        // 读取失败不吞:调用方本来就有"来源读不到就本轮放弃、下一轮重试"的约定,
+        // 所以一次传输抖动不会变成"把一个还在跑的工步交出去",也不会被当成结论缓存下来。
+        ScriptedPipe pipe = finishedTestPipe(DeviceWorkStatus.WORKING).failing("inquire");
+        try (TestDataObservationAdapter adapter = (TestDataObservationAdapter) adapter(pipe))
+        {
+            assertThrows(RuntimeException.class, () -> adapter.stepRecords(CHANNEL));
+
+            pipe.recover();
+            // 恢复后重新读,拿到的是完整结论(在跑 → 交出 3 步、按住最后一步),不是上一次的空结果。
+            assertEquals(3, adapter.stepRecords(CHANNEL).size());
+        }
+    }
+
+    /** 一个已结束测试的工步层(现场实测原文,测试 316):4 步都在循环 1(归一后为 0)。 */
+    private static ScriptedPipe finishedTestPipe(DeviceWorkStatus workStatus)
+    {
+        return new ScriptedPipe()
+                .on("inquire", "<bts><cmd>inquire_resp</cmd><list count=\"1\"><inquire dev=\"25-81-1-2-0\""
+                        + " cycle_id=\"1\" step_id=\"4\" step_type=\"dc\" workstatus=\""
+                        + workStatus.name().toLowerCase(java.util.Locale.ROOT)
+                        + "\" current=\"0\" voltage=\"1.2\" /></list></bts>")
+                .on("downloadStepLayer", "<bts><cmd>downloadStepLayer_resp</cmd>"
+                        + "<downloadStepLayer devtype=\"25\" devid=\"81\" subdevid=\"1\" chlid=\"2\" testid=\"316\" />"
+                        + "<list count=\"4\">"
+                        + "<data startseqid=\"1\" endseqid=\"2\" stepindex=\"1\" stepid=\"1\" cycleid=\"1\""
+                        + " steptype=\"rest\" steptime=\"23100\" endatime=\"2026-10-10 09:09:14\""
+                        + " startvolt=\"0.806611083984375\" endvolt=\"0.80638037109375\" startcurr=\"0\""
+                        + " endcurr=\"0\" cap=\"0\" eng=\"0\" dcir=\"0\" />"
+                        + "<data startseqid=\"3\" endseqid=\"304\" stepindex=\"2\" stepid=\"2\" cycleid=\"1\""
+                        + " steptype=\"cc\" steptime=\"18000000\" endatime=\"2026-10-10 14:09:14\""
+                        + " startvolt=\"1.07542861328125\" endvolt=\"1.972108203125\""
+                        + " startcurr=\"0.563903076171875\" endcurr=\"0.563332702636719\""
+                        + " cap=\"2.81630277777778\" eng=\"5.58447055555556\" dcir=\"0\" />"
+                        + "<data startseqid=\"305\" endseqid=\"307\" stepindex=\"3\" stepid=\"3\" cycleid=\"1\""
+                        + " steptype=\"rest\" steptime=\"83600\" endatime=\"2026-10-10 14:10:38\""
+                        + " startvolt=\"1.7850955078125\" endvolt=\"1.42794287109375\" startcurr=\"0\""
+                        + " endcurr=\"0\" cap=\"0\" eng=\"0\" dcir=\"0\" />"
+                        + "<data startseqid=\"308\" endseqid=\"309\" stepindex=\"4\" stepid=\"4\" cycleid=\"1\""
+                        + " steptype=\"dc\" steptime=\"27600\" endatime=\"2026-10-10 14:11:05\""
+                        + " startvolt=\"1.27452001953125\" endvolt=\"0.49604326171875\""
+                        + " startcurr=\"-0.562971618652344\" endcurr=\"-0.5622275390625\""
+                        + " cap=\"0.00431017035590278\" eng=\"0.00426197048611111\" dcir=\"0\" /></list></bts>");
     }
 
     private static DeviceAdapter adapter(ScriptedPipe pipe)
@@ -216,13 +337,27 @@
 
         private final Map<String, String> responses = new java.util.LinkedHashMap<>();
         private final List<String> commands = new ArrayList<>();
+        /** 让指定命令暂时失败,用来验"读取失败不吞、不当结论缓存"。 */
+        private final java.util.Set<String> failing = new java.util.LinkedHashSet<>();
         private int firstRow = 0;
         private int lastRow = 0;
 
         ScriptedPipe on(String command, String response)
         {
-            responses.put(command, response);
+            // 请求里的命令名按小写匹配(与生产传输的比对口径一致)。
+            responses.put(command.toLowerCase(java.util.Locale.ROOT), response);
             return this;
+        }
+
+        ScriptedPipe failing(String command)
+        {
+            failing.add(command.toLowerCase(java.util.Locale.ROOT));
+            return this;
+        }
+
+        void recover()
+        {
+            failing.clear();
         }
 
         /** 生成 {@code [first, last]} 的逐点行集,供 download/inquiredf 按窗口回答。 */
@@ -250,6 +385,8 @@
             Matcher matcher = COMMAND.matcher(request);
             if (!matcher.find()) throw new IllegalStateException("请求缺少命令:" + request);
             String command = matcher.group(1).toLowerCase(java.util.Locale.ROOT);
+            if (failing.contains(command))
+                throw new BtsTransportException(BtsTransportException.Reason.TIMEOUT, "测试脚本让 " + command + " 失败", null);
             if ("inquiredf".equals(command))
             {
                 commands.add("inquiredf");

--
Gitblit v1.7.1