package com.ruoyi.device.bts; import java.io.IOException; import java.io.StringReader; import java.math.BigDecimal; import java.time.Instant; import java.util.ArrayList; import java.util.List; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamReader; import com.ruoyi.device.model.DeviceWorkStatus; import com.ruoyi.device.simulator.BtsSimulatorStateStore; import com.ruoyi.device.simulator.BtsSimulatorStateStore.ChannelState; import com.ruoyi.device.simulator.BtsSimulatorStateStore.ChannelTombstone; import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorChannelState; import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorHistoryAppend; import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorHistoryFaults; import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorMeasurementRecord; import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorMetricState; import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorRealtimeMeasurement; import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorState; import com.ruoyi.device.simulator.BtsSimulatorStateStore.DeviceState; import com.ruoyi.device.simulator.BtsSimulatorStateStore.BtsDeviceProfile; import com.ruoyi.device.simulator.BtsSimulatorStateStore.RecentRequest; import com.ruoyi.device.simulator.BtsSimulatorStateStore.SimulatorState; import com.ruoyi.device.simulator.InMemoryBtsSimulatorStateStore; import com.ruoyi.device.simulator.DevelopmentCollectorSimulation; import com.ruoyi.device.simulator.DevelopmentCollectorSimulation.CollectorAdvance; import com.ruoyi.device.simulator.DevelopmentCollectorSimulation.CollectorHistoryPage; import com.ruoyi.device.simulator.DevelopmentCollectorSimulation.CollectorRealtimeSnapshot; /** 提供由开发后端托管的临时只读 BTS 模拟端点。 */ public final class BtsDevelopmentSimulator implements AutoCloseable { public static final String HOST = "127.0.0.1"; public static final int DEFAULT_PORT = 15020; public static final String ADAPTER_CODE = "dev-sim"; public static final String ADAPTER_NAME = "dev-sim BTS 模拟器"; public static final int POLL_INTERVAL_MS = 5000; public static final String NEWARE_CT_4008T_5V6A_S1_F = "CT-4008T-5V6A-S1-F"; public static final String NEWARE_CT_4008TN_5V12A_S1_F = "CT-4008Tn-5V12A-S1-F"; private static final int MAX_CHANNEL_COUNT = 99; private static final int MAX_RECENT_REQUESTS = 20; private static final int TOMBSTONE_DISCOVERY_COUNT = 3; private final ScriptedBtsTcpSimulator simulator; private final BtsSimulatorStateStore stateStore; private SimulatorState state; private boolean running = true; private long observationSequence; /** 启动仅监听 IPv4 本机回环地址的开发模拟器;传入 0 时由系统分配测试端口。 */ public BtsDevelopmentSimulator(int port) throws IOException { this(port, new InMemoryBtsSimulatorStateStore()); } /** 从长期状态恢复后再启动 TCP 服务,避免服务先暴露默认拓扑。 */ public BtsDevelopmentSimulator(int port, BtsSimulatorStateStore stateStore) throws IOException { this.stateStore = stateStore; state = stateStore.loadOrInitialize(defaultState()); validateRestoredState(); stateStore.completeValidatedRestore(state); simulator = ScriptedBtsTcpSimulator.responding(port, this::respond); } public int port() { return simulator.port(); } /** 新增一台具有独立虚拟 IP 和通道数的开发设备。 */ public synchronized int addDevice(String ip, int channelCount) { validateDeviceConfiguration(ip, channelCount, null); int deviceId = state.nextDeviceId(); List devices = new ArrayList<>(state.devices()); devices.add(newDevice(deviceId, ip, BtsDeviceProfile.virtual(channelCount))); persist(new SimulatorState(BtsSimulatorStateStore.CURRENT_VERSION, devices, state.collectors(), deviceId + 1, state.nextCollectorId(), state.tombstones())); return deviceId; } /** 修改开发设备的虚拟地址、通道数和在线状态。 */ public synchronized void updateDevice(int deviceId, String ip, int channelCount, boolean online) { DeviceState current = requireDevice(deviceId); validateDeviceConfiguration(ip, channelCount, deviceId); List channels = new ArrayList<>(channelCount); for (int channelId = 1; channelId <= channelCount; channelId++) { channels.add(channelId <= current.channels().size() ? current.channels().get(channelId - 1) : new ChannelState(channelId, true, DeviceWorkStatus.STOPPED)); } List devices = replaceDevice(new DeviceState(deviceId, ip.trim(), online, channels, resizeProfile(current.profile(), channelCount))); List tombstones = new ArrayList<>(state.tombstones()); for (int channelId = channelCount + 1; channelId <= current.channels().size(); channelId++) upsertTombstone(tombstones, current.deviceId(), current.ip(), channelId); tombstones.removeIf(item -> item.deviceId() == deviceId && item.channelId() <= channelCount); persist(new SimulatorState(state.version(), devices, state.collectors(), state.nextDeviceId(), state.nextCollectorId(), tombstones)); } /** 使用现场确认的新威检测柜 profile 新增设备,型号之外的控制语义仍保持只读。 */ public synchronized int addNewareDevice(String ip, String model) { BtsDeviceProfile profile = newareProfile(model); validateDeviceConfiguration(ip, profile.channelCount(), null); int deviceId = state.nextDeviceId(); List devices = new ArrayList<>(state.devices()); devices.add(newDevice(deviceId, ip, profile)); persist(new SimulatorState(state.version(), devices, state.collectors(), deviceId + 1, state.nextCollectorId(), state.tombstones())); return deviceId; } /** 将已有模拟设备切换为新威 profile,并按 profile 通道数调整拓扑。 */ public synchronized void configureNewareProfile(int deviceId, String model) { DeviceState current = requireDevice(deviceId); BtsDeviceProfile profile = newareProfile(model); updateDeviceWithProfile(current, profile); } /** 删除指定开发设备;后续发现响应不再返回它。 */ public synchronized void deleteDevice(int deviceId) { DeviceState removed = requireDevice(deviceId); List devices = new ArrayList<>(state.devices()); devices.removeIf(item -> item.deviceId() == deviceId); List tombstones = new ArrayList<>(state.tombstones()); for (ChannelState channel : removed.channels()) upsertTombstone(tombstones, removed.deviceId(), removed.ip(), channel.channelId()); persist(new SimulatorState(state.version(), devices, state.collectors(), state.nextDeviceId(), state.nextCollectorId(), tombstones)); } /** 修改指定开发设备的单个通道状态。 */ public synchronized void updateChannel(int deviceId, int channelId, boolean online, DeviceWorkStatus workStatus) { requireControllableStatus(workStatus); DeviceState device = requireDevice(deviceId); if (channelId < 1 || channelId > device.channels().size()) throw new IllegalArgumentException("channelId must be between 1 and " + device.channels().size()); List channels = new ArrayList<>(device.channels()); channels.set(channelId - 1, new ChannelState(channelId, online, workStatus)); 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 检测柜拓扑隔离的开发采集器。 */ public synchronized int addCollector(String ip, int channelCount) { validateCollectorConfiguration(ip, channelCount, null); int collectorId = state.nextCollectorId(); List collectors = new ArrayList<>(state.collectors()); collectors.add(newCollector(collectorId, ip, channelCount)); persist(new SimulatorState(state.version(), state.devices(), collectors, state.nextDeviceId(), collectorId + 1, state.tombstones())); return collectorId; } /** 修改开发采集器的虚拟地址、通道数和在线状态。 */ public synchronized void updateCollector(int collectorId, String ip, int channelCount, boolean online) { validateCollectorConfiguration(ip, channelCount, collectorId); CollectorState current = requireCollector(collectorId); List collectors = new ArrayList<>(state.collectors()); collectors.replaceAll(item -> item.collectorId() == collectorId ? DevelopmentCollectorSimulation.resize(current, ip, channelCount, online, Instant.now()) : item); persist(new SimulatorState(state.version(), state.devices(), collectors, state.nextDeviceId(), state.nextCollectorId(), state.tombstones())); } /** 删除指定开发采集器,不影响检测柜 TCP 发现结果。 */ public synchronized void deleteCollector(int collectorId) { CollectorState collector = requireCollector(collectorId); List collectors = new ArrayList<>(state.collectors()); 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)); } /** 配置一个稳定 S 通道的固定或动态演示指标。 */ public synchronized void configureCollectorChannel(int collectorId, String channelCode, List metrics) { CollectorState current = requireCollector(collectorId); replaceCollector(DevelopmentCollectorSimulation.configureChannel(current, channelCode, metrics)); } /** * 模拟现场人工统一开关机:关机停止采样但保留历史,开机后检测柜通道保持待机。 * 该操作只改变模拟设备事实,不代表系统具备真实设备控制能力。 */ public synchronized void setAllDevicesPoweredOn(boolean poweredOn) { List 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())) .toList(); Instant changedAt = Instant.now(); List collectors = state.collectors().stream() .map(collector -> DevelopmentCollectorSimulation.resize(collector, collector.ip(), collector.channelCount(), poweredOn, changedAt)) .toList(); persist(new SimulatorState(state.version(), devices, collectors, state.nextDeviceId(), state.nextCollectorId(), state.tombstones())); } /** 按每台采集器自己的频率推进实时和历史采样。 */ 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 collectors = new ArrayList<>(state.collectors().size()); List 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 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); } /** 返回控制页需要的运行状态快照。 */ public synchronized StateSnapshot snapshot() { List 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())) .toList())) .toList(); List 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())) .toList(); return new StateSnapshot(running, HOST + ":" + port(), deviceSnapshots, collectorSnapshots, stateStore.loadRecentRequests()); } /** 人工恢复默认拓扑;这是正常运行中唯一清空长期模拟状态的入口。 */ public synchronized void reset() { SimulatorState defaultState = defaultState(); stateStore.reset(defaultState); state = defaultState; } private SimulatorState defaultState() { return new SimulatorState(BtsSimulatorStateStore.CURRENT_VERSION, List.of(newDevice(1, "127.0.0.2", 4)), List.of(newCollector(1, "127.0.1.2", 4)), 2, 2, List.of()); } private DeviceState newDevice(int deviceId, String ip, int channelCount) { return newDevice(deviceId, ip, BtsDeviceProfile.virtual(channelCount)); } private DeviceState newDevice(int deviceId, String ip, BtsDeviceProfile profile) { List channels = new ArrayList<>(profile.channelCount()); for (int channelId = 1; channelId <= profile.channelCount(); channelId++) channels.add(new ChannelState(channelId, true, DeviceWorkStatus.STOPPED)); return new DeviceState(deviceId, ip.trim(), true, channels, profile); } private CollectorState newCollector(int collectorId, String ip, int channelCount) { return new CollectorState(collectorId, "dev-sim-collector-" + collectorId, ip.trim(), simulatorCollectorMac(collectorId), "dev-sim 采集器 " + collectorId, "开发模拟器", "Virtual Collector", channelCount, true); } private void replaceCollector(CollectorState replacement) { List collectors = new ArrayList<>(state.collectors()); collectors.replaceAll(item -> item.collectorId() == replacement.collectorId() ? replacement : item); persist(new SimulatorState(state.version(), state.devices(), collectors, state.nextDeviceId(), state.nextCollectorId(), state.tombstones())); } private String respond(String request) { String command = readCommand(request); recordRequest(command, request); return switch (command) { case "getdevinfo" -> deviceInfoResponse(); case "inquire" -> observationResponse(request); default -> "error"; }; } private synchronized void recordRequest(String command, String request) { List recentRequests = new ArrayList<>(stateStore.loadRecentRequests()); recentRequests.add(0, new RecentRequest(Instant.now(), command, request)); if (recentRequests.size() > MAX_RECENT_REQUESTS) recentRequests = new ArrayList<>(recentRequests.subList(0, MAX_RECENT_REQUESTS)); stateStore.saveRecentRequests(recentRequests); } private String readCommand(String request) { try { XMLInputFactory factory = XMLInputFactory.newFactory(); factory.setProperty(XMLInputFactory.SUPPORT_DTD, false); factory.setProperty("javax.xml.stream.isSupportingExternalEntities", false); XMLStreamReader reader = factory.createXMLStreamReader(new StringReader(request)); while (reader.hasNext()) { if (reader.next() == XMLStreamConstants.START_ELEMENT && "cmd".equals(reader.getLocalName())) return reader.getElementText().trim(); } } catch (Exception exception) { throw new IllegalArgumentException("Invalid BTS request XML", exception); } throw new IllegalArgumentException("BTS request does not contain cmd"); } private synchronized String deviceInfoResponse() { int channelCount = state.devices().stream().mapToInt(device -> device.channels().size()).sum() + state.tombstones().size(); StringBuilder response = new StringBuilder("getdevinfo_resp") .append(""); for (DeviceState device : state.devices()) { for (ChannelState channel : device.channels()) { boolean available = device.online() && channel.online(); appendDiscoveredChannel(response, device.deviceId(), device.ip(), channel.channelId(), available); } } List remaining = new ArrayList<>(); for (ChannelTombstone tombstone : state.tombstones()) { appendDiscoveredChannel(response, tombstone.deviceId(), tombstone.ip(), tombstone.channelId(), false); if (tombstone.remainingDiscoveries() > 1) remaining.add(new ChannelTombstone(tombstone.deviceId(), tombstone.ip(), tombstone.channelId(), tombstone.remainingDiscoveries() - 1)); } if (!remaining.equals(state.tombstones())) persist(new SimulatorState(state.version(), state.devices(), state.collectors(), state.nextDeviceId(), state.nextCollectorId(), remaining)); return response.append("").toString(); } private void appendDiscoveredChannel(StringBuilder response, int deviceId, String ip, int channelId, boolean available) { // 展示元数据是开发模拟器扩展,不代表真实 BTS 协议必然提供这些属性。 DeviceState device = state.devices().stream().filter(item -> item.deviceId() == deviceId).findFirst().orElse(null); BtsDeviceProfile profile = device == null ? BtsDeviceProfile.virtual(channelId) : profileFor(device); response.append("") .append(available).append(""); } private synchronized String observationResponse(String request) { long tick = ++observationSequence; java.util.Set requested = requestedChannels(request); List 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()))) available.add(new ObservedChannel(device.deviceId(), channel)); StringBuilder response = new StringBuilder("inquire_resp"); for (ObservedChannel observed : available) { 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()); double capacity = working ? Math.max(0d, tick * 0.001d * current) : 0d; double energy = capacity * voltage; response.append(""); } return response.append("").toString(); } private java.util.Set requestedChannels(String request) { java.util.Set channels = new java.util.HashSet<>(); try { XMLInputFactory factory = XMLInputFactory.newFactory(); factory.setProperty(XMLInputFactory.SUPPORT_DTD, false); factory.setProperty("javax.xml.stream.isSupportingExternalEntities", false); XMLStreamReader reader = factory.createXMLStreamReader(new StringReader(request)); while (reader.hasNext()) { if (reader.next() == XMLStreamConstants.START_ELEMENT && "inquire".equals(reader.getLocalName())) { String devid = reader.getAttributeValue(null, "devid"); String chlid = reader.getAttributeValue(null, "chlid"); if (devid != null && chlid != null) channels.add(devid + ":" + chlid); } } return channels; } catch (Exception exception) { throw new IllegalArgumentException("Invalid BTS inquire XML", exception); } } private BtsDeviceProfile profileFor(DeviceState device) { return device.profile() == null ? BtsDeviceProfile.virtual(device.channels().size()) : device.profile(); } private BtsDeviceProfile resizeProfile(BtsDeviceProfile profile, int channelCount) { return new BtsDeviceProfile(profile.devtype(), profile.subDeviceId(), profile.name(), profile.manufacturer(), profile.model(), profile.serialNumber(), profile.firmwareVersion(), channelCount, profile.currentMinA(), profile.currentMaxA(), profile.voltageMinV(), profile.voltageMaxV(), profile.sampleIntervalMs()); } private void updateDeviceWithProfile(DeviceState current, BtsDeviceProfile profile) { int channelCount = profile.channelCount(); List channels = new ArrayList<>(channelCount); for (int channelId = 1; channelId <= channelCount; channelId++) channels.add(channelId <= current.channels().size() ? current.channels().get(channelId - 1) : new ChannelState(channelId, true, DeviceWorkStatus.STOPPED)); List devices = replaceDevice(new DeviceState(current.deviceId(), current.ip(), current.online(), channels, profile)); List tombstones = new ArrayList<>(state.tombstones()); for (int channelId = channelCount + 1; channelId <= current.channels().size(); channelId++) upsertTombstone(tombstones, current.deviceId(), current.ip(), channelId); tombstones.removeIf(item -> item.deviceId() == current.deviceId() && item.channelId() <= channelCount); persist(new SimulatorState(state.version(), devices, state.collectors(), state.nextDeviceId(), state.nextCollectorId(), tombstones)); } private BtsDeviceProfile newareProfile(String model) { if (NEWARE_CT_4008T_5V6A_S1_F.equals(model)) return new BtsDeviceProfile(22, 1, "新威 CT-4008T", "新威", model, "SIM-CT4008T-5V6A", "BTS7.5.4", 8, new BigDecimal("-6.000"), new BigDecimal("6.000"), BigDecimal.ZERO, new BigDecimal("5.000"), 100); if (NEWARE_CT_4008TN_5V12A_S1_F.equals(model)) return new BtsDeviceProfile(22, 1, "新威 CT-4008Tn", "新威", model, "SIM-CT4008TN-5V12A", "BTS8.0", 8, new BigDecimal("-12.000"), new BigDecimal("12.000"), BigDecimal.ZERO, new BigDecimal("5.000"), 100); 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); } private String toBtsWorkStatus(DeviceWorkStatus status) { return switch (status) { case STOPPED -> "stop"; case WORKING -> "working"; case PAUSED -> "pause"; case FINISHED -> "finish"; default -> throw new IllegalArgumentException("Unsupported simulator work status: " + status); }; } private void validateDeviceConfiguration(String ip, int channelCount, Integer currentDeviceId) { if (!isIpv4(ip)) throw new IllegalArgumentException("Simulator device IP must be a valid IPv4 address"); if (channelCount < 1 || channelCount > MAX_CHANNEL_COUNT) throw new IllegalArgumentException("channelCount must be between 1 and " + MAX_CHANNEL_COUNT); boolean duplicate = state.devices().stream() .anyMatch(device -> device.ip().equals(ip.trim()) && device.deviceId() != (currentDeviceId == null ? -1 : currentDeviceId)); duplicate = duplicate || state.collectors().stream().anyMatch(collector -> collector.ip().equals(ip.trim())); if (duplicate) throw new IllegalArgumentException("Simulator device IP must be unique"); } private void validateCollectorConfiguration(String ip, int channelCount, Integer currentCollectorId) { if (!isIpv4(ip)) throw new IllegalArgumentException("Simulator collector IP must be a valid IPv4 address"); if (channelCount < 1 || channelCount > MAX_CHANNEL_COUNT) throw new IllegalArgumentException("channelCount must be between 1 and " + MAX_CHANNEL_COUNT); boolean duplicate = state.collectors().stream() .anyMatch(collector -> collector.ip().equals(ip.trim()) && collector.collectorId() != (currentCollectorId == null ? -1 : currentCollectorId)); duplicate = duplicate || state.devices().stream().anyMatch(device -> device.ip().equals(ip.trim())); if (duplicate) throw new IllegalArgumentException("Simulator collector IP must be unique"); } private boolean isIpv4(String ip) { if (ip == null) return false; String[] parts = ip.trim().split("\\.", -1); if (parts.length != 4) return false; for (String part : parts) { try { int value = Integer.parseInt(part); if (part.isEmpty() || value < 0 || value > 255) return false; } catch (NumberFormatException exception) { return false; } } return true; } private DeviceState requireDevice(int deviceId) { return state.devices().stream().filter(item -> item.deviceId() == deviceId).findFirst() .orElseThrow(() -> new IllegalArgumentException("Unknown simulator device: " + deviceId)); } private CollectorState requireCollector(int collectorId) { return state.collectors().stream().filter(item -> item.collectorId() == collectorId).findFirst() .orElseThrow(() -> new IllegalArgumentException("Unknown simulator collector: " + collectorId)); } private List replaceDevice(DeviceState replacement) { List devices = new ArrayList<>(state.devices()); devices.replaceAll(item -> item.deviceId() == replacement.deviceId() ? replacement : item); return devices; } private void upsertTombstone(List tombstones, int deviceId, String ip, int channelId) { tombstones.removeIf(item -> item.deviceId() == deviceId && item.channelId() == channelId); tombstones.add(new ChannelTombstone(deviceId, ip, channelId, TOMBSTONE_DISCOVERY_COUNT)); } private void persist(SimulatorState nextState) { // 写穿失败时保留当前内存副本,避免控制页显示未持久化状态。 stateStore.save(nextState); state = nextState; } private void persist(SimulatorState nextState, List historyAppends) { // 主状态与原始历史批次必须一起成功,内存序号才能向前推进。 stateStore.save(nextState, historyAppends); state = nextState; } private String simulatorMac(int devtype, int devid) { return String.format("02:44:%02X:%02X:%02X:%02X", (devtype >>> 8) & 0xFF, devtype & 0xFF, (devid >>> 8) & 0xFF, devid & 0xFF); } private String simulatorCollectorMac(int collectorId) { return String.format("02:43:00:00:%02X:%02X", (collectorId >>> 8) & 0xFF, collectorId & 0xFF); } 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"); } private void validateRestoredState() { java.util.Set deviceIds = new java.util.HashSet<>(); java.util.Set collectorIds = new java.util.HashSet<>(); java.util.Set ips = new java.util.HashSet<>(); int maximumDeviceId = 0; for (DeviceState device : state.devices()) { if (device.deviceId() < 1 || !deviceIds.add(device.deviceId()) || !isIpv4(device.ip()) || !ips.add(device.ip()) || device.channels().isEmpty() || device.channels().size() > MAX_CHANNEL_COUNT) throw new IllegalStateException("Invalid BTS simulator device state in Redis"); if (profileFor(device).channelCount() != device.channels().size()) throw new IllegalStateException("BTS simulator device profile channel count does not match topology"); maximumDeviceId = Math.max(maximumDeviceId, device.deviceId()); for (int index = 0; index < device.channels().size(); index++) { ChannelState channel = device.channels().get(index); if (channel.channelId() != index + 1) throw new IllegalStateException("Invalid BTS simulator channel state in Redis"); requireControllableStatus(channel.workStatus()); } } int maximumCollectorId = 0; for (CollectorState collector : state.collectors()) { if (collector.collectorId() < 1 || !collectorIds.add(collector.collectorId()) || !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); maximumCollectorId = Math.max(maximumCollectorId, collector.collectorId()); } java.util.Set tombstoneIds = new java.util.HashSet<>(); boolean invalidTombstone = state.tombstones().stream().anyMatch(item -> item.deviceId() < 1 || !isIpv4(item.ip()) || item.channelId() < 1 || item.channelId() > MAX_CHANNEL_COUNT || item.remainingDiscoveries() < 1 || item.remainingDiscoveries() > TOMBSTONE_DISCOVERY_COUNT || !tombstoneIds.add(item.deviceId() + ":" + item.channelId())); if (state.nextDeviceId() <= maximumDeviceId || state.nextCollectorId() <= maximumCollectorId || invalidTombstone) throw new IllegalStateException("Invalid BTS simulator numbering or tombstone state in Redis"); } @Override public synchronized void close() throws Exception { running = false; simulator.close(); } private record ObservedChannel(int deviceId, ChannelState channel) { } public record StateSnapshot(boolean running, String listenAddress, List devices, List collectors, List recentRequests) { } public record DeviceSnapshot(int deviceId, String ip, boolean online, BtsDeviceProfile profile, List channels) { } public record ChannelSnapshot(int channelId, boolean online, DeviceWorkStatus workStatus) { } public record CollectorSnapshot(int collectorId, String deviceCode, String ip, String mac, String name, String manufacturer, String model, int channelCount, boolean online, int sampleIntervalMs, List channels, CollectorHistoryFaults historyFaults, long historyRecordCount, List latestMeasurements) { public CollectorSnapshot { channels = List.copyOf(channels); latestMeasurements = List.copyOf(latestMeasurements); } } }