From 7259e265b9237286996b09da03038ea5a133e1e0 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期六, 05 九月 2026 17:45:07 +0800
Subject: [PATCH] 收敛模拟器为设备事实与实时观测
---
battery-device-simulator/src/main/java/com/battery/device/simulator/BtsDevelopmentSimulator.java | 821 +++++++++------------------------------------------------
1 files changed, 138 insertions(+), 683 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..f20ee73 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
@@ -25,22 +25,15 @@
import com.battery.device.simulator.BtsSimulatorStateStore.ChannelState;
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 模拟端点。 */
@@ -62,11 +55,8 @@
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");
@Override
public String adapterCode()
@@ -92,11 +82,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 +98,25 @@
{
if (channels == null || channels.isEmpty()) return java.util.Map.of();
java.util.Map<ChannelIdentity, DeviceObservation> result = new java.util.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));
- }
+ for (ChannelIdentity requested : channels) result.put(requested, observation(requested));
+ recordRequest("observe", channels.toString(), result.toString());
return java.util.Map.copyOf(result);
+ }
+
+ 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,11 +130,7 @@
{
this.stateStore = stateStore;
state = stateStore.loadOrInitialize(defaultState());
- // 仅清理嵌在主状态中的旧演示历史;独立历史由模拟器存储保留。
- state = stateStore.resetHistoryForDemoStartup(state);
- restoreBtsHistory();
validateRestoredState();
- stateStore.completeValidatedRestore(state);
simulator = ScriptedBtsTcpSimulator.responding(port, this::respond);
}
@@ -219,15 +201,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));
@@ -281,16 +257,7 @@
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;
- }
-
- /** 配置采集器独立采样频率和历史下载故障场景。 */
- public synchronized void configureCollectorSampling(int collectorId, int sampleIntervalMs,
- CollectorHistoryFaults historyFaults)
- {
- CollectorState current = requireCollector(collectorId);
- replaceCollector(DevelopmentCollectorSimulation.configureSampling(current, sampleIntervalMs, historyFaults));
+ persist(next);
}
/** 配置一个稳定 S 通道的固定或动态演示指标。 */
@@ -301,373 +268,135 @@
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());
+ return new TestPlan(1, 1, 1, List.of(new PlanRow("STEP", 1, 1, "恒流充电", 60d, 3.7d,
+ requireDevice(channel.devid()).channels().get(channel.chlid() - 1).workStatus() == DeviceWorkStatus.FINISHED
+ ? "已完成" : "待执行", null, null)));
}
+ /** 请求当前检测柜测量;离线或无法观测时不返回缓存值。 */
@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;
}
+ private CabinetRealtime cabinetMeasurement(ChannelIdentity identity)
+ {
+ 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();
+ double wave = Math.sin((sequence + channel.channelId()) * 0.7d);
+ BtsDeviceProfile profile = profileFor(device);
+ double current = channel.workStatus() == DeviceWorkStatus.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());
+ CabinetRealtime result = new CabinetRealtime(sequence, now, current, voltage, 1.2d, 4.44d,
+ 1, 1, "恒流充电", "cc", now, DEVICE_TIME.format(now.atZone(DEVICE_ZONE)), DEVICE_ZONE.getId());
+ return result;
+ }
+
+ /** 请求采集器当前配置值;软件不可访问或设备离线时返回空测量。 */
@Override
public synchronized CollectorRealtime latestCollector(String collectorCode)
{
- var collector = snapshot().collectors().stream().filter(item -> item.deviceCode().equals(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));
+ CollectorState collector = requireCollector(collectorId);
+ 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;
}
- /** 分页下载采集器模拟历史,并应用已配置的重复、冲突、乱序或缺失场景。 */
- public synchronized CollectorHistoryPage collectorHistory(int collectorId, long offset, int limit)
+ private String failureReason()
{
- CollectorState collector = requireCollector(collectorId);
- return DevelopmentCollectorSimulation.history(collector, stateStore, offset, limit);
+ 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;
}
/** 返回控制页需要的运行状态快照。 */
@@ -684,10 +413,9 @@
.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 +424,6 @@
{
SimulatorState defaultState = defaultState();
stateStore.reset(defaultState);
- btsHistory.clear();
state = defaultState;
}
@@ -735,24 +462,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 +551,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 +567,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 +581,13 @@
.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(0).append("\" relativetime=\"").append(0)
.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 +605,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;
@@ -1222,6 +680,7 @@
case WORKING -> "working";
case PAUSED -> "pause";
case FINISHED -> "finish";
+ case PROTECTED -> "protect";
default -> throw new IllegalArgumentException("Unsupported simulator work status: " + status);
};
}
@@ -1298,15 +757,11 @@
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 +778,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 +813,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<>();
@@ -1386,7 +842,7 @@
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,
@@ -1396,8 +852,7 @@
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