yeg
2026-09-05 f522c9ffa487e3710d691e8b98df8c4f0cfafd43
battery-device-simulator/src/main/java/com/battery/device/simulator/BtsDevelopmentSimulator.java
@@ -23,6 +23,7 @@
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;
@@ -218,10 +219,26 @@
        if (channelId < 1 || channelId > device.channels().size())
            throw new IllegalArgumentException("channelId must be between 1 and " + device.channels().size());
        List<ChannelState> channels = new ArrayList<>(device.channels());
        channels.set(channelId - 1, new ChannelState(channelId, online, workStatus));
        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<ChannelState> 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 检测柜拓扑隔离的开发采集器。 */
@@ -297,14 +314,29 @@
        replaceCollector(DevelopmentCollectorSimulation.configureStatus(requireCollector(collectorId), channelCode, status));
    }
    /** 返回固定的开发计划,不推进工步或生成历史。 */
    /** 返回通道已配置的开发方案及当前工步位置,不自动推进或生成历史。 */
    @Override
    public synchronized TestPlan testPlan(ChannelIdentity channel)
    {
        if (!canObserve(channel)) return null;
        return new TestPlan(1, 1, 1, List.of(new PlanRow("STEP", 1, 1, "恒流充电", 60d, 3.7d,
                requireDevice(channel.devid()).channels().get(channel.chlid() - 1).workStatus() == DeviceWorkStatus.FINISHED
                        ? "已完成" : "待执行", null, null)));
        ChannelState source = requireDevice(channel.devid()).channels().get(channel.chlid() - 1);
        ChannelConfiguration configuration = source.configuration();
        List<PlanRow> 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);
    }
    /** 请求当前检测柜测量;离线或无法观测时不返回缓存值。 */
@@ -324,14 +356,13 @@
        if (channel.workStatus() == DeviceWorkStatus.UNOBSERVABLE) return null;
        Instant now = Instant.now().truncatedTo(java.time.temporal.ChronoUnit.MILLIS);
        long sequence = nextObservationSequence();
        double wave = Math.sin((sequence + channel.channelId()) * 0.7d);
        BtsDeviceProfile profile = profileFor(device);
        double current = channel.workStatus() == DeviceWorkStatus.WORKING
                ? clamp(profile.currentMaxA().doubleValue() * (0.18d + wave * 0.02d),
                        profile.currentMinA().doubleValue(), profile.currentMaxA().doubleValue()) : 0d;
        double voltage = clamp(3.7d + wave * 0.01d, profile.voltageMinV().doubleValue(), profile.voltageMaxV().doubleValue());
        CabinetRealtime result = new CabinetRealtime(sequence, now, current, voltage, 1.2d, 4.44d,
                1, 1, "恒流充电", "cc", now, DEVICE_TIME.format(now.atZone(DEVICE_ZONE)), DEVICE_ZONE.getId());
        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());
        return result;
    }
@@ -406,7 +437,7 @@
                .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.workStatus(), channel.configuration()))
                                .toList()))
                .toList();
        List<CollectorSnapshot> collectorSnapshots = state.collectors().stream()
@@ -581,7 +612,8 @@
                    .append("\" current=\"").append(decimal(current)).append("\" voltage=\"")
                    .append(decimal(voltage)).append("\" capacity=\"").append(decimal(capacity))
                    .append("\" energy=\"").append(decimal(energy)).append("\" totaltime=\"")
                    .append(0).append("\" relativetime=\"").append(0)
                    .append(channel.configuration().totalTimeSeconds()).append("\" relativetime=\"")
                    .append(channel.configuration().relativeTimeSeconds())
                    .append("\" open_or_close=\"0\" log_code=\"0\">")
                    // 模拟器返回当前设备方案,供统一观测模型展示;不代表真实 BTS 的完整方案字段。
                    .append("<plan><step step_no=\"").append(stepNo).append("\" step_type=\"")
@@ -662,14 +694,9 @@
        throw new IllegalArgumentException("Unsupported Neware simulator model: " + model);
    }
    private double clamp(double value, double minimum, double maximum)
    {
        return Math.max(minimum, Math.min(maximum, value));
    }
    private String decimal(double value)
    {
        return String.format(java.util.Locale.ROOT, "%.3f", value);
        return java.math.BigDecimal.valueOf(value).toPlainString();
    }
    private String toBtsWorkStatus(DeviceWorkStatus status)
@@ -848,7 +875,8 @@
    public record DeviceSnapshot(int deviceId, String ip, boolean online, BtsDeviceProfile profile,
            List<ChannelSnapshot> channels) { }
    public record ChannelSnapshot(int channelId, boolean online, DeviceWorkStatus workStatus) { }
    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,