yeg
2026-09-09 a011865a0989b51d5818f2791d04790b6408acd1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
package com.battery.device.simulator;
 
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 com.battery.device.api.model.DeviceWorkStatus;
import com.battery.device.simulator.BtsSimulatorStateStore.*;
 
/** 提供请求时的采集器当前值;S 通道仍是待现场验证的开发合同。 */
public final class DevelopmentCollectorSimulation
{
    public static final int DEFAULT_SAMPLE_INTERVAL_MS = 1000;
    private static final ZoneId DEVICE_ZONE = ZoneId.of("Asia/Hong_Kong");
    private static final DateTimeFormatter DEVICE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
 
    private DevelopmentCollectorSimulation() { }
 
    /** 创建带稳定 S 通道和三项固定演示指标的采集器状态。 */
    public static List<CollectorChannelState> defaultChannels(int channelCount)
    {
        List<CollectorChannelState> channels = new ArrayList<>(channelCount);
        for (int index = 1; index <= channelCount; index++)
        {
            channels.add(new CollectorChannelState(channelCode(index), List.of(
                    new CollectorMetricState("temperature", new BigDecimal("25.0")
                            .add(BigDecimal.valueOf(index, 1)), "℃"),
                    new CollectorMetricState("positive_potential", new BigDecimal("3.700"), "V"),
                    new CollectorMetricState("negative_potential", new BigDecimal("-0.100"), "V"))));
        }
        return List.copyOf(channels);
    }
 
 
    /** 调整设备拓扑,保留已有通道的配置。 */
    public static CollectorState resize(CollectorState current, String ip, int channelCount, boolean online, Instant changedAt)
    {
        var defaults = defaultChannels(channelCount);
        List<CollectorChannelState> channels = new ArrayList<>();
        for (int index = 0; index < channelCount; index++)
            channels.add(index < current.channels().size() ? current.channels().get(index) : defaults.get(index));
        return new CollectorState(current.collectorId(), current.deviceCode(), ip.trim(), current.mac(),
                current.name(), current.manufacturer(), current.model(), channelCount, online,
                current.sampleIntervalMs(), channels, List.of());
    }
 
    /** 保存当前测量配置,等待下一次实时请求。 */
    public static CollectorState configureChannel(CollectorState current, String channelCode, List<CollectorMetricState> metrics)
    {
        validateMetrics(metrics);
        int index = parseChannelIndex(channelCode, current.channelCount());
        List<CollectorChannelState> channels = new ArrayList<>(current.channels());
        channels.set(index, new CollectorChannelState(channelCode, metrics, channels.get(index).workStatus()));
        return copy(current, channels, List.of());
    }
 
    /** 保存通道工作状态,等待下一次实时请求。 */
    public static CollectorState configureStatus(CollectorState current, String channelCode, DeviceWorkStatus status)
    {
        int index = parseChannelIndex(channelCode, current.channelCount());
        List<CollectorChannelState> channels = new ArrayList<>(current.channels());
        channels.set(index, new CollectorChannelState(channelCode, channels.get(index).metrics(), status));
        return copy(current, channels, List.of());
    }
 
    /** 仅生成本次请求的当前值,不补算上一次请求与本次之间的空档。 */
    public static CollectorState sample(CollectorState current, Instant now, long sequence)
    {
        Instant observedAt = now.truncatedTo(java.time.temporal.ChronoUnit.MILLIS);
        List<CollectorRealtimeMeasurement> measurements = new ArrayList<>();
        for (CollectorChannelState channel : current.channels())
        {
            if (channel.workStatus() == DeviceWorkStatus.UNOBSERVABLE) continue;
            for (CollectorMetricState metric : channel.metrics())
                measurements.add(new CollectorRealtimeMeasurement("dev-sim", current.deviceCode(), channel.channelCode(),
                        sequence, observedAt, DEVICE_TIME_FORMATTER.format(observedAt.atZone(DEVICE_ZONE)), observedAt,
                        metric.metricCode(), metric.value(), metric.unit()));
        }
        return copy(current, current.channels(), measurements);
    }
 
    /** 校验恢复后的稳定通道和测量配置。 */
    public static void validate(CollectorState state)
    {
        if (state.channels().size() != state.channelCount()) throw new IllegalStateException("Invalid collector topology");
        for (int index = 0; index < state.channels().size(); index++)
        {
            var channel = state.channels().get(index);
            if (!channelCode(index + 1).equals(channel.channelCode())) throw new IllegalStateException("Invalid collector channel identity");
            validateMetrics(channel.metrics());
        }
    }
 
    private static CollectorState copy(CollectorState current, List<CollectorChannelState> channels,
            List<CollectorRealtimeMeasurement> measurements)
    {
        return new CollectorState(current.collectorId(), current.deviceCode(), current.ip(), current.mac(),
                current.name(), current.manufacturer(), current.model(), current.channelCount(), current.online(),
                current.sampleIntervalMs(), channels, measurements);
    }
 
    private static void validateMetrics(List<CollectorMetricState> metrics)
    {
        if (metrics == null || metrics.isEmpty())
            throw new IllegalArgumentException("Collector channel must contain at least one metric");
        java.util.HashSet<String> codes = new java.util.HashSet<>();
        for (CollectorMetricState metric : metrics)
        {
            if (metric.metricCode() == null || metric.metricCode().isBlank() || metric.value() == null
                    || !metric.metricCode().equals(metric.metricCode().trim())
                    || metric.unit() == null || metric.unit().isBlank()
                    || !metric.unit().equals(metric.unit().trim()) || !codes.add(metric.metricCode()))
                throw new IllegalArgumentException("Collector metric code, value and unit must be valid and unique");
        }
    }
 
    private static int parseChannelIndex(String value, int channelCount)
    {
        if (value == null || !value.matches("S-\\d{2}"))
            throw new IllegalArgumentException("Collector channel must use S-01...S-N identity");
        int index = Integer.parseInt(value.substring(2)) - 1;
        if (index < 0 || index >= channelCount || !channelCode(index + 1).equals(value))
            throw new IllegalArgumentException("Unknown collector channel: " + value);
        return index;
    }
 
    private static String channelCode(int index)
    {
        return String.format("S-%02d", index);
    }
 
    public record CollectorRealtimeSnapshot(String collectorCode, boolean online, int sampleIntervalMs,
            List<CollectorRealtimeMeasurement> measurements)
    {
        public CollectorRealtimeSnapshot { measurements = List.copyOf(measurements); }
    }
 
}