yeg
2026-08-30 9c0fd80309b277420c492180ae9f98f779f0f5c8
ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java
@@ -4,6 +4,8 @@
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;
@@ -22,6 +24,7 @@
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;
@@ -50,6 +53,9 @@
    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
@@ -62,7 +68,9 @@
    {
        this.stateStore = stateStore;
        state = stateStore.loadOrInitialize(defaultState());
        // 仅清理 Ticket13 之前嵌在主状态中的旧演示历史;独立历史由存储实现保留并继续补采。
        state = stateStore.resetHistoryForDemoStartup(state);
        restoreBtsHistory();
        validateRestoredState();
        stateStore.completeValidatedRestore(state);
        simulator = ScriptedBtsTcpSimulator.responding(port, this::respond);
@@ -341,6 +349,8 @@
        {
            case "getdevinfo" -> deviceInfoResponse();
            case "inquire" -> observationResponse(request);
            case "inquiredf" -> historyAvailabilityResponse(request);
            case "download" -> historyResponse(request);
            default -> "<bts version=\"1.0\"><cmd>error</cmd></bts>";
        };
    }
@@ -445,6 +455,16 @@
                    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=\"")
@@ -458,6 +478,88 @@
        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<>();