| | |
| | | import org.w3c.dom.NodeList; |
| | | import org.xml.sax.InputSource; |
| | | import org.xml.sax.helpers.DefaultHandler; |
| | | import com.battery.device.model.ChannelIdentity; |
| | | import com.battery.device.model.DeviceConnectionStatus; |
| | | import com.battery.device.model.DetectedChannel; |
| | | import com.battery.device.model.DetectedDeviceMetadata; |
| | | import com.battery.device.model.DeviceWorkStatus; |
| | | import com.battery.device.model.HistoryAvailability; |
| | | import com.battery.device.model.HistoryPage; |
| | | import com.battery.device.model.HistorySample; |
| | | import com.battery.device.model.Measurement; |
| | | import com.battery.device.model.PlannedStep; |
| | | import com.battery.device.model.RealtimeSnapshot; |
| | | import com.battery.device.api.model.ChannelIdentity; |
| | | import com.battery.device.api.model.DeviceConnectionStatus; |
| | | import com.battery.device.api.model.DetectedChannel; |
| | | import com.battery.device.api.model.DetectedDeviceMetadata; |
| | | import com.battery.device.api.model.DeviceWorkStatus; |
| | | import com.battery.device.api.model.Measurement; |
| | | import com.battery.device.api.model.PlannedStep; |
| | | import com.battery.device.api.model.RealtimeSnapshot; |
| | | |
| | | public final class BtsXmlCodec |
| | | { |
| | |
| | | "dev", "cycle_id", "step_id", "step_type", "workstatus", "barcode", "current", "voltage", |
| | | "capacity", "energy", "totaltime", "relativetime", "auxtemp", "auxvol", "open_or_close", "log_code", |
| | | "device_time", "deviceTime", "time", "atime", "testtime"); |
| | | private static final Set<String> HISTORY_ATTRIBUTES = Set.of( |
| | | "seqid", "stepid", "cycleid", "steptype", "testtime", "atime", "volt", "curr", "cap", "eng"); |
| | | |
| | | private final String adapterCode; |
| | | private final ZoneId deviceZone; |
| | |
| | | return envelope(list.append("</list>").toString()); |
| | | } |
| | | |
| | | /** |
| | | * 查询各通道当前 BTS 测试的历史数据上传进度。 |
| | | */ |
| | | public String inquireHistoryRequest(Set<ChannelIdentity> channels) |
| | | { |
| | | StringBuilder list = new StringBuilder("<cmd>inquiredf</cmd><list count=\"").append(channels.size()).append("\">"); |
| | | for (ChannelIdentity channel : channels) |
| | | { |
| | | requireOwned(channel); |
| | | list.append("<chl devtype=\"").append(channel.devtype()) |
| | | .append("\" devid=\"").append(channel.devid()) |
| | | .append("\" subdevid=\"").append(channel.subdevid()) |
| | | .append("\" chlid=\"").append(channel.chlid()) |
| | | .append("\" testid=\"0\"/>"); |
| | | } |
| | | return envelope(list.append("</list>").toString()); |
| | | } |
| | | |
| | | public String downloadRequest(ChannelIdentity channel, String btsTestId, long startPosition, int limit) |
| | | { |
| | | requireOwned(channel); |
| | | if (btsTestId == null || btsTestId.isBlank()) throw new IllegalArgumentException("btsTestId must not be blank"); |
| | | if (startPosition < 1) throw new IllegalArgumentException("startPosition must be positive"); |
| | | if (limit < 1 || limit > 1000) throw new IllegalArgumentException("BTS history limit must be between 1 and 1000"); |
| | | return envelope("<cmd>download</cmd><download devtype=\"" + channel.devtype() |
| | | + "\" devid=\"" + channel.devid() + "\" subdevid=\"" + channel.subdevid() |
| | | + "\" chlid=\"" + channel.chlid() + "\" auxid=\"0\" testid=\"" + escape(btsTestId) |
| | | + "\" startpos=\"" + startPosition + "\" count=\"" + limit + "\"/>"); |
| | | } |
| | | |
| | | public List<DetectedChannel> parseDeviceInfo(String xml) |
| | | { |
| | |
| | | return Map.copyOf(snapshots); |
| | | } |
| | | |
| | | /** |
| | | * 将 BTS 历史清单映射为厂商无关的通道历史可用性。 |
| | | */ |
| | | public List<HistoryAvailability> parseHistoryAvailability(String xml) |
| | | { |
| | | Document document = parse(xml, "inquiredf_resp"); |
| | | NodeList nodes = firstElement(document, "list").getElementsByTagName("chl"); |
| | | List<HistoryAvailability> availability = new ArrayList<>(nodes.getLength()); |
| | | for (int i = 0; i < nodes.getLength(); i++) |
| | | { |
| | | Element element = (Element) nodes.item(i); |
| | | ChannelIdentity channel = new ChannelIdentity(adapterCode, integerAttribute(element, "devtype"), |
| | | integerAttribute(element, "devid"), integerAttribute(element, "subdevid"), |
| | | integerAttribute(element, "chlid")); |
| | | availability.add(new HistoryAvailability(channel, requiredAttribute(element, "testid"), |
| | | longAttribute(element, "count"), booleanText(element.getTextContent(), "uploadComplete"))); |
| | | } |
| | | return List.copyOf(availability); |
| | | } |
| | | |
| | | public HistoryPage parseHistory(String xml, ChannelIdentity channel, String requestedTestId, long requestedStart) |
| | | { |
| | | Document document = parse(xml, "download_resp"); |
| | | Element download = firstElement(document, "download"); |
| | | String btsTestId = requiredAttribute(download, "testid"); |
| | | // download 是单通道响应,身份不一致时拒绝结果,避免历史事实被归入错误测试。 |
| | | if (integerAttribute(download, "devtype") != channel.devtype() |
| | | || integerAttribute(download, "devid") != channel.devid() |
| | | || integerAttribute(download, "subdevid") != channel.subdevid() |
| | | || integerAttribute(download, "chlid") != channel.chlid() |
| | | || !btsTestId.equals(requestedTestId)) |
| | | throw new BtsProtocolException(BtsProtocolException.Reason.IDENTITY_MISMATCH, |
| | | "BTS history response identity does not match the request"); |
| | | NodeList nodes = firstElement(document, "list").getElementsByTagName("data"); |
| | | List<HistorySample> samples = new ArrayList<>(nodes.getLength()); |
| | | long next = requestedStart; |
| | | for (int i = 0; i < nodes.getLength(); i++) |
| | | { |
| | | Element element = (Element) nodes.item(i); |
| | | long sequence = longAttribute(element, "seqid"); |
| | | String rawTime = requiredAttribute(element, "atime"); |
| | | samples.add(new HistorySample(sequence, integerAttribute(element, "stepid"), integerAttribute(element, "cycleid"), |
| | | requiredAttribute(element, "steptype"), optionalLong(element, "testtime"), parseDeviceTime(rawTime), rawTime, |
| | | optionalDecimal(element, "volt"), optionalDecimal(element, "curr"), optionalDecimal(element, "cap"), |
| | | optionalDecimal(element, "eng"), dynamicMeasurements(element, HISTORY_ATTRIBUTES))); |
| | | // 解析层保留重复和乱序事实;业务层只展示并记录这些设备数据事实。 |
| | | next = Math.max(next, sequence + 1); |
| | | } |
| | | return new HistoryPage(channel, btsTestId, requestedStart, next, samples); |
| | | } |
| | | |
| | | private Document parse(String xml, String expectedCommand) |
| | | { |