From 1c26cdb81070af7f29df4462d3bfc1ae588ff8f3 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期日, 30 八月 2026 14:11:34 +0800
Subject: [PATCH] 模拟器优化
---
ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java | 179 ++++++++++++++++++++++++++++++++++++++++++++++++++++-------
1 files changed, 156 insertions(+), 23 deletions(-)
diff --git a/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java b/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java
index 5c4a71b..1af43b7 100644
--- a/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java
+++ b/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java
@@ -2,6 +2,7 @@
import java.io.IOException;
import java.io.StringReader;
+import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@@ -20,6 +21,7 @@
import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorRealtimeMeasurement;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorState;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.DeviceState;
+import com.ruoyi.device.simulator.BtsSimulatorStateStore.BtsDeviceProfile;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.RecentRequest;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.SimulatorState;
import com.ruoyi.device.simulator.InMemoryBtsSimulatorStateStore;
@@ -36,9 +38,9 @@
public static final String ADAPTER_CODE = "dev-sim";
public static final String ADAPTER_NAME = "dev-sim BTS 模拟器";
public static final int POLL_INTERVAL_MS = 5000;
+ public static final String NEWARE_CT_4008T_5V6A_S1_F = "CT-4008T-5V6A-S1-F";
+ public static final String NEWARE_CT_4008TN_5V12A_S1_F = "CT-4008Tn-5V12A-S1-F";
- private static final int DEVICE_TYPE = 22;
- private static final int SUB_DEVICE_ID = 1;
private static final int MAX_CHANNEL_COUNT = 99;
private static final int MAX_RECENT_REQUESTS = 20;
private static final int TOMBSTONE_DISCOVERY_COUNT = 3;
@@ -47,6 +49,7 @@
private final BtsSimulatorStateStore stateStore;
private SimulatorState state;
private boolean running = true;
+ private long observationSequence;
/** 启动仅监听 IPv4 本机回环地址的开发模拟器;传入 0 时由系统分配测试端口。 */
public BtsDevelopmentSimulator(int port) throws IOException
@@ -75,7 +78,7 @@
validateDeviceConfiguration(ip, channelCount, null);
int deviceId = state.nextDeviceId();
List<DeviceState> devices = new ArrayList<>(state.devices());
- devices.add(newDevice(deviceId, ip, channelCount));
+ devices.add(newDevice(deviceId, ip, BtsDeviceProfile.virtual(channelCount)));
persist(new SimulatorState(BtsSimulatorStateStore.CURRENT_VERSION, devices, state.collectors(),
deviceId + 1, state.nextCollectorId(), state.tombstones()));
return deviceId;
@@ -93,13 +96,35 @@
? current.channels().get(channelId - 1)
: new ChannelState(channelId, true, DeviceWorkStatus.STOPPED));
}
- List<DeviceState> devices = replaceDevice(new DeviceState(deviceId, ip.trim(), online, channels));
+ List<DeviceState> devices = replaceDevice(new DeviceState(deviceId, ip.trim(), online, channels,
+ resizeProfile(current.profile(), channelCount)));
List<ChannelTombstone> tombstones = new ArrayList<>(state.tombstones());
for (int channelId = channelCount + 1; channelId <= current.channels().size(); channelId++)
upsertTombstone(tombstones, current.deviceId(), current.ip(), channelId);
tombstones.removeIf(item -> item.deviceId() == deviceId && item.channelId() <= channelCount);
persist(new SimulatorState(state.version(), devices, state.collectors(), state.nextDeviceId(),
state.nextCollectorId(), tombstones));
+ }
+
+ /** 使用现场确认的新威检测柜 profile 新增设备,型号之外的控制语义仍保持只读。 */
+ public synchronized int addNewareDevice(String ip, String model)
+ {
+ BtsDeviceProfile profile = newareProfile(model);
+ validateDeviceConfiguration(ip, profile.channelCount(), null);
+ int deviceId = state.nextDeviceId();
+ List<DeviceState> devices = new ArrayList<>(state.devices());
+ devices.add(newDevice(deviceId, ip, profile));
+ persist(new SimulatorState(state.version(), devices, state.collectors(), deviceId + 1,
+ state.nextCollectorId(), state.tombstones()));
+ return deviceId;
+ }
+
+ /** 将已有模拟设备切换为新威 profile,并按 profile 通道数调整拓扑。 */
+ public synchronized void configureNewareProfile(int deviceId, String model)
+ {
+ DeviceState current = requireDevice(deviceId);
+ BtsDeviceProfile profile = newareProfile(model);
+ updateDeviceWithProfile(current, profile);
}
/** 删除指定开发设备;后续发现响应不再返回它。 */
@@ -126,7 +151,7 @@
List<ChannelState> channels = new ArrayList<>(device.channels());
channels.set(channelId - 1, new ChannelState(channelId, online, workStatus));
persist(new SimulatorState(state.version(),
- replaceDevice(new DeviceState(deviceId, device.ip(), device.online(), channels)), state.collectors(),
+ replaceDevice(new DeviceState(deviceId, device.ip(), device.online(), channels, device.profile())), state.collectors(),
state.nextDeviceId(), state.nextCollectorId(), state.tombstones()));
}
@@ -194,7 +219,7 @@
device.channels().stream()
.map(channel -> new ChannelState(channel.channelId(), poweredOn,
DeviceWorkStatus.STOPPED))
- .toList()))
+ .toList(), device.profile()))
.toList();
Instant changedAt = Instant.now();
List<CollectorState> collectors = state.collectors().stream()
@@ -247,7 +272,7 @@
public synchronized StateSnapshot snapshot()
{
List<DeviceSnapshot> deviceSnapshots = state.devices().stream()
- .map(device -> new DeviceSnapshot(device.deviceId(), device.ip(), device.online(),
+ .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()))
@@ -281,10 +306,15 @@
private DeviceState newDevice(int deviceId, String ip, int channelCount)
{
- List<ChannelState> channels = new ArrayList<>(channelCount);
- for (int channelId = 1; channelId <= channelCount; channelId++)
+ return newDevice(deviceId, ip, BtsDeviceProfile.virtual(channelCount));
+ }
+
+ private DeviceState newDevice(int deviceId, String ip, BtsDeviceProfile profile)
+ {
+ List<ChannelState> channels = new ArrayList<>(profile.channelCount());
+ for (int channelId = 1; channelId <= profile.channelCount(); channelId++)
channels.add(new ChannelState(channelId, true, DeviceWorkStatus.STOPPED));
- return new DeviceState(deviceId, ip.trim(), true, channels);
+ return new DeviceState(deviceId, ip.trim(), true, channels, profile);
}
private CollectorState newCollector(int collectorId, String ip, int channelCount)
@@ -309,7 +339,7 @@
return switch (command)
{
case "getdevinfo" -> deviceInfoResponse();
- case "inquire" -> observationResponse();
+ case "inquire" -> observationResponse(request);
default -> "<bts version=\"1.0\"><cmd>error</cmd></bts>";
};
}
@@ -376,34 +406,134 @@
boolean available)
{
// 展示元数据是开发模拟器扩展,不代表真实 BTS 协议必然提供这些属性。
- response.append("<channel ip=\"").append(ip).append("\" devtype=\"").append(DEVICE_TYPE)
- .append("\" devid=\"").append(deviceId).append("\" subdevid=\"").append(SUB_DEVICE_ID)
- .append("\" Channelid=\"").append(channelId).append("\" name=\"dev-sim 设备 ")
- .append(deviceId).append("\" manufacturer=\"开发模拟器\" model=\"BTS Virtual Cabinet\" mac=\"")
- .append(simulatorMac(DEVICE_TYPE, deviceId)).append("\">")
+ DeviceState device = state.devices().stream().filter(item -> item.deviceId() == deviceId).findFirst().orElse(null);
+ BtsDeviceProfile profile = device == null ? BtsDeviceProfile.virtual(channelId) : profileFor(device);
+ response.append("<channel ip=\"").append(ip).append("\" devtype=\"").append(profile.devtype())
+ .append("\" devid=\"").append(deviceId).append("\" subdevid=\"").append(profile.subDeviceId())
+ .append("\" Channelid=\"").append(channelId).append("\" name=\"").append(profile.name())
+ .append("\" manufacturer=\"").append(profile.manufacturer()).append("\" model=\"")
+ .append(profile.model()).append("\" serialNumber=\"").append(profile.serialNumber())
+ .append("\" firmwareVersion=\"").append(profile.firmwareVersion()).append("\" mac=\"")
+ .append(simulatorMac(profile.devtype(), deviceId)).append("\">")
.append(available).append("</channel>");
}
- private synchronized String observationResponse()
+ private synchronized String observationResponse(String request)
{
+ long tick = ++observationSequence;
+ java.util.Set<String> requested = requestedChannels(request);
List<ObservedChannel> available = new ArrayList<>();
for (DeviceState device : state.devices())
for (ChannelState channel : device.channels())
- if (device.online() && channel.online()) available.add(new ObservedChannel(device.deviceId(), channel));
+ if (device.online() && channel.online()
+ && (requested.isEmpty() || requested.contains(device.deviceId() + ":" + channel.channelId())))
+ available.add(new ObservedChannel(device.deviceId(), channel));
StringBuilder response = new StringBuilder("<bts version=\"1.0\"><cmd>inquire_resp</cmd><list count=\"")
.append(available.size()).append("\">");
for (ObservedChannel observed : available)
{
ChannelState channel = observed.channel();
- response.append("<inquire dev=\"").append(DEVICE_TYPE).append('-').append(observed.deviceId())
- .append('-').append(SUB_DEVICE_ID).append('-').append(channel.channelId())
+ DeviceState device = requireDevice(observed.deviceId());
+ BtsDeviceProfile profile = profileFor(device);
+ boolean working = channel.workStatus() == DeviceWorkStatus.WORKING;
+ double wave = Math.sin((tick + channel.channelId()) * 0.7d);
+ double current = 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());
+ double capacity = working ? Math.max(0d, tick * 0.001d * current) : 0d;
+ double energy = capacity * voltage;
+ 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(toBtsWorkStatus(channel.workStatus()))
- .append("\" current=\"0\" voltage=\"3.700\" capacity=\"0\" energy=\"0\" ")
- .append("totaltime=\"0\" relativetime=\"0\" open_or_close=\"0\" log_code=\"0\"/>");
+ .append("\" current=\"").append(decimal(current)).append("\" voltage=\"")
+ .append(decimal(voltage)).append("\" capacity=\"").append(decimal(capacity))
+ .append("\" energy=\"").append(decimal(energy)).append("\" totaltime=\"")
+ .append(working ? tick : 0).append("\" relativetime=\"").append(working ? tick : 0)
+ .append("\" open_or_close=\"0\" log_code=\"0\"/>");
}
return response.append("</list></bts>").toString();
+ }
+
+ private java.util.Set<String> requestedChannels(String request)
+ {
+ java.util.Set<String> channels = new java.util.HashSet<>();
+ try
+ {
+ XMLInputFactory factory = XMLInputFactory.newFactory();
+ factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
+ factory.setProperty("javax.xml.stream.isSupportingExternalEntities", false);
+ XMLStreamReader reader = factory.createXMLStreamReader(new StringReader(request));
+ while (reader.hasNext())
+ {
+ if (reader.next() == XMLStreamConstants.START_ELEMENT && "inquire".equals(reader.getLocalName()))
+ {
+ String devid = reader.getAttributeValue(null, "devid");
+ String chlid = reader.getAttributeValue(null, "chlid");
+ if (devid != null && chlid != null) channels.add(devid + ":" + chlid);
+ }
+ }
+ return channels;
+ }
+ catch (Exception exception)
+ {
+ throw new IllegalArgumentException("Invalid BTS inquire XML", exception);
+ }
+ }
+
+ private BtsDeviceProfile profileFor(DeviceState device)
+ {
+ return device.profile() == null ? BtsDeviceProfile.virtual(device.channels().size()) : device.profile();
+ }
+
+ private BtsDeviceProfile resizeProfile(BtsDeviceProfile profile, int channelCount)
+ {
+ return new BtsDeviceProfile(profile.devtype(), profile.subDeviceId(), profile.name(), profile.manufacturer(),
+ profile.model(), profile.serialNumber(), profile.firmwareVersion(), channelCount,
+ profile.currentMinA(), profile.currentMaxA(), profile.voltageMinV(), profile.voltageMaxV(),
+ profile.sampleIntervalMs());
+ }
+
+ private void updateDeviceWithProfile(DeviceState current, BtsDeviceProfile profile)
+ {
+ int channelCount = profile.channelCount();
+ List<ChannelState> channels = new ArrayList<>(channelCount);
+ for (int channelId = 1; channelId <= channelCount; channelId++)
+ channels.add(channelId <= current.channels().size() ? current.channels().get(channelId - 1)
+ : new ChannelState(channelId, true, DeviceWorkStatus.STOPPED));
+ List<DeviceState> devices = replaceDevice(new DeviceState(current.deviceId(), current.ip(), current.online(),
+ channels, profile));
+ List<ChannelTombstone> tombstones = new ArrayList<>(state.tombstones());
+ for (int channelId = channelCount + 1; channelId <= current.channels().size(); channelId++)
+ upsertTombstone(tombstones, current.deviceId(), current.ip(), channelId);
+ tombstones.removeIf(item -> item.deviceId() == current.deviceId() && item.channelId() <= channelCount);
+ persist(new SimulatorState(state.version(), devices, state.collectors(), state.nextDeviceId(),
+ state.nextCollectorId(), tombstones));
+ }
+
+ private BtsDeviceProfile newareProfile(String model)
+ {
+ if (NEWARE_CT_4008T_5V6A_S1_F.equals(model))
+ return new BtsDeviceProfile(22, 1, "新威 CT-4008T", "新威", model, "SIM-CT4008T-5V6A",
+ "BTS7.5.4", 8, new BigDecimal("-6.000"), new BigDecimal("6.000"), BigDecimal.ZERO,
+ new BigDecimal("5.000"), 100);
+ if (NEWARE_CT_4008TN_5V12A_S1_F.equals(model))
+ return new BtsDeviceProfile(22, 1, "新威 CT-4008Tn", "新威", model, "SIM-CT4008TN-5V12A",
+ "BTS8.0", 8, new BigDecimal("-12.000"), new BigDecimal("12.000"), BigDecimal.ZERO,
+ new BigDecimal("5.000"), 100);
+ 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);
}
private String toBtsWorkStatus(DeviceWorkStatus status)
@@ -531,6 +661,8 @@
|| !ips.add(device.ip()) || device.channels().isEmpty()
|| device.channels().size() > MAX_CHANNEL_COUNT)
throw new IllegalStateException("Invalid BTS simulator device state in Redis");
+ if (profileFor(device).channelCount() != device.channels().size())
+ throw new IllegalStateException("BTS simulator device profile channel count does not match topology");
maximumDeviceId = Math.max(maximumDeviceId, device.deviceId());
for (int index = 0; index < device.channels().size(); index++)
{
@@ -572,7 +704,8 @@
public record StateSnapshot(boolean running, String listenAddress, List<DeviceSnapshot> devices,
List<CollectorSnapshot> collectors, List<RecentRequest> recentRequests) { }
- public record DeviceSnapshot(int deviceId, String ip, boolean online, List<ChannelSnapshot> channels) { }
+ public record DeviceSnapshot(int deviceId, String ip, boolean online, BtsDeviceProfile profile,
+ List<ChannelSnapshot> channels) { }
public record ChannelSnapshot(int channelId, boolean online, DeviceWorkStatus workStatus) { }
--
Gitblit v1.7.1