package com.battery.device.simulator; import java.io.IOException; import java.io.StringReader; import java.math.BigDecimal; import java.time.Instant; 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; import com.battery.device.api.model.DeviceObservation; import com.battery.device.api.model.DetectedChannel; 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.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.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.CollectorRealtimeSnapshot; /** * 提供由开发后端托管的临时只读 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; 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 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() { return AdapterCapabilities.observeOnly(); } /** 直接提供模拟器当前拓扑,供不需要 TCP/XML 的契约测试使用。 */ @Override public synchronized List discoverChannels() { List result = new ArrayList<>(); for (DeviceState device : state.devices()) { BtsDeviceProfile profile = profileFor(device); for (ChannelState channel : device.channels()) { ChannelIdentity identity = new ChannelIdentity(ADAPTER_CODE, profile.devtype(), device.deviceId(), profile.subDeviceId(), channel.channelId()); 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); } /** 直接提供模拟器观测;离线通道不保留上一次测量或工步事实。 */ @Override public synchronized java.util.Map observe( java.util.Set channels) { if (channels == null || channels.isEmpty()) return java.util.Map.of(); java.util.Map 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 inquire(Set channels) { if (channels == null || channels.isEmpty()) return Map.of(); Map result = new LinkedHashMap<>(); for (ChannelIdentity requested : channels) { 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)); } 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 时由系统分配测试端口。 */ 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(); simulator = ScriptedBtsTcpSimulator.responding(port, this::respond); } /** 返回模拟器 TCP 端点实际监听的端口。 */ 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)); } /** 按预设新威型号元数据新增模拟设备;真实协议和拓扑仍待现场验证。 */ 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, 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 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 检测柜拓扑隔离的开发采集器。 */ 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()); persist(next); } /** 保存稳定 S 通道的固定测量值及自定义指标,下一次请求时形成观测。 */ public synchronized void configureCollectorChannel(int collectorId, String channelCode, List metrics) { CollectorState current = requireCollector(collectorId); 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) { List devices = state.devices().stream() .map(device -> new DeviceState(device.deviceId(), device.ip(), poweredOn, device.channels(), device.profile())) .toList(); List collectors = state.collectors().stream() .map(collector -> DevelopmentCollectorSimulation.resize(collector, collector.ip(), collector.channelCount(), poweredOn, Instant.now())).toList(); persist(new SimulatorState(state.version(), devices, collectors, state.nextDeviceId(), state.nextCollectorId(), state.tombstones())); } /** 修改采集器单个通道工作状态;不代表真实采集器控制能力。 */ public synchronized void updateCollectorChannelStatus(int collectorId, String channelCode, DeviceWorkStatus status) { requireControllableStatus(status); replaceCollector(DevelopmentCollectorSimulation.configureStatus(requireCollector(collectorId), channelCode, status)); } /** 返回通道已配置的开发方案及当前工步位置,不自动推进或生成历史。 */ @Override public synchronized TestPlan testPlan(ChannelIdentity channel) { if (!canObserve(channel)) return null; ChannelState source = requireDevice(channel.devid()).channels().get(channel.chlid() - 1); ChannelConfiguration configuration = source.configuration(); List 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) { 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(); 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; 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(), DEVICE_ZONE.getId()); } /** 模拟器不提供历史工步统计。 */ @Override public List stepRecords(ChannelIdentity channel) { return List.of(); } /** 模拟器不提供历史循环统计。 */ @Override public List cycleRecords(ChannelIdentity channel) { return List.of(); } /** 使用后端正常持久化周期,不提供开发倍速。 */ @Override public long persistenceIntervalMillis(ChannelIdentity channel) { return 60000L; } private boolean canObserve(ChannelIdentity channel) { // 完整来源身份限定数据归属;不可访问和离线只改变观测结果。 return channel != null && observation(channel).connectionStatus() == DeviceConnectionStatus.ONLINE && observation(channel).workStatus() != DeviceWorkStatus.UNOBSERVABLE; } /** 请求采集器当前观测,读取状态页面本身不会触发此请求。 */ public synchronized CollectorRealtimeSnapshot collectorRealtime(int 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; } 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 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.configuration())) .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(), state.softwareAccessible() && collector.online() ? collector.latestMeasurements() : List.of())) .toList(); return new StateSnapshot(running, state.softwareAccessible(), 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 synchronized String respond(String request) { String command = readCommand(request); String response = state.softwareAccessible() ? switch (command) { case "getdevinfo" -> deviceInfoResponse(); case "inquire" -> observationResponse(request); default -> "error"; } : "error"; recordRequest(command, request, state.softwareAccessible() ? response : "SOFTWARE_UNAVAILABLE"); return response; } private void recordRequest(String command, String request, String response) { List recentRequests = new ArrayList<>(stateStore.loadRecentRequests()); 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); } 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) { 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() && 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("inquire_resp"); for (ObservedChannel observed : available) { ChannelState channel = observed.channel(); DeviceState device = requireDevice(observed.deviceId()); BtsDeviceProfile profile = profileFor(device); 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("") // 模拟器返回当前设备方案,供统一观测模型展示;不代表真实 BTS 的完整方案字段。 .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"); 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; } 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 String decimal(double value) { return java.math.BigDecimal.valueOf(value).toPlainString(); } private String toBtsWorkStatus(DeviceWorkStatus status) { return switch (status) { case STOPPED -> "stop"; case WORKING -> "working"; case PAUSED -> "pause"; case FINISHED -> "finish"; case PROTECTED -> "protect"; 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) { // 写穿失败时保留当前内存副本,避免模拟器页面显示未持久化状态。 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) { 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 && status != DeviceWorkStatus.PROTECTED && status != DeviceWorkStatus.UNOBSERVABLE)) throw new IllegalArgumentException("Unsupported simulator work status"); } 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); 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"); } /** 停止模拟器:标记为不再运行并释放 TCP 端点;释放失败时以运行时异常报告。 */ @Override public synchronized void close() { running = false; try { simulator.close(); } catch (Exception exception) { throw new IllegalStateException("关闭开发模拟器失败", exception); } } private record ObservedChannel(int deviceId, ChannelState channel) { } public record StateSnapshot(boolean running, boolean softwareAccessible, 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, 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 channels, List latestMeasurements) { public CollectorSnapshot { channels = List.copyOf(channels); latestMeasurements = List.copyOf(latestMeasurements); } } }