| | |
| | | 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; |
| | |
| | | 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 检测柜拓扑隔离的开发采集器。 */ |
| | |
| | | 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); |
| | | } |
| | | |
| | | /** 请求当前检测柜测量;离线或无法观测时不返回缓存值。 */ |
| | |
| | | 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; |
| | | } |
| | | |
| | |
| | | .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() |
| | |
| | | .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=\"") |
| | |
| | | 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) |
| | |
| | | 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, |