From b78295e8da96c3e5e76befeef9582ce5273dbb74 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期二, 29 九月 2026 13:10:23 +0800
Subject: [PATCH] 按代码规范检查整改实体注释、Service Javadoc 与分层边界

---
 battery-device-simulator/src/main/java/com/battery/device/simulator/BtsDevelopmentSimulator.java |  924 ++++++++++++++-------------------------------------------
 1 files changed, 224 insertions(+), 700 deletions(-)

diff --git a/battery-device-simulator/src/main/java/com/battery/device/simulator/BtsDevelopmentSimulator.java b/battery-device-simulator/src/main/java/com/battery/device/simulator/BtsDevelopmentSimulator.java
index c4e597b..1dea589 100644
--- a/battery-device-simulator/src/main/java/com/battery/device/simulator/BtsDevelopmentSimulator.java
+++ b/battery-device-simulator/src/main/java/com/battery/device/simulator/BtsDevelopmentSimulator.java
@@ -7,10 +7,14 @@
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import javax.xml.stream.XMLInputFactory;
 import javax.xml.stream.XMLStreamConstants;
 import javax.xml.stream.XMLStreamReader;
+import com.battery.device.api.DeviceAdapter;
 import com.battery.device.api.model.DeviceWorkStatus;
 import com.battery.device.api.model.DeviceConnectionStatus;
 import com.battery.device.api.model.DeviceIdentity;
@@ -19,32 +23,31 @@
 import com.battery.device.api.model.DetectedDeviceMetadata;
 import com.battery.device.api.model.ChannelIdentity;
 import com.battery.device.api.model.AdapterCapabilities;
+import com.battery.device.api.model.RealtimeSnapshot;
 import com.battery.device.api.DeviceObservationAdapter;
 import com.battery.device.api.TestDataObservationAdapter;
 import com.battery.device.simulator.BtsSimulatorStateStore;
 import com.battery.device.simulator.BtsSimulatorStateStore.ChannelState;
+import com.battery.device.simulator.BtsSimulatorStateStore.ChannelConfiguration;
 import com.battery.device.simulator.BtsSimulatorStateStore.ChannelTombstone;
 import com.battery.device.simulator.BtsSimulatorStateStore.CollectorChannelState;
-import com.battery.device.simulator.BtsSimulatorStateStore.CollectorHistoryAppend;
-import com.battery.device.simulator.BtsSimulatorStateStore.CollectorHistoryFaults;
-import com.battery.device.simulator.BtsSimulatorStateStore.CollectorMeasurementRecord;
 import com.battery.device.simulator.BtsSimulatorStateStore.CollectorMetricState;
 import com.battery.device.simulator.BtsSimulatorStateStore.CollectorRealtimeMeasurement;
 import com.battery.device.simulator.BtsSimulatorStateStore.CollectorState;
 import com.battery.device.simulator.BtsSimulatorStateStore.DeviceState;
 import com.battery.device.simulator.BtsSimulatorStateStore.BtsDeviceProfile;
-import com.battery.device.simulator.BtsSimulatorStateStore.BtsHistoryRecord;
-import com.battery.device.simulator.BtsSimulatorStateStore.BtsPlaybackState;
 import com.battery.device.simulator.BtsSimulatorStateStore.RecentRequest;
 import com.battery.device.simulator.BtsSimulatorStateStore.SimulatorState;
 import com.battery.device.simulator.InMemoryBtsSimulatorStateStore;
 import com.battery.device.simulator.DevelopmentCollectorSimulation;
-import com.battery.device.simulator.DevelopmentCollectorSimulation.CollectorAdvance;
-import com.battery.device.simulator.DevelopmentCollectorSimulation.CollectorHistoryPage;
 import com.battery.device.simulator.DevelopmentCollectorSimulation.CollectorRealtimeSnapshot;
 
-/** 提供由开发后端托管的临时只读 BTS 模拟端点。 */
-public final class BtsDevelopmentSimulator implements TestDataObservationAdapter
+/**
+ * 提供由开发后端托管的临时只读 BTS 模拟端点。
+ * 同时实现发现与测试数据观测契约:模拟器直接暴露自身状态,因此发现与取数按同一实例解析,
+ * 不再需要单独注入一个扩展观测 Bean。TCP 端点仍保留给协议层测试使用。
+ */
+public final class BtsDevelopmentSimulator implements DeviceAdapter, TestDataObservationAdapter
 {
     public static final String HOST = "127.0.0.1";
     public static final int DEFAULT_PORT = 15020;
@@ -62,18 +65,17 @@
     private final BtsSimulatorStateStore stateStore;
     private SimulatorState state;
     private boolean running = true;
-    private long observationSequence;
-    private final java.util.Map<String, java.util.List<BtsHistoryRecord>> btsHistory = new java.util.LinkedHashMap<>();
-    private static final long SIMULATED_RECORD_INTERVAL_SECONDS = 60L;
-    private static final DateTimeFormatter BTS_HISTORY_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH.mm.ss")
-            .withZone(ZoneId.of("Asia/Hong_Kong"));
+    private static final ZoneId DEVICE_ZONE = ZoneId.of("Asia/Hong_Kong");
+    private static final DateTimeFormatter DEVICE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
 
+    /** 返回固定的适配器实例代码,用于把模拟观测归属到 dev-sim 这一实例。 */
     @Override
     public String adapterCode()
     {
         return ADAPTER_CODE;
     }
 
+    /** 返回只观测能力:支持发现与实时观测,不支持历史下载,也不支持向设备下发控制命令。 */
     @Override
     public AdapterCapabilities capabilities()
     {
@@ -92,11 +94,12 @@
             {
                 ChannelIdentity identity = new ChannelIdentity(ADAPTER_CODE, profile.devtype(), device.deviceId(),
                         profile.subDeviceId(), channel.channelId());
-                result.add(new DetectedChannel(identity, device.ip(), device.online() && channel.online(),
+                result.add(new DetectedChannel(identity, device.ip(), state.softwareAccessible() && device.online() && channel.online(),
                         new DetectedDeviceMetadata(profile.name(), profile.manufacturer(), profile.model(),
                                 simulatorMac(profile.devtype(), device.deviceId()))));
             }
         }
+        recordRequest("discover", ADAPTER_CODE, result.toString());
         return List.copyOf(result);
     }
 
@@ -107,30 +110,53 @@
     {
         if (channels == null || channels.isEmpty()) return java.util.Map.of();
         java.util.Map<ChannelIdentity, DeviceObservation> result = new java.util.LinkedHashMap<>();
+        for (ChannelIdentity requested : channels) result.put(requested, observation(requested));
+        recordRequest("observe", channels.toString(), result.toString());
+        return java.util.Map.copyOf(result);
+    }
+
+    /** 直接提供模拟器实时快照;不可观测通道不出现在结果里,与 BTS 协议解析后的语义一致。 */
+    @Override
+    public synchronized Map<ChannelIdentity, RealtimeSnapshot> inquire(Set<ChannelIdentity> channels)
+    {
+        if (channels == null || channels.isEmpty()) return Map.of();
+        Map<ChannelIdentity, RealtimeSnapshot> result = new LinkedHashMap<>();
         for (ChannelIdentity requested : channels)
         {
-            DeviceState device = state.devices().stream().filter(item -> item.deviceId() == requested.devid())
-                    .findFirst().orElse(null);
-            ChannelState channel = device == null ? null : device.channels().stream()
-                    .filter(item -> item.channelId() == requested.chlid()).findFirst().orElse(null);
-            if (device == null || channel == null || !ADAPTER_CODE.equals(requested.adapterCode())
-                    || !device.online() || !channel.online())
-            {
-                result.put(requested, DeviceObservation.offline(requested, "OFFLINE"));
-                continue;
-            }
-            BtsDeviceProfile profile = profileFor(device);
-            if (requested.devtype() != profile.devtype() || requested.subdevid() != profile.subDeviceId())
-            {
-                result.put(requested, DeviceObservation.offline(requested, "OFFLINE"));
-                continue;
-            }
-            result.put(requested, new DeviceObservation(
-                    new DeviceIdentity(ADAPTER_CODE, profile.devtype(), device.deviceId(), profile.subDeviceId()),
-                    requested, DeviceConnectionStatus.ONLINE, channel.workStatus(), List.of(), null,
-                    java.util.Map.of(), null));
+            if (!canObserve(requested)) continue;
+            CabinetRealtime measurement = cabinetMeasurement(requested);
+            if (measurement == null) continue;
+            DeviceObservation observed = observation(requested);
+            result.put(requested, new RealtimeSnapshot(requested, observed.workStatus(), measurement.cycleNo(),
+                    measurement.stepNo(), measurement.stepType(), null, BigDecimal.valueOf(measurement.currentA()),
+                    BigDecimal.valueOf(measurement.voltageV()), BigDecimal.valueOf(measurement.capacityAh()),
+                    BigDecimal.valueOf(measurement.energyWh()), totalTimeSeconds(requested),
+                    measurement.relativeTimeSeconds(), null, measurement.serverReceivedAt(), Map.of(),
+                    DeviceConnectionStatus.ONLINE, measurement.deviceTime(), List.of(), null));
         }
-        return java.util.Map.copyOf(result);
+        recordRequest("inquire", channels.toString(), result.toString());
+        return Map.copyOf(result);
+    }
+
+    private long totalTimeSeconds(ChannelIdentity identity)
+    {
+        return requireDevice(identity.devid()).channels().get(identity.chlid() - 1).configuration().totalTimeSeconds();
+    }
+
+    private DeviceObservation observation(ChannelIdentity requested)
+    {
+        DeviceState device = state.devices().stream().filter(item -> item.deviceId() == requested.devid())
+                .findFirst().orElse(null);
+        ChannelState channel = device == null ? null : device.channels().stream()
+                .filter(item -> item.channelId() == requested.chlid()).findFirst().orElse(null);
+        if (!state.softwareAccessible() || device == null || channel == null
+                || !ADAPTER_CODE.equals(requested.adapterCode()) || !device.online() || !channel.online()
+                || requested.devtype() != profileFor(device).devtype()
+                || requested.subdevid() != profileFor(device).subDeviceId())
+            return DeviceObservation.offline(requested, failureReason());
+        return new DeviceObservation(new DeviceIdentity(ADAPTER_CODE, requested.devtype(), requested.devid(),
+                requested.subdevid()), requested, DeviceConnectionStatus.ONLINE, channel.workStatus(),
+                List.of(), null, java.util.Map.of(), null);
     }
 
     /** 启动仅监听 IPv4 本机回环地址的开发模拟器;传入 0 时由系统分配测试端口。 */
@@ -144,14 +170,11 @@
     {
         this.stateStore = stateStore;
         state = stateStore.loadOrInitialize(defaultState());
-        // 仅清理嵌在主状态中的旧演示历史;独立历史由模拟器存储保留。
-        state = stateStore.resetHistoryForDemoStartup(state);
-        restoreBtsHistory();
         validateRestoredState();
-        stateStore.completeValidatedRestore(state);
         simulator = ScriptedBtsTcpSimulator.responding(port, this::respond);
     }
 
+    /** 返回模拟器 TCP 端点实际监听的端口。 */
     public int port()
     {
         return simulator.port();
@@ -191,7 +214,7 @@
                 state.nextCollectorId(), tombstones));
     }
 
-    /** 使用现场确认的新威检测柜 profile 新增设备,型号之外的控制语义仍保持只读。 */
+    /** 按预设新威型号元数据新增模拟设备;真实协议和拓扑仍待现场验证。 */
     public synchronized int addNewareDevice(String ip, String model)
     {
         BtsDeviceProfile profile = newareProfile(model);
@@ -219,15 +242,9 @@
         List<DeviceState> devices = new ArrayList<>(state.devices());
         devices.removeIf(item -> item.deviceId() == deviceId);
         List<ChannelTombstone> tombstones = new ArrayList<>(state.tombstones());
-        BtsDeviceProfile removedProfile = profileFor(removed);
         for (ChannelState channel : removed.channels())
         {
             upsertTombstone(tombstones, removed.deviceId(), removed.ip(), channel.channelId());
-            String key = historyKey(removedProfile.devtype(), removed.deviceId(), removedProfile.subDeviceId(),
-                    channel.channelId());
-            stateStore.deleteBtsHistory(key);
-            stateStore.deleteBtsPlaybackState(key);
-            btsHistory.remove(key);
         }
         persist(new SimulatorState(state.version(), devices, state.collectors(), state.nextDeviceId(),
                 state.nextCollectorId(), tombstones));
@@ -242,10 +259,26 @@
         if (channelId < 1 || channelId > device.channels().size())
             throw new IllegalArgumentException("channelId must be between 1 and " + device.channels().size());
         List<ChannelState> channels = new ArrayList<>(device.channels());
-        channels.set(channelId - 1, new ChannelState(channelId, online, workStatus));
+        channels.set(channelId - 1, new ChannelState(channelId, online, workStatus,
+                channels.get(channelId - 1).configuration()));
         persist(new SimulatorState(state.version(),
                 replaceDevice(new DeviceState(deviceId, device.ip(), device.online(), channels, device.profile())), state.collectors(),
                 state.nextDeviceId(), state.nextCollectorId(), state.tombstones()));
+    }
+
+    /** 保存单通道的当前测量和工步方案,不改写在线或工作状态,不产生观测。 */
+    public synchronized void configureChannel(int deviceId, int channelId, ChannelConfiguration configuration)
+    {
+        if (configuration == null) throw new IllegalArgumentException("通道配置不能为空");
+        DeviceState device = requireDevice(deviceId);
+        if (channelId < 1 || channelId > device.channels().size())
+            throw new IllegalArgumentException("通道不存在");
+        List<ChannelState> channels = new ArrayList<>(device.channels());
+        ChannelState channel = channels.get(channelId - 1);
+        channels.set(channelId - 1, new ChannelState(channelId, channel.online(), channel.workStatus(), configuration));
+        persist(new SimulatorState(state.version(),
+                replaceDevice(new DeviceState(deviceId, device.ip(), device.online(), channels, device.profile())),
+                state.collectors(), state.nextDeviceId(), state.nextCollectorId(), state.tombstones()));
     }
 
     /** 新增一台与 BTS 检测柜拓扑隔离的开发采集器。 */
@@ -281,19 +314,10 @@
         collectors.removeIf(item -> item.collectorId() == collectorId);
         SimulatorState next = new SimulatorState(state.version(), state.devices(), collectors, state.nextDeviceId(),
                 state.nextCollectorId(), state.tombstones());
-        stateStore.saveAndDeleteCollectorHistory(next, collector.deviceCode());
-        this.state = next;
+        persist(next);
     }
 
-    /** 配置采集器独立采样频率和历史下载故障场景。 */
-    public synchronized void configureCollectorSampling(int collectorId, int sampleIntervalMs,
-            CollectorHistoryFaults historyFaults)
-    {
-        CollectorState current = requireCollector(collectorId);
-        replaceCollector(DevelopmentCollectorSimulation.configureSampling(current, sampleIntervalMs, historyFaults));
-    }
-
-    /** 配置一个稳定 S 通道的固定或动态演示指标。 */
+    /** 保存稳定 S 通道的固定测量值及自定义指标,下一次请求时形成观测。 */
     public synchronized void configureCollectorChannel(int collectorId, String channelCode,
             List<CollectorMetricState> metrics)
     {
@@ -301,393 +325,168 @@
         replaceCollector(DevelopmentCollectorSimulation.configureChannel(current, channelCode, metrics));
     }
 
-    /**
-     * 模拟现场人工统一开关机:关机停止采样,开机重启检测柜夹具生命周期并保持通道待机。
-     * 采集器历史继续保留,检测柜夹具历史在关机后的开机上升沿清理。
-     * 该操作只改变模拟设备事实,不代表系统具备真实设备控制能力。
-     */
+    /** 修改软件可访问性,不改变设备身份、在线配置或通道工作状态。 */
+    public synchronized void setSoftwareAccessible(boolean accessible)
+    {
+        SimulatorState next = new SimulatorState(state.version(), state.devices(), state.collectors(),
+                state.nextDeviceId(), state.nextCollectorId(), state.tombstones(), accessible, state.observationSequence());
+        stateStore.save(next);
+        state = next;
+    }
+
+    /** 批量修改设备在线事实,保留各通道工作状态。 */
     public synchronized void setAllDevicesPoweredOn(boolean poweredOn)
     {
-        boolean wasPoweredOff = (!state.devices().isEmpty() || !state.collectors().isEmpty())
-                && state.devices().stream().noneMatch(DeviceState::online)
-                && state.collectors().stream().noneMatch(CollectorState::online);
-        Instant changedAt = Instant.now();
-        if (poweredOn && wasPoweredOff)
-        {
-            // 一键开机代表新一轮演示;清理检测柜夹具,避免序号从 1 重放并与旧生命周期冲突。
-            for (DeviceState device : state.devices())
-                for (ChannelState channel : device.channels())
-                    resetBtsFixtureLifecycle(btsKey(device, channel.channelId()), changedAt);
-        }
         List<DeviceState> devices = state.devices().stream()
-                .map(device -> new DeviceState(device.deviceId(), device.ip(), poweredOn,
-                        device.channels().stream()
-                                .map(channel -> new ChannelState(channel.channelId(), poweredOn,
-                                        DeviceWorkStatus.STOPPED))
-                                .toList(), device.profile()))
+                .map(device -> new DeviceState(device.deviceId(), device.ip(), poweredOn, device.channels(), device.profile()))
                 .toList();
         List<CollectorState> collectors = state.collectors().stream()
                 .map(collector -> DevelopmentCollectorSimulation.resize(collector, collector.ip(),
-                        collector.channelCount(), poweredOn, changedAt))
-                .toList();
+                        collector.channelCount(), poweredOn, Instant.now())).toList();
         persist(new SimulatorState(state.version(), devices, collectors, state.nextDeviceId(),
                 state.nextCollectorId(), state.tombstones()));
     }
 
-    /** 清理检测柜夹具的历史和播放指针;采集器历史由总电源开关独立保留。 */
-    private void resetBtsFixtureLifecycle(String key, Instant now)
+    /** 修改采集器单个通道工作状态;不代表真实采集器控制能力。 */
+    public synchronized void updateCollectorChannelStatus(int collectorId, String channelCode, DeviceWorkStatus status)
     {
-        btsHistory.remove(key);
-        stateStore.deleteBtsHistory(key);
-        stateStore.deleteBtsPlaybackState(key);
-        savePlayback(key, defaultPlayback(now));
+        requireControllableStatus(status);
+        replaceCollector(DevelopmentCollectorSimulation.configureStatus(requireCollector(collectorId), channelCode, status));
     }
 
-    /** 按每台采集器自己的频率推进实时和历史采样。 */
-    public synchronized void advanceCollectors(Instant now)
-    {
-        boolean hasOnlineCollector = false;
-        for (CollectorState collector : state.collectors())
-        {
-            if (!collector.online()) continue;
-            hasOnlineCollector = true;
-            break;
-        }
-        if (!hasOnlineCollector) return;
-        List<CollectorState> collectors = new ArrayList<>(state.collectors().size());
-        List<CollectorHistoryAppend> historyAppends = new ArrayList<>();
-        for (CollectorState collector : state.collectors())
-        {
-            CollectorAdvance advance = DevelopmentCollectorSimulation.advance(collector, now);
-            collectors.add(advance.state());
-            if (!advance.appendedRecords().isEmpty())
-                historyAppends.add(new CollectorHistoryAppend(collector.deviceCode(), advance.appendedRecords()));
-        }
-        if (!historyAppends.isEmpty())
-            persist(new SimulatorState(state.version(), state.devices(), collectors, state.nextDeviceId(),
-                    state.nextCollectorId(), state.tombstones()), historyAppends);
-    }
-
-    /** 按墙上时间推进检测柜模拟播放;设备状态和测试接收状态互不影响。 */
-    public synchronized void advanceBts(Instant now)
-    {
-        for (DeviceState device : state.devices())
-        {
-            if (!device.online()) continue;
-            BtsDeviceProfile profile = profileFor(device);
-            for (ChannelState channel : device.channels())
-            {
-                if (!channel.online() || channel.workStatus() != DeviceWorkStatus.WORKING) continue;
-                String key = historyKey(profile.devtype(), device.deviceId(), profile.subDeviceId(), channel.channelId());
-                BtsPlaybackState playback = playbackState(key, now);
-                advanceBtsPlayback(key, profile, playback,
-                        profile.currentMaxA().doubleValue() * 0.2d, 3.7d, now);
-            }
-        }
-    }
-
-    /** 设置检测柜通道演示倍速,只接受 1x、60x 和 600x。 */
-    public synchronized void setBtsSpeed(int deviceId, int channelId, int speedMultiplier)
-    {
-        DeviceState device = requireDevice(deviceId);
-        requireChannel(device, channelId);
-        String key = btsKey(device, channelId);
-        BtsPlaybackState current = playbackState(key, Instant.now());
-        stateStore.saveBtsPlaybackState(key, new BtsPlaybackState(current.playing(), speedMultiplier,
-                current.cycleNo(), current.stepNo(), current.stepName(), current.stepType(), current.simulatedAt(),
-                Instant.now(), current.nextSequence(), current.naturallyFinished()));
-    }
-
-    /** 开始播放指定检测柜通道的演示时间线。 */
-    public synchronized void playBts(int deviceId, int channelId) { setBtsPlaying(deviceId, channelId, true); }
-    /** 暂停模拟器时间线,不改变检测柜通道工作事实。 */
-    public synchronized void pauseBts(int deviceId, int channelId) { setBtsPlaying(deviceId, channelId, false); }
-    /** 从暂停状态恢复模拟器时间线。 */
-    public synchronized void resumeBts(int deviceId, int channelId) { setBtsPlaying(deviceId, channelId, true); }
-
-    private void setBtsPlaying(int deviceId, int channelId, boolean playing)
-    {
-        DeviceState device = requireDevice(deviceId);
-        requireChannel(device, channelId);
-        String key = btsKey(device, channelId);
-        Instant now = Instant.now();
-        BtsPlaybackState current = playbackState(key, now);
-        if (current.naturallyFinished() && playing) return;
-        stateStore.saveBtsPlaybackState(key, new BtsPlaybackState(playing, current.speedMultiplier(), current.cycleNo(),
-                current.stepNo(), current.stepName(), current.stepType(), current.simulatedAt(), now,
-                current.nextSequence(), current.naturallyFinished()));
-    }
-
-    /** 跳转到下一实际工步;不向设备发送控制报文。 */
-    public synchronized void nextBtsStep(int deviceId, int channelId)
-    {
-        DeviceState device = requireDevice(deviceId);
-        requireChannel(device, channelId);
-        String key = btsKey(device, channelId);
-        BtsPlaybackState current = playbackState(key, Instant.now());
-        BtsExcelFixture.FixtureRecord next = nextFixtureStep(current.cycleNo(), current.stepNo());
-        if (next == null)
-        {
-            BtsExcelFixture.FixtureRecord last = BtsExcelFixture.record(BtsExcelFixture.recordCount());
-            savePlayback(key, new BtsPlaybackState(false, current.speedMultiplier(), last.cycleNo(), last.stepNo(),
-                    last.stepName(), last.stepType(), current.simulatedAt(), Instant.now(),
-                    BtsExcelFixture.recordCount() + 1L, true));
-            return;
-        }
-        savePlayback(key, new BtsPlaybackState(current.playing(), current.speedMultiplier(), next.cycleNo(), next.stepNo(),
-                next.stepName(), next.stepType(), current.simulatedAt(), Instant.now(), next.sequenceId(), false));
-    }
-
-    /** 跳转到下一循环的首工步;不补齐当前循环缺失的首尾工步。 */
-    public synchronized void nextBtsCycle(int deviceId, int channelId)
-    {
-        DeviceState device = requireDevice(deviceId);
-        requireChannel(device, channelId);
-        String key = btsKey(device, channelId);
-        BtsPlaybackState current = playbackState(key, Instant.now());
-        BtsExcelFixture.FixtureRecord next = BtsExcelFixture.records().stream()
-                .filter(item -> item.cycleNo() > current.cycleNo()).findFirst().orElse(null);
-        if (next == null)
-        {
-            BtsExcelFixture.FixtureRecord last = BtsExcelFixture.record(BtsExcelFixture.recordCount());
-            savePlayback(key, new BtsPlaybackState(false, current.speedMultiplier(), last.cycleNo(), last.stepNo(),
-                    last.stepName(), last.stepType(), current.simulatedAt(), Instant.now(),
-                    BtsExcelFixture.recordCount() + 1L, true));
-            return;
-        }
-        savePlayback(key, new BtsPlaybackState(current.playing(), current.speedMultiplier(), next.cycleNo(), next.stepNo(),
-                next.stepName(), next.stepType(), current.simulatedAt(), Instant.now(), next.sequenceId(), false));
-    }
-
-    /** 清空单通道播放和原始数据,恢复到首循环首工步。 */
-    public synchronized void resetBts(int deviceId, int channelId)
-    {
-        DeviceState device = requireDevice(deviceId);
-        requireChannel(device, channelId);
-        String key = btsKey(device, channelId);
-        resetBtsFixtureLifecycle(key, Instant.now());
-    }
-
-    /** 返回播放状态及当前通道的 record、step、cycle 数据。 */
-    public synchronized PlaybackSnapshot btsPlayback(int deviceId, int channelId)
-    {
-        DeviceState device = requireDevice(deviceId);
-        requireChannel(device, channelId);
-        String key = btsKey(device, channelId);
-        BtsPlaybackState state = playbackState(key, Instant.now());
-        return new PlaybackSnapshot(deviceId, channelId, state, btsRecords(deviceId, channelId),
-                btsSteps(deviceId, channelId), btsCycles(deviceId, channelId));
-    }
-
-    /** 返回只追加的检测柜原始 record。 */
-    public synchronized List<BtsHistoryRecord> btsRecords(int deviceId, int channelId)
-    {
-        DeviceState device = requireDevice(deviceId);
-        requireChannel(device, channelId);
-        return List.copyOf(btsHistoryFor(btsKey(device, channelId)));
-    }
-
-    /** 返回开发模拟器当前通道的持久化槽位间隔;仅供开发调度层使用。 */
-    public synchronized long persistenceIntervalMillis(int deviceId, int channelId)
-    {
-        DeviceState device = requireDevice(deviceId);
-        requireChannel(device, channelId);
-        BtsPlaybackState playback = playbackState(btsKey(device, channelId), Instant.now());
-        return Math.max(100L, 60000L / Math.max(1, playback.speedMultiplier()));
-    }
-
+    /** 返回通道已配置的开发方案及当前工步位置,不自动推进或生成历史。 */
     @Override
     public synchronized TestPlan testPlan(ChannelIdentity channel)
     {
         if (!canObserve(channel)) return null;
-        var plan = btsTest(channel.devid(), channel.chlid());
-        return new TestPlan(plan.currentCycleNo(), plan.currentStepNo(), plan.plannedCycleCount(),
-                plan.rows().stream().map(row -> new PlanRow(row.rowType(), row.stepNo(), row.cyclePosition(),
-                        row.stepName(), row.durationSeconds(), row.voltageV(), row.executionStatus(),
-                        row.cycleCount(), row.recordCount())).toList());
+        ChannelState source = requireDevice(channel.devid()).channels().get(channel.chlid() - 1);
+        ChannelConfiguration configuration = source.configuration();
+        List<PlanRow> rows = new ArrayList<>();
+        for (int index = 0; index < configuration.steps().size(); index++) {
+            var step = configuration.steps().get(index);
+            String status = index + 1 < configuration.stepNo() ? "已完成" : "待执行";
+            if (index + 1 == configuration.stepNo()) status = switch (source.workStatus()) {
+                case WORKING -> "进行中";
+                case PAUSED -> "暂停";
+                case FINISHED -> "已完成";
+                case PROTECTED -> "异常";
+                default -> "待执行";
+            };
+            // cyclePosition 表示循环内的工步位置,不能使用当前循环号,否则控制台会合并不同工步。
+            rows.add(new PlanRow("STEP", index + 1, index + 1, step.name(), step.durationSeconds(),
+                    step.voltageV(), status, null, null));
+        }
+        return new TestPlan(configuration.cycleNo(), configuration.stepNo(), configuration.plannedCycleCount(), rows);
     }
 
+    /** 请求当前检测柜测量;离线或无法观测时不返回缓存值。 */
     @Override
     public synchronized CabinetRealtime latestCabinet(ChannelIdentity channel)
     {
-        if (!canObserve(channel)) return null;
-        BtsHistoryRecord record = btsRecords(channel.devid(), channel.chlid()).stream()
-                .max(java.util.Comparator.comparing(BtsHistoryRecord::deviceTime)).orElse(null);
-        return record == null ? null : new CabinetRealtime(record.sequenceId(), record.deviceTime(), record.currentA(),
-                record.voltageV(), record.capacityAh(), record.energyWh(), record.cycleNo(), record.stepNo(),
-                record.stepName(), record.stepType(), record.serverReceivedAt(), record.deviceTime().toString(), "UTC");
+        CabinetRealtime result = canObserve(channel) ? cabinetMeasurement(channel) : null;
+        recordRequest("cabinet-realtime", String.valueOf(channel),
+                result == null ? failureReason() : result.toString());
+        return result;
     }
 
-    @Override
-    public synchronized CollectorRealtime latestCollector(String collectorCode)
+    private CabinetRealtime cabinetMeasurement(ChannelIdentity identity)
     {
-        var collector = snapshot().collectors().stream().filter(item -> item.deviceCode().equals(collectorCode))
+        DeviceState device = requireDevice(identity.devid());
+        ChannelState channel = device.channels().get(identity.chlid() - 1);
+        if (channel.workStatus() == DeviceWorkStatus.UNOBSERVABLE) return null;
+        Instant now = Instant.now().truncatedTo(java.time.temporal.ChronoUnit.MILLIS);
+        long sequence = nextObservationSequence();
+        ChannelConfiguration configuration = channel.configuration();
+        var step = configuration.steps().get(configuration.stepNo() - 1);
+        // 保存值直接成为当前观测;工作状态和设备量程不反向覆盖操作员配置。
+        CabinetRealtime result = new CabinetRealtime(sequence, now, configuration.currentA().doubleValue(),
+                configuration.voltageV().doubleValue(), configuration.capacityAh().doubleValue(), configuration.energyWh().doubleValue(),
+                configuration.cycleNo(), configuration.stepNo(), step.name(), step.stepType(),
+                now, DEVICE_TIME.format(now.atZone(DEVICE_ZONE)), DEVICE_ZONE.getId(), configuration.relativeTimeSeconds());
+        return result;
+    }
+
+    /** 请求采集器当前配置值;软件不可访问或设备离线时返回空测量。 */
+    @Override
+    public synchronized CollectorRealtime latestCollector(String adapterCode, String collectorCode)
+    {
+        CollectorState collector = state.collectors().stream().filter(item -> item.deviceCode().equals(collectorCode))
                 .findFirst().orElse(null);
         if (collector == null) return null;
-        CollectorRealtimeSnapshot snapshot = collectorRealtime(collector.collectorId());
+        var snapshot = collectorRealtime(collector.collectorId());
         return new CollectorRealtime(snapshot.collectorCode(), snapshot.online(), snapshot.sampleIntervalMs(),
                 snapshot.measurements().stream().map(item -> new CollectorMeasurement(item.channelCode(), item.sequenceId(),
                         item.deviceTime(), item.deviceTimeRaw(), item.serverReceivedAt(), item.metricCode(), item.value(),
-                        item.unit())).toList(), "Asia/Hong_Kong");
+                        item.unit())).toList(), DEVICE_ZONE.getId());
     }
 
+    /** 模拟器不提供历史工步统计。 */
     @Override
-    public synchronized List<StepRecord> stepRecords(ChannelIdentity channel)
-    {
-        if (!canObserve(channel)) return List.of();
-        var playback = playbackState(btsKey(requireDevice(channel.devid()), channel.chlid()), Instant.now());
-        // 仅发布已离开的工步;手动跳转也可结束工步,不用记录数作为完整性门禁。
-        return btsSteps(channel.devid(), channel.chlid()).stream().filter(item -> playback.naturallyFinished()
-                || item.cycleNo() != playback.cycleNo() || item.stepNo() != playback.stepNo()).map(item -> new StepRecord(item.recordNo(),
-                item.cycleNo(), item.stepNo(), item.stepName(), item.stepType(), item.startedAt(), item.endedAt(),
-                item.endVoltageV(), item.capacityAh(), item.energyWh(), item.endedAt().toString(), "UTC", receivedAt(channel, item.endedAt()),
-                java.util.Map.of("stepDuration", java.math.BigDecimal.valueOf(item.durationSeconds()),
-                        "startVoltage", java.math.BigDecimal.valueOf(item.startVoltageV()),
-                        "chargePeakVoltage", java.math.BigDecimal.valueOf(item.chargePeakVoltageV()),
-                        "dischargeValleyVoltage", java.math.BigDecimal.valueOf(item.dischargeValleyVoltageV())))).toList();
-    }
+    public List<StepRecord> stepRecords(ChannelIdentity channel) { return List.of(); }
 
+    /** 模拟器不提供历史循环统计。 */
     @Override
-    public synchronized List<CycleRecord> cycleRecords(ChannelIdentity channel)
-    {
-        if (!canObserve(channel)) return List.of();
-        var playback = playbackState(btsKey(requireDevice(channel.devid()), channel.chlid()), Instant.now());
-        return btsCycles(channel.devid(), channel.chlid()).stream().filter(item -> playback.naturallyFinished()
-                || item.cycleNo() != playback.cycleNo()).map(item -> new CycleRecord(item.recordNo(),
-                item.cycleNo(), item.startedAt(), item.endedAt(), item.chargeCapacityAh(), item.dischargeCapacityAh(),
-                item.chargeEnergyWh(), item.dischargeEnergyWh(), item.efficiency(), item.endedAt().toString(), "UTC", receivedAt(channel, item.endedAt()))).toList();
-    }
+    public List<CycleRecord> cycleRecords(ChannelIdentity channel) { return List.of(); }
 
+    /** 使用后端正常持久化周期,不提供开发倍速。 */
     @Override
-    public synchronized long persistenceIntervalMillis(ChannelIdentity channel)
-    { return canObserve(channel) ? persistenceIntervalMillis(channel.devid(), channel.chlid()) : 60000L; }
+    public long persistenceIntervalMillis(ChannelIdentity channel) { return 60000L; }
 
     private boolean canObserve(ChannelIdentity channel)
     {
-        // 全身份与在线事实共同限定来源,避免同编号的其他适配器被写入模拟数据。
-        return channel != null && observe(java.util.Set.of(channel)).get(channel).connectionStatus() == DeviceConnectionStatus.ONLINE;
+        // 完整来源身份限定数据归属;不可访问和离线只改变观测结果。
+        return channel != null && observation(channel).connectionStatus() == DeviceConnectionStatus.ONLINE
+                && observation(channel).workStatus() != DeviceWorkStatus.UNOBSERVABLE;
     }
 
-    private Instant receivedAt(ChannelIdentity channel, Instant endedAt)
-    {
-        return btsRecords(channel.devid(), channel.chlid()).stream().filter(row -> row.deviceTime().equals(endedAt))
-                .map(BtsHistoryRecord::serverReceivedAt).filter(java.util.Objects::nonNull).max(Instant::compareTo).orElse(null);
-    }
-
-    /** 按实际出现的循环和工步汇总 record,不补齐缺失行。 */
-    public synchronized List<BtsStepRecord> btsSteps(int deviceId, int channelId)
-    {
-        java.util.Map<String, BtsStepAccumulator> grouped = new java.util.LinkedHashMap<>();
-        for (BtsHistoryRecord item : btsRecords(deviceId, channelId))
-        {
-            String key = item.cycleNo() + ":" + item.stepNo();
-            int position = BtsExcelFixture.records().stream()
-                    .filter(record -> record.cycleNo() == item.cycleNo())
-                    .map(BtsExcelFixture.FixtureRecord::stepNo).distinct().toList().indexOf(item.stepNo()) + 1;
-            grouped.computeIfAbsent(key, ignored -> new BtsStepAccumulator(item, position)).add(item);
-        }
-        return grouped.values().stream().map(BtsStepAccumulator::finish).toList();
-    }
-
-    /** 按实际出现的循环汇总 record,不增加测试结束统计行。 */
-    public synchronized List<BtsCycleRecord> btsCycles(int deviceId, int channelId)
-    {
-        java.util.Map<Integer, BtsCycleAccumulator> grouped = new java.util.LinkedHashMap<>();
-        for (BtsHistoryRecord item : btsRecords(deviceId, channelId))
-            grouped.computeIfAbsent(item.cycleNo(), ignored -> new BtsCycleAccumulator(item)).add(item);
-        return grouped.entrySet().stream().map(entry -> {
-            BtsCycleRecord calculated = entry.getValue().finish();
-            BtsExcelFixture.CycleSummary expected = BtsExcelFixture.cycle(entry.getKey());
-            String recordNo = "CYCLE-" + deviceId + "-" + channelId + "-" + entry.getKey();
-            if (expected == null || entry.getValue().count < expected.recordCount())
-                return new BtsCycleRecord(recordNo, calculated.cycleNo(), calculated.recordCount(), calculated.startedAt(), calculated.endedAt(),
-                        calculated.chargeCapacityAh(), calculated.dischargeCapacityAh(), calculated.chargeEnergyWh(), calculated.dischargeEnergyWh(), calculated.efficiency());
-            return new BtsCycleRecord(recordNo, calculated.cycleNo(), expected.recordCount(), calculated.startedAt(),
-                    calculated.endedAt(), expected.chargeCapacityAh(), expected.dischargeCapacityAh(),
-                    expected.chargeEnergyWh(), expected.dischargeEnergyWh(),
-                    efficiency(expected.chargeCapacityAh(), expected.dischargeCapacityAh()));
-        }).toList();
-    }
-
-    /** 返回检测柜测试方案事实;方案行来自 Excel 夹具,当前状态来自该通道播放状态。 */
-    public synchronized BtsTestSnapshot btsTest(int deviceId, int channelId)
-    { return btsTest(deviceId, channelId, null); }
-
-    /** 返回指定循环的检测柜测试方案;未开始接收的测试固定使用第一个循环作为核对上下文。 */
-    public synchronized BtsTestSnapshot btsTest(int deviceId, int channelId, Integer forcedCycleNo)
-    {
-        DeviceState device = requireDevice(deviceId);
-        requireChannel(device, channelId);
-        BtsPlaybackState playback = playbackState(btsKey(device, channelId), Instant.now());
-        int requestedCycleNo = forcedCycleNo == null ? playback.cycleNo() : forcedCycleNo;
-        int currentCycleNo = requestedCycleNo;
-        List<BtsExcelFixture.FixtureRecord> currentCycleRecords = BtsExcelFixture.records().stream()
-                .filter(item -> item.cycleNo() == requestedCycleNo).toList();
-        if (currentCycleRecords.isEmpty())
-        {
-            int fallbackCycleNo = BtsExcelFixture.records().get(0).cycleNo();
-            currentCycleNo = fallbackCycleNo;
-            currentCycleRecords = BtsExcelFixture.records().stream()
-                    .filter(item -> item.cycleNo() == fallbackCycleNo).toList();
-        }
-        java.util.Map<Integer, BtsExcelFixture.FixtureRecord> firstByStep = new java.util.LinkedHashMap<>();
-        for (BtsExcelFixture.FixtureRecord item : currentCycleRecords)
-            firstByStep.putIfAbsent(item.stepNo(), item);
-
-        List<BtsTestPlanRow> rows = new ArrayList<>();
-        int cyclePosition = 1;
-        for (BtsExcelFixture.FixtureRecord first : firstByStep.values())
-        {
-            List<BtsExcelFixture.FixtureRecord> sameCycle = currentCycleRecords.stream()
-                    .filter(item -> item.stepNo() == first.stepNo()).toList();
-            BtsExcelFixture.FixtureRecord last = sameCycle.get(sameCycle.size() - 1);
-            // step 号在每个循环内重复,跨循环时必须按当前循环的工步位置判断,不能把上一循环的同号工步当成当前状态。
-            String status = forcedCycleNo != null ? "待执行" : playback.naturallyFinished() ? "已完成"
-                    : playback.stepNo() > first.stepNo() ? "已完成"
-                            : playback.stepNo() == first.stepNo() ? "进行中" : "待执行";
-            rows.add(new BtsTestPlanRow("STEP", first.stepNo(), cyclePosition++, first.stepName(),
-                    Math.max(0d, last.offsetSeconds() - first.offsetSeconds()), first.voltageV(), status,
-                    null, null));
-        }
-        rows.add(new BtsTestPlanRow("CYCLE", BtsExcelFixture.startStepNo(), null, "循环", 0d, null,
-                forcedCycleNo != null ? "待执行" : playback.naturallyFinished() ? "已完成" : "进行中", currentCycleNo,
-                (long) currentCycleRecords.size()));
-        rows.add(new BtsTestPlanRow("END", 0, null, "结束", 0d, null,
-                forcedCycleNo != null ? "待执行" : playback.naturallyFinished() ? "已完成" : "待执行", null, (long) BtsExcelFixture.recordCount()));
-        return new BtsTestSnapshot(currentCycleNo, playback.stepNo(), BtsExcelFixture.cycleCount(), rows);
-    }
-
-    /** 返回采集器最新实时快照;快照与分页历史保持不同读取路径。 */
+    /** 请求采集器当前观测,读取状态页面本身不会触发此请求。 */
     public synchronized CollectorRealtimeSnapshot collectorRealtime(int collectorId)
     {
-        return DevelopmentCollectorSimulation.realtime(requireCollector(collectorId));
-    }
-
-    /** 分页下载采集器模拟历史,并应用已配置的重复、冲突、乱序或缺失场景。 */
-    public synchronized CollectorHistoryPage collectorHistory(int collectorId, long offset, int limit)
-    {
         CollectorState collector = requireCollector(collectorId);
-        return DevelopmentCollectorSimulation.history(collector, stateStore, offset, limit);
+        boolean available = state.softwareAccessible() && collector.online();
+        if (available)
+        {
+            collector = DevelopmentCollectorSimulation.sample(collector, Instant.now(), nextObservationSequence());
+            replaceCollector(collector);
+        }
+        var result = new CollectorRealtimeSnapshot(collector.deviceCode(), available, collector.sampleIntervalMs(),
+                available ? collector.latestMeasurements() : List.of());
+        recordRequest("collector-realtime", collector.deviceCode(), available ? result.toString() : failureReason());
+        return result;
     }
 
-    /** 返回控制页需要的运行状态快照。 */
+    private String failureReason()
+    {
+        return state.softwareAccessible() ? "UNOBSERVABLE" : "SOFTWARE_UNAVAILABLE";
+    }
+
+    private long nextObservationSequence()
+    {
+        long sequence = Math.max(state.observationSequence() + 1, Instant.now().toEpochMilli());
+        SimulatorState next = new SimulatorState(state.version(), state.devices(), state.collectors(),
+                state.nextDeviceId(), state.nextCollectorId(), state.tombstones(), state.softwareAccessible(), sequence);
+        stateStore.save(next);
+        state = next;
+        return sequence;
+    }
+
+    /** 返回模拟器配置、当前状态和最近请求,不触发新的设备观测。 */
     public synchronized StateSnapshot snapshot()
     {
         List<DeviceSnapshot> deviceSnapshots = state.devices().stream()
                 .map(device -> new DeviceSnapshot(device.deviceId(), device.ip(), device.online(), device.profile(),
                         device.channels().stream()
                                 .map(channel -> new ChannelSnapshot(channel.channelId(), channel.online(),
-                                        channel.workStatus()))
+                                        channel.workStatus(), channel.configuration()))
                                 .toList()))
                 .toList();
         List<CollectorSnapshot> collectorSnapshots = state.collectors().stream()
                 .map(collector -> new CollectorSnapshot(collector.collectorId(), collector.deviceCode(),
                         collector.ip(), collector.mac(), collector.name(), collector.manufacturer(),
                         collector.model(), collector.channelCount(), collector.online(), collector.sampleIntervalMs(),
-                        collector.channels(), collector.historyFaults(), collector.historyRecordCount(),
-                        DevelopmentCollectorSimulation.realtime(collector).measurements()))
+                        collector.channels(), state.softwareAccessible() && collector.online() ? collector.latestMeasurements() : List.of()))
                 .toList();
-        return new StateSnapshot(running, HOST + ":" + port(), deviceSnapshots, collectorSnapshots,
+        return new StateSnapshot(running, state.softwareAccessible(), HOST + ":" + port(), deviceSnapshots, collectorSnapshots,
                 stateStore.loadRecentRequests());
     }
 
@@ -696,7 +495,6 @@
     {
         SimulatorState defaultState = defaultState();
         stateStore.reset(defaultState);
-        btsHistory.clear();
         state = defaultState;
     }
 
@@ -735,24 +533,23 @@
                 state.nextCollectorId(), state.tombstones()));
     }
 
-    private String respond(String request)
+    private synchronized String respond(String request)
     {
         String command = readCommand(request);
-        recordRequest(command, request);
-        return switch (command)
+        String response = state.softwareAccessible() ? switch (command)
         {
             case "getdevinfo" -> deviceInfoResponse();
             case "inquire" -> observationResponse(request);
-            case "inquiredf" -> historyAvailabilityResponse(request);
-            case "download" -> historyResponse(request);
             default -> "<bts version=\"1.0\"><cmd>error</cmd></bts>";
-        };
+        } : "<bts version=\"1.0\"><cmd>error</cmd></bts>";
+        recordRequest(command, request, state.softwareAccessible() ? response : "SOFTWARE_UNAVAILABLE");
+        return response;
     }
 
-    private synchronized void recordRequest(String command, String request)
+    private void recordRequest(String command, String request, String response)
     {
         List<RecentRequest> recentRequests = new ArrayList<>(stateStore.loadRecentRequests());
-        recentRequests.add(0, new RecentRequest(Instant.now(), command, request));
+        recentRequests.add(0, new RecentRequest(Instant.now(), command, request, response));
         if (recentRequests.size() > MAX_RECENT_REQUESTS)
             recentRequests = new ArrayList<>(recentRequests.subList(0, MAX_RECENT_REQUESTS));
         stateStore.saveRecentRequests(recentRequests);
@@ -825,13 +622,13 @@
 
     private synchronized String observationResponse(String request)
     {
-        long tick = ++observationSequence;
         java.util.Set<String> requested = requestedChannels(request);
         List<ObservedChannel> available = new ArrayList<>();
         for (DeviceState device : state.devices())
             for (ChannelState channel : device.channels())
-                if (device.online() && channel.online()
-                        && (requested.isEmpty() || requested.contains(device.deviceId() + ":" + channel.channelId())))
+                if (device.online() && channel.online() && channel.workStatus() != DeviceWorkStatus.UNOBSERVABLE
+                        && requested.contains(profileFor(device).devtype() + ":" + device.deviceId()
+                                + ":" + profileFor(device).subDeviceId() + ":" + channel.channelId()))
                     available.add(new ObservedChannel(device.deviceId(), channel));
 
         StringBuilder response = new StringBuilder("<bts version=\"1.0\"><cmd>inquire_resp</cmd><list count=\"")
@@ -841,29 +638,12 @@
             ChannelState channel = observed.channel();
             DeviceState device = requireDevice(observed.deviceId());
             BtsDeviceProfile profile = profileFor(device);
-            boolean working = channel.workStatus() == DeviceWorkStatus.WORKING;
-            double wave = Math.sin((tick + channel.channelId()) * 0.7d);
-            double current = working ? clamp(profile.currentMaxA().doubleValue() * (0.18d + wave * 0.02d),
-                    profile.currentMinA().doubleValue(), profile.currentMaxA().doubleValue()) : 0d;
-            double voltage = clamp(3.7d + wave * 0.01d, profile.voltageMinV().doubleValue(),
-                    profile.voltageMaxV().doubleValue());
-            String key = historyKey(profile.devtype(), observed.deviceId(), profile.subDeviceId(), channel.channelId());
-            BtsPlaybackState playback = playbackState(key, Instant.now());
-            double capacity = working ? latestCapacity(key) : 0d;
-            double energy = capacity * voltage;
-            if (working)
-            {
-                playback = advanceBtsPlayback(key, profile, playback, current, voltage);
-                BtsHistoryRecord latest = latestRecord(key);
-                if (latest != null)
-                {
-                    capacity = latest.capacityAh();
-                    energy = latest.energyWh();
-                }
-            }
-            int cycleNo = playback.cycleNo();
-            int stepNo = playback.stepNo();
-            String stepType = playback.stepType();
+            CabinetRealtime measurement = cabinetMeasurement(new ChannelIdentity(ADAPTER_CODE, profile.devtype(),
+                    device.deviceId(), profile.subDeviceId(), channel.channelId()));
+            double current = measurement.currentA(), voltage = measurement.voltageV();
+            double capacity = measurement.capacityAh(), energy = measurement.energyWh();
+            int cycleNo = measurement.cycleNo(), stepNo = measurement.stepNo();
+            String stepType = measurement.stepType();
             response.append("<inquire dev=\"").append(profile.devtype()).append('-').append(observed.deviceId())
                     .append('-').append(profile.subDeviceId()).append('-').append(channel.channelId())
                     .append("-0\" cycle_id=\"").append(cycleNo).append("\" step_id=\"").append(stepNo)
@@ -872,267 +652,14 @@
                     .append("\" current=\"").append(decimal(current)).append("\" voltage=\"")
                     .append(decimal(voltage)).append("\" capacity=\"").append(decimal(capacity))
                     .append("\" energy=\"").append(decimal(energy)).append("\" totaltime=\"")
-                    .append(working ? tick : 0).append("\" relativetime=\"").append(working ? tick : 0)
+                    .append(channel.configuration().totalTimeSeconds()).append("\" relativetime=\"")
+                    .append(channel.configuration().relativeTimeSeconds())
                     .append("\" open_or_close=\"0\" log_code=\"0\">")
                     // 模拟器返回当前设备方案,供统一观测模型展示;不代表真实 BTS 的完整方案字段。
                     .append("<plan><step step_no=\"").append(stepNo).append("\" step_type=\"")
                     .append(stepType).append("\" name=\"").append(stepType).append("\"/></plan></inquire>");
         }
         return response.append("</list></bts>").toString();
-    }
-
-    private synchronized String historyAvailabilityResponse(String request)
-    {
-        java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("<chl\\s+([^>]*)/?>").matcher(request);
-        StringBuilder response = new StringBuilder("<bts version=\"1.0\"><cmd>inquiredf_resp</cmd><list>");
-        while (matcher.find())
-        {
-            String attrs = matcher.group(1);
-            int devtype = Integer.parseInt(attribute(attrs, "devtype"));
-            int devid = Integer.parseInt(attribute(attrs, "devid"));
-            int subdevid = Integer.parseInt(attribute(attrs, "subdevid"));
-            int chlid = Integer.parseInt(attribute(attrs, "chlid"));
-            int count = btsHistoryFor(historyKey(devtype, devid, subdevid, chlid)).size();
-            response.append("<chl devtype=\"").append(devtype).append("\" devid=\"").append(devid)
-                    .append("\" subdevid=\"").append(subdevid).append("\" chlid=\"").append(chlid)
-                    .append("\" testid=\"SIM-").append(devid).append('-').append(chlid).append("\" count=\"")
-                    .append(count).append("\">true</chl>");
-        }
-        return response.append("</list></bts>").toString();
-    }
-
-    private synchronized String historyResponse(String request)
-    {
-        int devtype = Integer.parseInt(attribute(request, "devtype"));
-        int devid = Integer.parseInt(attribute(request, "devid"));
-        int subdevid = Integer.parseInt(attribute(request, "subdevid"));
-        int chlid = Integer.parseInt(attribute(request, "chlid"));
-        String testId = attribute(request, "testid");
-        long start = Long.parseLong(attribute(request, "startpos"));
-        int count = Integer.parseInt(attribute(request, "count"));
-        List<BtsHistoryRecord> history = btsHistoryFor(historyKey(devtype, devid, subdevid, chlid));
-        int from = (int) Math.min(Math.max(0L, start - 1L), history.size());
-        int to = Math.min(history.size(), from + Math.max(1, count));
-        StringBuilder response = new StringBuilder("<bts version=\"1.0\"><cmd>download_resp</cmd><download devtype=\"")
-                .append(devtype).append("\" devid=\"").append(devid).append("\" subdevid=\"").append(subdevid)
-                .append("\" chlid=\"").append(chlid).append("\" testid=\"").append(testId)
-                .append("\"><list>");
-        for (BtsHistoryRecord item : history.subList(from, to))
-            response.append("<data seqid=\"").append(item.sequenceId()).append("\" stepid=\"").append(item.stepNo())
-                    .append("\" cycleid=\"").append(item.cycleNo()).append("\" steptype=\"").append(item.stepType()).append("\" atime=\"")
-                    .append(BTS_HISTORY_TIME.format(item.deviceTime())).append("\" testtime=\"60000\" volt=\"")
-                    .append(decimal(item.voltageV())).append("\" curr=\"").append(decimal(item.currentA()))
-                    .append("\" cap=\"").append(decimal(item.capacityAh())).append("\" eng=\"").append(decimal(item.energyWh())).append("\"/>");
-        return response.append("</list></download></bts>").toString();
-    }
-
-    private String attribute(String xml, String name)
-    {
-        java.util.regex.Matcher matcher = java.util.regex.Pattern.compile(name + "=\\\"([^\\\"]+)\\\"").matcher(xml);
-        if (!matcher.find()) throw new IllegalArgumentException("Missing simulator attribute: " + name);
-        return matcher.group(1);
-    }
-
-    private String historyKey(int devtype, int devid, int subdevid, int chlid)
-    {
-        return devtype + ":" + devid + ":" + subdevid + ":" + chlid;
-    }
-
-    private void restoreBtsHistory()
-    {
-        for (DeviceState device : state.devices())
-        {
-            BtsDeviceProfile profile = profileFor(device);
-            for (ChannelState channel : device.channels())
-            {
-                String key = historyKey(profile.devtype(), device.deviceId(), profile.subDeviceId(), channel.channelId());
-                ArrayList<BtsHistoryRecord> restored = new ArrayList<>();
-                long offset = 0;
-                while (true)
-                {
-                    var page = stateStore.loadBtsHistoryPage(key, offset, BtsSimulatorStateStore.MAX_HISTORY_BATCH_SIZE);
-                    restored.addAll(page.records().stream().map(this::normalizeHistoryRecord).toList());
-                    if (!page.hasMore()) break;
-                    if (page.nextOffset() <= offset) throw new IllegalStateException("Invalid BTS history page");
-                    offset = page.nextOffset();
-                }
-                if (!restored.isEmpty()) btsHistory.put(key, restored);
-            }
-        }
-    }
-
-    private List<BtsHistoryRecord> btsHistoryFor(String key)
-    {
-        return btsHistory.getOrDefault(key, List.of());
-    }
-
-    private BtsHistoryRecord normalizeHistoryRecord(BtsHistoryRecord item)
-    {
-        return item.stepName() == null || item.stepName().isBlank()
-                ? new BtsHistoryRecord(item.sequenceId(), item.deviceTime(), item.currentA(), item.voltageV(),
-                        item.capacityAh(), item.energyWh(), item.cycleNo(), item.stepNo(), item.stepName(), item.stepType(),
-                        item.serverReceivedAt()) : item;
-    }
-
-    private String btsKey(DeviceState device, int channelId)
-    {
-        BtsDeviceProfile profile = profileFor(device);
-        return historyKey(profile.devtype(), device.deviceId(), profile.subDeviceId(), channelId);
-    }
-
-    private void requireChannel(DeviceState device, int channelId)
-    {
-        if (channelId < 1 || channelId > device.channels().size())
-            throw new IllegalArgumentException("channelId must be between 1 and " + device.channels().size());
-    }
-
-    private BtsPlaybackState playbackState(String key, Instant now)
-    {
-        BtsPlaybackState stored = stateStore.loadBtsPlaybackState(key);
-        if (stored != null) return stored;
-        List<BtsHistoryRecord> records = btsHistoryFor(key);
-        long next = records.stream().mapToLong(BtsHistoryRecord::sequenceId).max().orElse(0L) + 1L;
-        BtsExcelFixture.FixtureRecord first = BtsExcelFixture.record(1);
-        BtsPlaybackState created = new BtsPlaybackState(true, 1, first.cycleNo(), first.stepNo(), first.stepName(), first.stepType(),
-                now.minusSeconds(SIMULATED_RECORD_INTERVAL_SECONDS), now, next, false);
-        stateStore.saveBtsPlaybackState(key, created);
-        return created;
-    }
-
-    private BtsHistoryRecord latestRecord(String key)
-    {
-        List<BtsHistoryRecord> records = btsHistoryFor(key);
-        return records.isEmpty() ? null : records.get(records.size() - 1);
-    }
-
-    private double latestCapacity(String key)
-    {
-        BtsHistoryRecord latest = latestRecord(key);
-        return latest == null ? 0d : latest.capacityAh();
-    }
-
-    private BtsPlaybackState advanceBtsPlayback(String key, BtsDeviceProfile profile, BtsPlaybackState current,
-            double currentA, double voltageV)
-    {
-        return advanceBtsPlayback(key, profile, current, currentA, voltageV, Instant.now());
-    }
-
-    private BtsPlaybackState advanceBtsPlayback(String key, BtsDeviceProfile profile, BtsPlaybackState current,
-            double currentA, double voltageV, Instant now)
-    {
-        if (!current.playing() || current.naturallyFinished()) return current;
-        long elapsedMillis = Math.max(0L, java.time.Duration.between(current.wallClockAt(), now).toMillis())
-                * current.speedMultiplier();
-        List<BtsHistoryRecord> history = new ArrayList<>(btsHistoryFor(key));
-        List<BtsHistoryRecord> appended = new ArrayList<>();
-        Instant simulated = current.simulatedAt();
-        long sequence = current.nextSequence();
-        if (sequence < 1) sequence = 1;
-        if (sequence > BtsExcelFixture.recordCount() + 1L)
-            sequence = BtsExcelFixture.recordCount() + 1L;
-        long lastOffset = sequence == 1 ? -SIMULATED_RECORD_INTERVAL_SECONDS
-                : BtsExcelFixture.record(sequence - 1).offsetSeconds();
-        long budgetMillis = elapsedMillis;
-        while (sequence <= BtsExcelFixture.recordCount())
-        {
-            BtsExcelFixture.FixtureRecord fixture = BtsExcelFixture.record(sequence);
-            long deltaSeconds = fixture.offsetSeconds() - lastOffset;
-            long deltaMillis = Math.max(1L, deltaSeconds * 1000L);
-            boolean firstRecord = history.isEmpty() && sequence == 1L;
-            if (!firstRecord && budgetMillis < deltaMillis) break;
-            budgetMillis = Math.max(0L, budgetMillis - deltaMillis);
-            simulated = simulated.plusSeconds(deltaSeconds);
-            BtsHistoryRecord record = new BtsHistoryRecord(fixture.sequenceId(), simulated, fixture.currentA(), fixture.voltageV(),
-                    fixture.capacityAh(), fixture.energyWh(), fixture.cycleNo(), fixture.stepNo(), fixture.stepName(), fixture.stepType(), now);
-            history.add(record);
-            appended.add(record);
-            lastOffset = fixture.offsetSeconds();
-            sequence++;
-        }
-        if (appended.isEmpty()) return current;
-        if (history.size() > BtsSimulatorStateStore.MAX_HISTORY_RECORDS)
-            history = new ArrayList<>(history.subList(history.size() - (int) BtsSimulatorStateStore.MAX_HISTORY_RECORDS,
-                    history.size()));
-        BtsExcelFixture.FixtureRecord latest = BtsExcelFixture.record(sequence - 1L);
-        boolean finished = sequence > BtsExcelFixture.recordCount();
-        BtsPlaybackState next = new BtsPlaybackState(!finished && current.playing(), current.speedMultiplier(),
-                latest.cycleNo(), latest.stepNo(), latest.stepName(), latest.stepType(), simulated, now, sequence, finished);
-        stateStore.saveBtsPlaybackAndAppendHistory(key, next, appended);
-        btsHistory.put(key, history);
-        return next;
-    }
-
-    private BtsPlaybackState defaultPlayback(Instant now)
-    {
-        BtsExcelFixture.FixtureRecord first = BtsExcelFixture.record(1);
-        return new BtsPlaybackState(true, 1, first.cycleNo(), first.stepNo(), first.stepName(), first.stepType(),
-                now.minusSeconds(SIMULATED_RECORD_INTERVAL_SECONDS), now, 1, false);
-    }
-
-    private BtsExcelFixture.FixtureRecord nextFixtureStep(int cycleNo, int stepNo)
-    {
-        return BtsExcelFixture.records().stream()
-                .filter(item -> item.cycleNo() > cycleNo || (item.cycleNo() == cycleNo && item.stepNo() > stepNo))
-                .findFirst().orElse(null);
-    }
-
-    private void savePlayback(String key, BtsPlaybackState playback)
-    {
-        stateStore.saveBtsPlaybackState(key, playback);
-    }
-
-    public record BtsStepRecord(int cycleNo, int stepNo, Integer cyclePosition, String stepName, String stepType, long recordCount,
-            Instant startedAt, Instant endedAt, double durationSeconds, double capacityAh, double energyWh,
-            double startVoltageV, double endVoltageV, double chargePeakVoltageV, double dischargeValleyVoltageV,
-            String recordNo)
-    {
-        public BtsStepRecord(int cycleNo, int stepNo, Integer cyclePosition, String stepName, String stepType,
-                long recordCount, Instant startedAt, Instant endedAt, double durationSeconds, double capacityAh,
-                double energyWh, double startVoltageV, double endVoltageV, double chargePeakVoltageV,
-                double dischargeValleyVoltageV)
-        {
-            this(cycleNo, stepNo, cyclePosition, stepName, stepType, recordCount, startedAt, endedAt,
-                    durationSeconds, capacityAh, energyWh, startVoltageV, endVoltageV, chargePeakVoltageV,
-                    dischargeValleyVoltageV, null);
-        }
-    }
-
-    public record BtsCycleRecord(String recordNo, int cycleNo, long recordCount, Instant startedAt, Instant endedAt,
-            double chargeCapacityAh, double dischargeCapacityAh, double chargeEnergyWh, double dischargeEnergyWh,
-            double efficiency) { }
-
-    public record BtsTestPlanRow(String rowType, int stepNo, Integer cyclePosition, String stepName, double durationSeconds,
-            Double voltageV, String executionStatus, Integer cycleCount, Long recordCount) { }
-
-    public record BtsTestSnapshot(int currentCycleNo, int currentStepNo, int plannedCycleCount,
-            List<BtsTestPlanRow> rows) { }
-
-    private static double efficiency(double chargeCapacityAh, double dischargeCapacityAh)
-    {
-        return chargeCapacityAh > 0d ? dischargeCapacityAh / chargeCapacityAh * 100d : 0d;
-    }
-
-    public record PlaybackSnapshot(int deviceId, int channelId, BtsPlaybackState playback,
-            List<BtsHistoryRecord> records, List<BtsStepRecord> steps, List<BtsCycleRecord> cycles) { }
-
-    private static final class BtsStepAccumulator
-    {
-        private final int cycle; private final int step; private final int position; private final String name; private final String type;
-        private long count; private Instant first; private Instant last; private double duration; private double capacity; private double energy;
-        private double startVoltage; private double endVoltage; private double chargePeak = Double.NEGATIVE_INFINITY; private double dischargeValley = Double.POSITIVE_INFINITY;
-        BtsStepAccumulator(BtsHistoryRecord first, int position) { cycle = first.cycleNo(); step = first.stepNo(); this.position = position; name = first.stepName(); type = first.stepType(); }
-        void add(BtsHistoryRecord item) { count++; if (first == null) { first = item.deviceTime(); startVoltage = item.voltageV(); } last = item.deviceTime(); endVoltage = item.voltageV(); duration = first == null ? 0 : Math.max(0, java.time.Duration.between(first, last).toMillis() / 1000d); capacity = item.capacityAh(); energy = item.energyWh(); if (item.currentA() >= 0) chargePeak = Math.max(chargePeak, item.voltageV()); else dischargeValley = Math.min(dischargeValley, item.voltageV()); }
-        BtsStepRecord finish() { return new BtsStepRecord(cycle, step, position, name, type, count, first, last, duration, capacity, energy, startVoltage, endVoltage, chargePeak == Double.NEGATIVE_INFINITY ? 0 : chargePeak, dischargeValley == Double.POSITIVE_INFINITY ? 0 : dischargeValley, "STEP-" + cycle + "-" + step + "-" + last.toEpochMilli()); }
-    }
-
-    private static final class BtsCycleAccumulator
-    {
-        private final int cycle; private long count; private Instant first; private Instant last;
-        private double maximumCapacity = Double.NEGATIVE_INFINITY; private double minimumCapacity = Double.POSITIVE_INFINITY;
-        private double chargeEnergy; private double dischargeEnergy;
-        BtsCycleAccumulator(BtsHistoryRecord first) { cycle = first.cycleNo(); }
-        void add(BtsHistoryRecord item) { count++; if (first == null) first = item.deviceTime(); last = item.deviceTime(); maximumCapacity = Math.max(maximumCapacity, item.capacityAh()); minimumCapacity = Math.min(minimumCapacity, item.capacityAh()); if (item.currentA() >= 0) chargeEnergy = Math.max(chargeEnergy, item.energyWh()); else dischargeEnergy = Math.max(dischargeEnergy, Math.abs(item.energyWh())); }
-        BtsCycleRecord finish() { double charge = Math.max(0d, maximumCapacity); double discharge = Math.max(0d, maximumCapacity - minimumCapacity); return new BtsCycleRecord("", cycle, count, first, last, charge, discharge, chargeEnergy, dischargeEnergy, efficiency(charge, discharge)); }
     }
 
     private java.util.Set<String> requestedChannels(String request)
@@ -1150,7 +677,10 @@
                 {
                     String devid = reader.getAttributeValue(null, "devid");
                     String chlid = reader.getAttributeValue(null, "chlid");
-                    if (devid != null && chlid != null) channels.add(devid + ":" + chlid);
+                    String devtype = reader.getAttributeValue(null, "devtype");
+                    String subdevid = reader.getAttributeValue(null, "subdevid");
+                    if (devtype != null && devid != null && subdevid != null && chlid != null)
+                        channels.add(devtype + ":" + devid + ":" + subdevid + ":" + chlid);
                 }
             }
             return channels;
@@ -1204,14 +734,9 @@
         throw new IllegalArgumentException("Unsupported Neware simulator model: " + model);
     }
 
-    private double clamp(double value, double minimum, double maximum)
-    {
-        return Math.max(minimum, Math.min(maximum, value));
-    }
-
     private String decimal(double value)
     {
-        return String.format(java.util.Locale.ROOT, "%.3f", value);
+        return java.math.BigDecimal.valueOf(value).toPlainString();
     }
 
     private String toBtsWorkStatus(DeviceWorkStatus status)
@@ -1222,6 +747,7 @@
             case WORKING -> "working";
             case PAUSED -> "pause";
             case FINISHED -> "finish";
+            case PROTECTED -> "protect";
             default -> throw new IllegalArgumentException("Unsupported simulator work status: " + status);
         };
     }
@@ -1297,16 +823,12 @@
 
     private void persist(SimulatorState nextState)
     {
-        // 写穿失败时保留当前内存副本,避免控制页显示未持久化状态。
-        stateStore.save(nextState);
-        state = nextState;
-    }
-
-    private void persist(SimulatorState nextState, List<CollectorHistoryAppend> historyAppends)
-    {
-        // 主状态与原始历史批次必须一起成功,内存序号才能向前推进。
-        stateStore.save(nextState, historyAppends);
-        state = nextState;
+        // 写穿失败时保留当前内存副本,避免模拟器页面显示未持久化状态。
+        SimulatorState configured = new SimulatorState(nextState.version(), nextState.devices(), nextState.collectors(),
+                nextState.nextDeviceId(), nextState.nextCollectorId(), nextState.tombstones(),
+                state.softwareAccessible(), state.observationSequence());
+        stateStore.save(configured);
+        state = configured;
     }
 
     private String simulatorMac(int devtype, int devid)
@@ -1323,8 +845,9 @@
     private void requireControllableStatus(DeviceWorkStatus status)
     {
         if (status == null || (status != DeviceWorkStatus.STOPPED && status != DeviceWorkStatus.WORKING
-                && status != DeviceWorkStatus.PAUSED && status != DeviceWorkStatus.FINISHED))
-            throw new IllegalArgumentException("Simulator work status must be STOPPED, WORKING, PAUSED or FINISHED");
+                && status != DeviceWorkStatus.PAUSED && status != DeviceWorkStatus.FINISHED
+                && status != DeviceWorkStatus.PROTECTED && status != DeviceWorkStatus.UNOBSERVABLE))
+            throw new IllegalArgumentException("Unsupported simulator work status");
     }
 
     private void validateRestoredState()
@@ -1357,7 +880,7 @@
                     || !isIpv4(collector.ip()) || !ips.add(collector.ip())
                     || collector.channelCount() < 1 || collector.channelCount() > MAX_CHANNEL_COUNT)
                 throw new IllegalStateException("Invalid BTS simulator collector state in Redis");
-            DevelopmentCollectorSimulation.validate(collector, stateStore);
+            DevelopmentCollectorSimulation.validate(collector);
             maximumCollectorId = Math.max(maximumCollectorId, collector.collectorId());
         }
         java.util.Set<String> tombstoneIds = new java.util.HashSet<>();
@@ -1370,6 +893,7 @@
             throw new IllegalStateException("Invalid BTS simulator numbering or tombstone state in Redis");
     }
 
+    /** 停止模拟器:标记为不再运行并释放 TCP 端点;释放失败时以运行时异常报告。 */
     @Override
     public synchronized void close()
     {
@@ -1386,18 +910,18 @@
 
     private record ObservedChannel(int deviceId, ChannelState channel) { }
 
-    public record StateSnapshot(boolean running, String listenAddress, List<DeviceSnapshot> devices,
+    public record StateSnapshot(boolean running, boolean softwareAccessible, String listenAddress, List<DeviceSnapshot> devices,
             List<CollectorSnapshot> collectors, List<RecentRequest> recentRequests) { }
 
     public record DeviceSnapshot(int deviceId, String ip, boolean online, BtsDeviceProfile profile,
             List<ChannelSnapshot> channels) { }
 
-    public record ChannelSnapshot(int channelId, boolean online, DeviceWorkStatus workStatus) { }
+    public record ChannelSnapshot(int channelId, boolean online, DeviceWorkStatus workStatus,
+            ChannelConfiguration configuration) { }
 
     public record CollectorSnapshot(int collectorId, String deviceCode, String ip, String mac, String name,
             String manufacturer, String model, int channelCount, boolean online, int sampleIntervalMs,
-            List<CollectorChannelState> channels, CollectorHistoryFaults historyFaults, long historyRecordCount,
-            List<CollectorRealtimeMeasurement> latestMeasurements)
+            List<CollectorChannelState> channels, List<CollectorRealtimeMeasurement> latestMeasurements)
     {
         public CollectorSnapshot
         {

--
Gitblit v1.7.1