| | |
| | | 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.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.DevelopmentCollectorSimulation; |
| | | import com.battery.device.simulator.DevelopmentCollectorSimulation.CollectorRealtimeSnapshot; |
| | | |
| | | /** 提供由开发后端托管的临时只读 BTS 模拟端点。 */ |
| | | public final class BtsDevelopmentSimulator implements TestDataObservationAdapter |
| | | /** |
| | | * 提供由开发后端托管的临时只读 BTS 模拟端点。 |
| | | * 同时实现发现与测试数据观测契约:模拟器直接暴露自身状态,因此发现与取数按同一实例解析, |
| | | * 不再需要单独注入一个扩展观测 Bean。TCP 端点仍保留给协议层测试使用。 |
| | | */ |
| | | public final class BtsDevelopmentSimulator implements DeviceAdapter, TestDataObservationAdapter |
| | | { |
| | | public static final String HOST = "127.0.0.1"; |
| | | public static final int DEFAULT_PORT = 15020; |
| | |
| | | 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 java.util.Map.copyOf(result); |
| | | } |
| | | |
| | | /** 直接提供模拟器实时快照;不可观测通道不出现在结果里,与 BTS 协议解析后的语义一致。 */ |
| | | @Override |
| | | public synchronized Map<ChannelIdentity, RealtimeSnapshot> inquire(Set<ChannelIdentity> channels) |
| | | { |
| | | if (channels == null || channels.isEmpty()) return Map.of(); |
| | | Map<ChannelIdentity, RealtimeSnapshot> result = new LinkedHashMap<>(); |
| | | for (ChannelIdentity requested : channels) |
| | | { |
| | | 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()) |
| | |
| | | simulator = ScriptedBtsTcpSimulator.responding(port, this::respond); |
| | | } |
| | | |
| | | /** 返回模拟器 TCP 端点实际监听的端口。 */ |
| | | public int port() |
| | | { |
| | | return simulator.port(); |
| | |
| | | state.nextCollectorId(), tombstones)); |
| | | } |
| | | |
| | | /** 使用现场确认的新威检测柜 profile 新增设备,型号之外的控制语义仍保持只读。 */ |
| | | /** 按预设新威型号元数据新增模拟设备;真实协议和拓扑仍待现场验证。 */ |
| | | public synchronized int addNewareDevice(String ip, String model) |
| | | { |
| | | BtsDeviceProfile profile = newareProfile(model); |
| | |
| | | persist(next); |
| | | } |
| | | |
| | | /** 配置一个稳定 S 通道的固定或动态演示指标。 */ |
| | | /** 保存稳定 S 通道的固定测量值及自定义指标,下一次请求时形成观测。 */ |
| | | public synchronized void configureCollectorChannel(int collectorId, String channelCode, |
| | | List<CollectorMetricState> metrics) |
| | | { |
| | |
| | | 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()); |
| | | now, DEVICE_TIME.format(now.atZone(DEVICE_ZONE)), DEVICE_ZONE.getId(), configuration.relativeTimeSeconds()); |
| | | return result; |
| | | } |
| | | |
| | | /** 请求采集器当前配置值;软件不可访问或设备离线时返回空测量。 */ |
| | | @Override |
| | | public synchronized CollectorRealtime latestCollector(String collectorCode) |
| | | public synchronized CollectorRealtime latestCollector(String adapterCode, String collectorCode) |
| | | { |
| | | CollectorState collector = state.collectors().stream().filter(item -> item.deviceCode().equals(collectorCode)) |
| | | .findFirst().orElse(null); |
| | |
| | | return sequence; |
| | | } |
| | | |
| | | /** 返回控制页需要的运行状态快照。 */ |
| | | /** 返回模拟器配置、当前状态和最近请求,不触发新的设备观测。 */ |
| | | public synchronized StateSnapshot snapshot() |
| | | { |
| | | List<DeviceSnapshot> deviceSnapshots = state.devices().stream() |
| | |
| | | |
| | | 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()); |
| | |
| | | throw new IllegalStateException("Invalid BTS simulator numbering or tombstone state in Redis"); |
| | | } |
| | | |
| | | /** 停止模拟器:标记为不再运行并释放 TCP 端点;释放失败时以运行时异常报告。 */ |
| | | @Override |
| | | public synchronized void close() |
| | | { |