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