yeg
2026-08-31 6c38637d89f346a660618558451f2dd7286dc7a5
ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java
@@ -25,6 +25,7 @@
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.BtsPlaybackState;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.RecentRequest;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.SimulatorState;
import com.ruoyi.device.simulator.InMemoryBtsSimulatorStateStore;
@@ -53,7 +54,8 @@
    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 final java.util.Map<String, java.util.List<BtsHistoryRecord>> btsHistory = new java.util.LinkedHashMap<>();
    private static final long SIMULATED_RECORD_INTERVAL_SECONDS = 60L;
    private static final DateTimeFormatter BTS_HISTORY_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH.mm.ss")
            .withZone(ZoneId.of("Asia/Hong_Kong"));
@@ -143,8 +145,16 @@
        List<DeviceState> devices = new ArrayList<>(state.devices());
        devices.removeIf(item -> item.deviceId() == deviceId);
        List<ChannelTombstone> tombstones = new ArrayList<>(state.tombstones());
        BtsDeviceProfile removedProfile = profileFor(removed);
        for (ChannelState channel : removed.channels())
        {
            upsertTombstone(tombstones, removed.deviceId(), removed.ip(), channel.channelId());
            String key = historyKey(removedProfile.devtype(), removed.deviceId(), removedProfile.subDeviceId(),
                    channel.channelId());
            stateStore.deleteBtsHistory(key);
            stateStore.deleteBtsPlaybackState(key);
            btsHistory.remove(key);
        }
        persist(new SimulatorState(state.version(), devices, state.collectors(), state.nextDeviceId(),
                state.nextCollectorId(), tombstones));
    }
@@ -264,6 +274,156 @@
                    state.nextCollectorId(), state.tombstones()), historyAppends);
    }
    /** 按墙上时间推进检测柜模拟播放;设备状态和测试接收状态互不影响。 */
    public synchronized void advanceBts(Instant now)
    {
        for (DeviceState device : state.devices())
        {
            if (!device.online()) continue;
            BtsDeviceProfile profile = profileFor(device);
            for (ChannelState channel : device.channels())
            {
                if (!channel.online() || channel.workStatus() != DeviceWorkStatus.WORKING) continue;
                String key = historyKey(profile.devtype(), device.deviceId(), profile.subDeviceId(), channel.channelId());
                BtsPlaybackState playback = playbackState(key, now);
                advanceBtsPlayback(key, profile, playback,
                        profile.currentMaxA().doubleValue() * 0.2d, 3.7d, now);
            }
        }
    }
    /** 设置检测柜通道演示倍速,只接受 1x、60x 和 600x。 */
    public synchronized void setBtsSpeed(int deviceId, int channelId, int speedMultiplier)
    {
        DeviceState device = requireDevice(deviceId);
        requireChannel(device, channelId);
        String key = btsKey(device, channelId);
        BtsPlaybackState current = playbackState(key, Instant.now());
        stateStore.saveBtsPlaybackState(key, new BtsPlaybackState(current.playing(), speedMultiplier,
                current.cycleNo(), current.stepNo(), current.stepName(), current.stepType(), current.simulatedAt(),
                Instant.now(), current.nextSequence(), current.naturallyFinished()));
    }
    /** 开始播放指定检测柜通道的演示时间线。 */
    public synchronized void playBts(int deviceId, int channelId) { setBtsPlaying(deviceId, channelId, true); }
    /** 暂停模拟器时间线,不改变检测柜通道工作事实。 */
    public synchronized void pauseBts(int deviceId, int channelId) { setBtsPlaying(deviceId, channelId, false); }
    /** 从暂停状态恢复模拟器时间线。 */
    public synchronized void resumeBts(int deviceId, int channelId) { setBtsPlaying(deviceId, channelId, true); }
    private void setBtsPlaying(int deviceId, int channelId, boolean playing)
    {
        DeviceState device = requireDevice(deviceId);
        requireChannel(device, channelId);
        String key = btsKey(device, channelId);
        Instant now = Instant.now();
        BtsPlaybackState current = playbackState(key, now);
        if (current.naturallyFinished() && playing) return;
        stateStore.saveBtsPlaybackState(key, new BtsPlaybackState(playing, current.speedMultiplier(), current.cycleNo(),
                current.stepNo(), current.stepName(), current.stepType(), current.simulatedAt(), now,
                current.nextSequence(), current.naturallyFinished()));
    }
    /** 跳转到下一实际工步;不向设备发送控制报文。 */
    public synchronized void nextBtsStep(int deviceId, int channelId)
    {
        DeviceState device = requireDevice(deviceId);
        requireChannel(device, channelId);
        String key = btsKey(device, channelId);
        BtsPlaybackState current = playbackState(key, Instant.now());
        BtsExcelFixture.FixtureRecord next = nextFixtureStep(current.cycleNo(), current.stepNo());
        if (next == null)
        {
            BtsExcelFixture.FixtureRecord last = BtsExcelFixture.record(BtsExcelFixture.recordCount());
            savePlayback(key, new BtsPlaybackState(false, current.speedMultiplier(), last.cycleNo(), last.stepNo(),
                    last.stepName(), last.stepType(), current.simulatedAt(), Instant.now(),
                    BtsExcelFixture.recordCount() + 1L, true));
            return;
        }
        savePlayback(key, new BtsPlaybackState(current.playing(), current.speedMultiplier(), next.cycleNo(), next.stepNo(),
                next.stepName(), next.stepType(), current.simulatedAt(), Instant.now(), next.sequenceId(), false));
    }
    /** 跳转到下一循环的首工步;不补齐当前循环缺失的首尾工步。 */
    public synchronized void nextBtsCycle(int deviceId, int channelId)
    {
        DeviceState device = requireDevice(deviceId);
        requireChannel(device, channelId);
        String key = btsKey(device, channelId);
        BtsPlaybackState current = playbackState(key, Instant.now());
        BtsExcelFixture.FixtureRecord next = BtsExcelFixture.records().stream()
                .filter(item -> item.cycleNo() > current.cycleNo()).findFirst().orElse(null);
        if (next == null)
        {
            BtsExcelFixture.FixtureRecord last = BtsExcelFixture.record(BtsExcelFixture.recordCount());
            savePlayback(key, new BtsPlaybackState(false, current.speedMultiplier(), last.cycleNo(), last.stepNo(),
                    last.stepName(), last.stepType(), current.simulatedAt(), Instant.now(),
                    BtsExcelFixture.recordCount() + 1L, true));
            return;
        }
        savePlayback(key, new BtsPlaybackState(current.playing(), current.speedMultiplier(), next.cycleNo(), next.stepNo(),
                next.stepName(), next.stepType(), current.simulatedAt(), Instant.now(), next.sequenceId(), false));
    }
    /** 清空单通道播放和原始数据,恢复到首循环首工步。 */
    public synchronized void resetBts(int deviceId, int channelId)
    {
        DeviceState device = requireDevice(deviceId);
        requireChannel(device, channelId);
        String key = btsKey(device, channelId);
        btsHistory.remove(key);
        stateStore.deleteBtsHistory(key);
        stateStore.deleteBtsPlaybackState(key);
        savePlayback(key, defaultPlayback(Instant.now()));
    }
    /** 返回播放状态及当前通道的 record、step、cycle 数据。 */
    public synchronized PlaybackSnapshot btsPlayback(int deviceId, int channelId)
    {
        DeviceState device = requireDevice(deviceId);
        requireChannel(device, channelId);
        String key = btsKey(device, channelId);
        BtsPlaybackState state = playbackState(key, Instant.now());
        return new PlaybackSnapshot(deviceId, channelId, state, btsRecords(deviceId, channelId),
                btsSteps(deviceId, channelId), btsCycles(deviceId, channelId));
    }
    /** 返回只追加的检测柜原始 record。 */
    public synchronized List<BtsHistoryRecord> btsRecords(int deviceId, int channelId)
    {
        DeviceState device = requireDevice(deviceId);
        requireChannel(device, channelId);
        return List.copyOf(btsHistoryFor(btsKey(device, channelId)));
    }
    /** 按实际出现的循环和工步汇总 record,不补齐缺失行。 */
    public synchronized List<BtsStepRecord> btsSteps(int deviceId, int channelId)
    {
        java.util.Map<String, BtsStepAccumulator> grouped = new java.util.LinkedHashMap<>();
        for (BtsHistoryRecord item : btsRecords(deviceId, channelId))
        {
            String key = item.cycleNo() + ":" + item.stepNo();
            grouped.computeIfAbsent(key, ignored -> new BtsStepAccumulator(item)).add(item);
        }
        return grouped.values().stream().map(BtsStepAccumulator::finish).toList();
    }
    /** 按实际出现的循环汇总 record,不增加测试结束统计行。 */
    public synchronized List<BtsCycleRecord> btsCycles(int deviceId, int channelId)
    {
        java.util.Map<Integer, BtsCycleAccumulator> grouped = new java.util.LinkedHashMap<>();
        for (BtsHistoryRecord item : btsRecords(deviceId, channelId))
            grouped.computeIfAbsent(item.cycleNo(), ignored -> new BtsCycleAccumulator(item)).add(item);
        return grouped.entrySet().stream().map(entry -> {
            BtsCycleRecord calculated = entry.getValue().finish();
            BtsExcelFixture.CycleSummary expected = BtsExcelFixture.cycle(entry.getKey());
            if (expected == null || entry.getValue().count < expected.recordCount()) return calculated;
            return new BtsCycleRecord(calculated.cycleNo(), expected.recordCount(), calculated.startedAt(),
                    calculated.endedAt(), expected.chargeCapacityAh(), expected.dischargeCapacityAh(),
                    expected.chargeEnergyWh(), expected.dischargeEnergyWh());
        }).toList();
    }
    /** 返回采集器最新实时快照;快照与分页历史保持不同读取路径。 */
    public synchronized CollectorRealtimeSnapshot collectorRealtime(int collectorId)
    {
@@ -303,6 +463,7 @@
    {
        SimulatorState defaultState = defaultState();
        stateStore.reset(defaultState);
        btsHistory.clear();
        state = defaultState;
    }
@@ -453,21 +614,27 @@
                    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;
            String key = historyKey(profile.devtype(), observed.deviceId(), profile.subDeviceId(), channel.channelId());
            BtsPlaybackState playback = playbackState(key, Instant.now());
            double capacity = working ? latestCapacity(key) : 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);
                playback = advanceBtsPlayback(key, profile, playback, current, voltage);
                BtsHistoryRecord latest = latestRecord(key);
                if (latest != null)
                {
                    capacity = latest.capacityAh();
                    energy = latest.energyWh();
            }
            }
            int cycleNo = playback.cycleNo();
            int stepNo = playback.stepNo();
            String stepType = playback.stepType();
            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=\"")
                    .append("-0\" cycle_id=\"").append(cycleNo).append("\" step_id=\"").append(stepNo)
                    .append("\" step_type=\"").append(stepType).append("\" workstatus=\"")
                    .append(toBtsWorkStatus(channel.workStatus()))
                    .append("\" current=\"").append(decimal(current)).append("\" voltage=\"")
                    .append(decimal(voltage)).append("\" capacity=\"").append(decimal(capacity))
@@ -489,7 +656,7 @@
            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();
            int count = btsHistoryFor(historyKey(devtype, devid, subdevid, chlid)).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=\"")
@@ -507,18 +674,19 @@
        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());
        List<BtsHistoryRecord> history = btsHistoryFor(historyKey(devtype, devid, subdevid, chlid));
        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("\"/>");
        for (BtsHistoryRecord item : history.subList(from, to))
            response.append("<data seqid=\"").append(item.sequenceId()).append("\" stepid=\"").append(item.stepNo())
                    .append("\" cycleid=\"").append(item.cycleNo()).append("\" steptype=\"").append(item.stepType()).append("\" atime=\"")
                    .append(BTS_HISTORY_TIME.format(item.deviceTime())).append("\" testtime=\"60000\" volt=\"")
                    .append(decimal(item.voltageV())).append("\" curr=\"").append(decimal(item.currentA()))
                    .append("\" cap=\"").append(decimal(item.capacityAh())).append("\" eng=\"").append(decimal(item.energyWh())).append("\"/>");
        return response.append("</list></download></bts>").toString();
    }
@@ -542,13 +710,12 @@
            for (ChannelState channel : device.channels())
            {
                String key = historyKey(profile.devtype(), device.deviceId(), profile.subDeviceId(), channel.channelId());
                ArrayList<SimBtsHistory> restored = new ArrayList<>();
                ArrayList<BtsHistoryRecord> 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());
                    restored.addAll(page.records().stream().map(this::normalizeHistoryRecord).toList());
                    if (!page.hasMore()) break;
                    if (page.nextOffset() <= offset) throw new IllegalStateException("Invalid BTS history page");
                    offset = page.nextOffset();
@@ -558,7 +725,152 @@
        }
    }
    private record SimBtsHistory(long sequence, Instant at, double current, double voltage, double capacity, double energy) { }
    private List<BtsHistoryRecord> btsHistoryFor(String key)
    {
        return btsHistory.getOrDefault(key, List.of());
    }
    private BtsHistoryRecord normalizeHistoryRecord(BtsHistoryRecord item)
    {
        return item.stepName() == null || item.stepName().isBlank()
                ? new BtsHistoryRecord(item.sequenceId(), item.deviceTime(), item.currentA(), item.voltageV(),
                        item.capacityAh(), item.energyWh()) : item;
    }
    private String btsKey(DeviceState device, int channelId)
    {
        BtsDeviceProfile profile = profileFor(device);
        return historyKey(profile.devtype(), device.deviceId(), profile.subDeviceId(), channelId);
    }
    private void requireChannel(DeviceState device, int channelId)
    {
        if (channelId < 1 || channelId > device.channels().size())
            throw new IllegalArgumentException("channelId must be between 1 and " + device.channels().size());
    }
    private BtsPlaybackState playbackState(String key, Instant now)
    {
        BtsPlaybackState stored = stateStore.loadBtsPlaybackState(key);
        if (stored != null) return stored;
        List<BtsHistoryRecord> records = btsHistoryFor(key);
        long next = records.stream().mapToLong(BtsHistoryRecord::sequenceId).max().orElse(0L) + 1L;
        BtsExcelFixture.FixtureRecord first = BtsExcelFixture.record(1);
        BtsPlaybackState created = new BtsPlaybackState(true, 1, first.cycleNo(), first.stepNo(), first.stepName(), first.stepType(),
                now.minusSeconds(SIMULATED_RECORD_INTERVAL_SECONDS), now, next, false);
        stateStore.saveBtsPlaybackState(key, created);
        return created;
    }
    private BtsHistoryRecord latestRecord(String key)
    {
        List<BtsHistoryRecord> records = btsHistoryFor(key);
        return records.isEmpty() ? null : records.get(records.size() - 1);
    }
    private double latestCapacity(String key)
    {
        BtsHistoryRecord latest = latestRecord(key);
        return latest == null ? 0d : latest.capacityAh();
    }
    private BtsPlaybackState advanceBtsPlayback(String key, BtsDeviceProfile profile, BtsPlaybackState current,
            double currentA, double voltageV)
    {
        return advanceBtsPlayback(key, profile, current, currentA, voltageV, Instant.now());
    }
    private BtsPlaybackState advanceBtsPlayback(String key, BtsDeviceProfile profile, BtsPlaybackState current,
            double currentA, double voltageV, Instant now)
    {
        if (!current.playing() || current.naturallyFinished()) return current;
        long elapsedMillis = Math.max(0L, java.time.Duration.between(current.wallClockAt(), now).toMillis())
                * current.speedMultiplier();
        List<BtsHistoryRecord> history = new ArrayList<>(btsHistoryFor(key));
        List<BtsHistoryRecord> appended = new ArrayList<>();
        Instant simulated = current.simulatedAt();
        long sequence = current.nextSequence();
        if (sequence < 1) sequence = 1;
        if (sequence > BtsExcelFixture.recordCount() + 1L)
            sequence = BtsExcelFixture.recordCount() + 1L;
        long lastOffset = sequence == 1 ? -SIMULATED_RECORD_INTERVAL_SECONDS
                : BtsExcelFixture.record(sequence - 1).offsetSeconds();
        long budgetMillis = elapsedMillis;
        while (sequence <= BtsExcelFixture.recordCount())
        {
            BtsExcelFixture.FixtureRecord fixture = BtsExcelFixture.record(sequence);
            long deltaSeconds = fixture.offsetSeconds() - lastOffset;
            long deltaMillis = Math.max(1L, deltaSeconds * 1000L);
            boolean firstRecord = history.isEmpty() && sequence == 1L;
            if (!firstRecord && budgetMillis < deltaMillis) break;
            budgetMillis = Math.max(0L, budgetMillis - deltaMillis);
            simulated = simulated.plusSeconds(deltaSeconds);
            BtsHistoryRecord record = new BtsHistoryRecord(fixture.sequenceId(), simulated, fixture.currentA(), fixture.voltageV(),
                    fixture.capacityAh(), fixture.energyWh(), fixture.cycleNo(), fixture.stepNo(), fixture.stepName(), fixture.stepType());
            history.add(record);
            appended.add(record);
            lastOffset = fixture.offsetSeconds();
            sequence++;
        }
        if (appended.isEmpty()) return current;
        if (history.size() > BtsSimulatorStateStore.MAX_HISTORY_RECORDS)
            history = new ArrayList<>(history.subList(history.size() - (int) BtsSimulatorStateStore.MAX_HISTORY_RECORDS,
                    history.size()));
        BtsExcelFixture.FixtureRecord latest = BtsExcelFixture.record(sequence - 1L);
        boolean finished = sequence > BtsExcelFixture.recordCount();
        BtsPlaybackState next = new BtsPlaybackState(!finished && current.playing(), current.speedMultiplier(),
                latest.cycleNo(), latest.stepNo(), latest.stepName(), latest.stepType(), simulated, now, sequence, finished);
        stateStore.saveBtsPlaybackAndAppendHistory(key, next, appended);
        btsHistory.put(key, history);
        return next;
    }
    private BtsPlaybackState defaultPlayback(Instant now)
    {
        BtsExcelFixture.FixtureRecord first = BtsExcelFixture.record(1);
        return new BtsPlaybackState(true, 1, first.cycleNo(), first.stepNo(), first.stepName(), first.stepType(),
                now.minusSeconds(SIMULATED_RECORD_INTERVAL_SECONDS), now, 1, false);
    }
    private BtsExcelFixture.FixtureRecord nextFixtureStep(int cycleNo, int stepNo)
    {
        return BtsExcelFixture.records().stream()
                .filter(item -> item.cycleNo() > cycleNo || (item.cycleNo() == cycleNo && item.stepNo() > stepNo))
                .findFirst().orElse(null);
    }
    private void savePlayback(String key, BtsPlaybackState playback)
    {
        stateStore.saveBtsPlaybackState(key, playback);
    }
    public record BtsStepRecord(int cycleNo, int stepNo, String stepName, String stepType, long recordCount,
            Instant startedAt, Instant endedAt, double capacityAh, double energyWh) { }
    public record BtsCycleRecord(int cycleNo, long recordCount, Instant startedAt, Instant endedAt,
            double chargeCapacityAh, double dischargeCapacityAh, double chargeEnergyWh, double dischargeEnergyWh) { }
    public record PlaybackSnapshot(int deviceId, int channelId, BtsPlaybackState playback,
            List<BtsHistoryRecord> records, List<BtsStepRecord> steps, List<BtsCycleRecord> cycles) { }
    private static final class BtsStepAccumulator
    {
        private final int cycle; private final int step; private final String name; private final String type;
        private long count; private Instant first; private Instant last; private double capacity; private double energy;
        BtsStepAccumulator(BtsHistoryRecord first) { cycle = first.cycleNo(); step = first.stepNo(); name = first.stepName(); type = first.stepType(); }
        void add(BtsHistoryRecord item) { count++; if (first == null) first = item.deviceTime(); last = item.deviceTime(); capacity = item.capacityAh(); energy = item.energyWh(); }
        BtsStepRecord finish() { return new BtsStepRecord(cycle, step, name, type, count, first, last, capacity, energy); }
    }
    private static final class BtsCycleAccumulator
    {
        private final int cycle; private long count; private Instant first; private Instant last;
        private double maximumCapacity = Double.NEGATIVE_INFINITY; private double minimumCapacity = Double.POSITIVE_INFINITY;
        private double chargeEnergy; private double dischargeEnergy;
        BtsCycleAccumulator(BtsHistoryRecord first) { cycle = first.cycleNo(); }
        void add(BtsHistoryRecord item) { count++; if (first == null) first = item.deviceTime(); last = item.deviceTime(); maximumCapacity = Math.max(maximumCapacity, item.capacityAh()); minimumCapacity = Math.min(minimumCapacity, item.capacityAh()); if (item.currentA() >= 0) chargeEnergy = Math.max(chargeEnergy, item.energyWh()); else dischargeEnergy = Math.max(dischargeEnergy, Math.abs(item.energyWh())); }
        BtsCycleRecord finish() { return new BtsCycleRecord(cycle, count, first, last, Math.max(0d, maximumCapacity), Math.max(0d, maximumCapacity - minimumCapacity), chargeEnergy, dischargeEnergy); }
    }
    private java.util.Set<String> requestedChannels(String request)
    {