| | |
| | | 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.List; |
| | | import javax.xml.stream.XMLInputFactory; |
| | |
| | | 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.BtsHistoryRecord; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.RecentRequest; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.SimulatorState; |
| | | import com.ruoyi.device.simulator.InMemoryBtsSimulatorStateStore; |
| | |
| | | private SimulatorState state; |
| | | private boolean running = true; |
| | | private long observationSequence; |
| | | private final java.util.Map<String, java.util.List<SimBtsHistory>> btsHistory = new java.util.LinkedHashMap<>(); |
| | | private static final DateTimeFormatter BTS_HISTORY_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH.mm.ss") |
| | | .withZone(ZoneId.of("Asia/Hong_Kong")); |
| | | |
| | | /** 启动仅监听 IPv4 本机回环地址的开发模拟器;传入 0 时由系统分配测试端口。 */ |
| | | public BtsDevelopmentSimulator(int port) throws IOException |
| | |
| | | { |
| | | this.stateStore = stateStore; |
| | | state = stateStore.loadOrInitialize(defaultState()); |
| | | // 仅清理 Ticket13 之前嵌在主状态中的旧演示历史;独立历史由存储实现保留并继续补采。 |
| | | state = stateStore.resetHistoryForDemoStartup(state); |
| | | restoreBtsHistory(); |
| | | validateRestoredState(); |
| | | stateStore.completeValidatedRestore(state); |
| | | simulator = ScriptedBtsTcpSimulator.responding(port, this::respond); |
| | |
| | | { |
| | | case "getdevinfo" -> deviceInfoResponse(); |
| | | case "inquire" -> observationResponse(request); |
| | | case "inquiredf" -> historyAvailabilityResponse(request); |
| | | case "download" -> historyResponse(request); |
| | | default -> "<bts version=\"1.0\"><cmd>error</cmd></bts>"; |
| | | }; |
| | | } |
| | |
| | | profile.voltageMaxV().doubleValue()); |
| | | double capacity = working ? Math.max(0d, tick * 0.001d * current) : 0d; |
| | | double energy = capacity * voltage; |
| | | if (working) |
| | | { |
| | | String key = historyKey(profile.devtype(), observed.deviceId(), profile.subDeviceId(), channel.channelId()); |
| | | java.util.List<SimBtsHistory> history = btsHistory.computeIfAbsent(key, ignored -> new ArrayList<>()); |
| | | history.add(new SimBtsHistory(history.size() + 1L, Instant.now(), current, voltage, capacity, energy)); |
| | | SimBtsHistory latest = history.get(history.size() - 1); |
| | | stateStore.appendBtsHistory(key, new BtsHistoryRecord(latest.sequence(), latest.at(), latest.current(), |
| | | latest.voltage(), latest.capacity(), latest.energy())); |
| | | if (history.size() > BtsSimulatorStateStore.MAX_HISTORY_RECORDS) history.remove(0); |
| | | } |
| | | response.append("<inquire dev=\"").append(profile.devtype()).append('-').append(observed.deviceId()) |
| | | .append('-').append(profile.subDeviceId()).append('-').append(channel.channelId()) |
| | | .append("-0\" cycle_id=\"1\" step_id=\"1\" step_type=\"cc\" workstatus=\"") |
| | |
| | | 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 = btsHistory.getOrDefault(historyKey(devtype, devid, subdevid, chlid), List.of()).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<SimBtsHistory> history = btsHistory.getOrDefault(historyKey(devtype, devid, subdevid, chlid), List.of()); |
| | | 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 (SimBtsHistory item : history.subList(from, to)) |
| | | response.append("<data seqid=\"").append(item.sequence()).append("\" stepid=\"1\" cycleid=\"1\" steptype=\"cc\" atime=\"") |
| | | .append(BTS_HISTORY_TIME.format(item.at())).append("\" testtime=\"1000\" volt=\"") |
| | | .append(decimal(item.voltage())).append("\" curr=\"").append(decimal(item.current())) |
| | | .append("\" cap=\"").append(decimal(item.capacity())).append("\" eng=\"").append(decimal(item.energy())).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<SimBtsHistory> 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(item -> new SimBtsHistory(item.sequenceId(), |
| | | item.deviceTime(), item.currentA(), item.voltageV(), item.capacityAh(), item.energyWh())).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 record SimBtsHistory(long sequence, Instant at, double current, double voltage, double capacity, double energy) { } |
| | | |
| | | private java.util.Set<String> requestedChannels(String request) |
| | | { |
| | | java.util.Set<String> channels = new java.util.HashSet<>(); |