yeg
2026-09-29 b78295e8da96c3e5e76befeef9582ce5273dbb74
battery-device-simulator/src/main/java/com/battery/device/simulator/BtsDevelopmentSimulator.java
@@ -7,10 +7,14 @@
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamReader;
import com.battery.device.api.DeviceAdapter;
import com.battery.device.api.model.DeviceWorkStatus;
import com.battery.device.api.model.DeviceConnectionStatus;
import com.battery.device.api.model.DeviceIdentity;
@@ -19,10 +23,12 @@
import com.battery.device.api.model.DetectedDeviceMetadata;
import com.battery.device.api.model.ChannelIdentity;
import com.battery.device.api.model.AdapterCapabilities;
import com.battery.device.api.model.RealtimeSnapshot;
import com.battery.device.api.DeviceObservationAdapter;
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;
@@ -36,8 +42,12 @@
import com.battery.device.simulator.DevelopmentCollectorSimulation;
import com.battery.device.simulator.DevelopmentCollectorSimulation.CollectorRealtimeSnapshot;
/** 提供由开发后端托管的临时只读 BTS 模拟端点。 */
public final class BtsDevelopmentSimulator implements TestDataObservationAdapter
/**
 * 提供由开发后端托管的临时只读 BTS 模拟端点。
 * 同时实现发现与测试数据观测契约:模拟器直接暴露自身状态,因此发现与取数按同一实例解析,
 * 不再需要单独注入一个扩展观测 Bean。TCP 端点仍保留给协议层测试使用。
 */
public final class BtsDevelopmentSimulator implements DeviceAdapter, TestDataObservationAdapter
{
    public static final String HOST = "127.0.0.1";
    public static final int DEFAULT_PORT = 15020;
@@ -58,12 +68,14 @@
    private static final ZoneId DEVICE_ZONE = ZoneId.of("Asia/Hong_Kong");
    private static final DateTimeFormatter DEVICE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
    /** 返回固定的适配器实例代码,用于把模拟观测归属到 dev-sim 这一实例。 */
    @Override
    public String adapterCode()
    {
        return ADAPTER_CODE;
    }
    /** 返回只观测能力:支持发现与实时观测,不支持历史下载,也不支持向设备下发控制命令。 */
    @Override
    public AdapterCapabilities capabilities()
    {
@@ -103,6 +115,34 @@
        return java.util.Map.copyOf(result);
    }
    /** 直接提供模拟器实时快照;不可观测通道不出现在结果里,与 BTS 协议解析后的语义一致。 */
    @Override
    public synchronized Map<ChannelIdentity, RealtimeSnapshot> inquire(Set<ChannelIdentity> channels)
    {
        if (channels == null || channels.isEmpty()) return Map.of();
        Map<ChannelIdentity, RealtimeSnapshot> result = new LinkedHashMap<>();
        for (ChannelIdentity requested : channels)
        {
            if (!canObserve(requested)) continue;
            CabinetRealtime measurement = cabinetMeasurement(requested);
            if (measurement == null) continue;
            DeviceObservation observed = observation(requested);
            result.put(requested, new RealtimeSnapshot(requested, observed.workStatus(), measurement.cycleNo(),
                    measurement.stepNo(), measurement.stepType(), null, BigDecimal.valueOf(measurement.currentA()),
                    BigDecimal.valueOf(measurement.voltageV()), BigDecimal.valueOf(measurement.capacityAh()),
                    BigDecimal.valueOf(measurement.energyWh()), totalTimeSeconds(requested),
                    measurement.relativeTimeSeconds(), null, measurement.serverReceivedAt(), Map.of(),
                    DeviceConnectionStatus.ONLINE, measurement.deviceTime(), List.of(), null));
        }
        recordRequest("inquire", channels.toString(), result.toString());
        return Map.copyOf(result);
    }
    private long totalTimeSeconds(ChannelIdentity identity)
    {
        return requireDevice(identity.devid()).channels().get(identity.chlid() - 1).configuration().totalTimeSeconds();
    }
    private DeviceObservation observation(ChannelIdentity requested)
    {
        DeviceState device = state.devices().stream().filter(item -> item.deviceId() == requested.devid())
@@ -134,6 +174,7 @@
        simulator = ScriptedBtsTcpSimulator.responding(port, this::respond);
    }
    /** 返回模拟器 TCP 端点实际监听的端口。 */
    public int port()
    {
        return simulator.port();
@@ -173,7 +214,7 @@
                state.nextCollectorId(), tombstones));
    }
    /** 使用现场确认的新威检测柜 profile 新增设备,型号之外的控制语义仍保持只读。 */
    /** 按预设新威型号元数据新增模拟设备;真实协议和拓扑仍待现场验证。 */
    public synchronized int addNewareDevice(String ip, String model)
    {
        BtsDeviceProfile profile = newareProfile(model);
@@ -218,10 +259,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 检测柜拓扑隔离的开发采集器。 */
@@ -260,7 +317,7 @@
        persist(next);
    }
    /** 配置一个稳定 S 通道的固定或动态演示指标。 */
    /** 保存稳定 S 通道的固定测量值及自定义指标,下一次请求时形成观测。 */
    public synchronized void configureCollectorChannel(int collectorId, String channelCode,
            List<CollectorMetricState> metrics)
    {
@@ -297,14 +354,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,20 +396,19 @@
        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(), configuration.relativeTimeSeconds());
        return result;
    }
    /** 请求采集器当前配置值;软件不可访问或设备离线时返回空测量。 */
    @Override
    public synchronized CollectorRealtime latestCollector(String collectorCode)
    public synchronized CollectorRealtime latestCollector(String adapterCode, String collectorCode)
    {
        CollectorState collector = state.collectors().stream().filter(item -> item.deviceCode().equals(collectorCode))
                .findFirst().orElse(null);
@@ -399,14 +470,14 @@
        return sequence;
    }
    /** 返回控制页需要的运行状态快照。 */
    /** 返回模拟器配置、当前状态和最近请求,不触发新的设备观测。 */
    public synchronized StateSnapshot snapshot()
    {
        List<DeviceSnapshot> deviceSnapshots = state.devices().stream()
                .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 +652,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 +734,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)
@@ -756,7 +823,7 @@
    private void persist(SimulatorState nextState)
    {
        // 写穿失败时保留当前内存副本,避免控制页显示未持久化状态。
        // 写穿失败时保留当前内存副本,避免模拟器页面显示未持久化状态。
        SimulatorState configured = new SimulatorState(nextState.version(), nextState.devices(), nextState.collectors(),
                nextState.nextDeviceId(), nextState.nextCollectorId(), nextState.tombstones(),
                state.softwareAccessible(), state.observationSequence());
@@ -826,6 +893,7 @@
            throw new IllegalStateException("Invalid BTS simulator numbering or tombstone state in Redis");
    }
    /** 停止模拟器:标记为不再运行并释放 TCP 端点;释放失败时以运行时异常报告。 */
    @Override
    public synchronized void close()
    {
@@ -848,7 +916,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,