battery-device-bts/src/main/java/com/battery/device/bts/NewareBtsAdapter.java
@@ -62,6 +62,11 @@
    private final AdapterCapabilities capabilities;
    /** 每通道最近一次工步层读取;见 {@link #stepLayer}。 */
    private final Map<String, StepLayer> stepLayers = new ConcurrentHashMap<>();
    /**
     * 该客户端有没有数据文件命令({@code inquiredf} / {@code download} / {@code downloadStepLayer})。
     * 未知为 null;探测一次后固定下来——见 {@link #supportsDataFileCommands}。
     */
    private volatile Boolean dataFileCommandsSupported;
    public NewareBtsAdapter(String adapterCode, BtsTransport transport, BtsXmlCodec codec)
    {
@@ -191,6 +196,12 @@
        }
        catch (BtsProtocolException unsupported)
        {
            // 客户端不提供 inquiredf(实测 7.6.0.1475):本来源不按设备数据行接收,逐点回落到轮询槽位。
            return null;
        }
        catch (BtsTransportException unanswered)
        {
            // 探测说支持、发出去却不作答(已登记不再试):同样回落到轮询槽位,不把来源不可达说成"不按行接收"。
            return null;
        }
        if (state.count() < 1) return new SampleWatermark(null, 0L);
@@ -256,9 +267,19 @@
    private StepLayer fetchStepLayer(ChannelIdentity channel)
    {
        long fetchedAtNanos = System.nanoTime();
        // 客户端没有这条命令时一条都不发:见 supportsDataFileCommands。
        if (!supportsDataFileCommands(channel)) return new StepLayer(List.of(), null, fetchedAtNanos);
        Instant receivedAt = Instant.now();
        List<StepRecord> layers = codec.parseStepLayer(
                transport.exchange(codec.downloadStepLayerRequest(channel)), receivedAt);
        List<StepRecord> layers;
        try
        {
            layers = codec.parseStepLayer(transport.exchange(codec.downloadStepLayerRequest(channel)), receivedAt);
        }
        catch (BtsTransportException unanswered)
        {
            rememberUnanswered(unanswered);
            return new StepLayer(List.of(), null, fetchedAtNanos);
        }
        if (layers.isEmpty()) return new StepLayer(List.of(), null, fetchedAtNanos);
        // 通道仍在动(运行、暂停、保护中)时最后一步没结束:它和它所在的循环都先按住。
        if (!isStillRunning(channel)) return new StepLayer(layers, null, fetchedAtNanos);
@@ -343,7 +364,18 @@
    private BtsXmlCodec.DataFileState dataFileState(ChannelIdentity channel)
    {
        return codec.parseDataFileState(transport.exchange(codec.inquiredfRequest(channel)));
        if (!supportsDataFileCommands(channel))
            throw new BtsProtocolException(BtsProtocolException.Reason.MISSING_FIELD,
                    "该客户端不提供 inquiredf");
        try
        {
            return codec.parseDataFileState(transport.exchange(codec.inquiredfRequest(channel)));
        }
        catch (BtsTransportException unanswered)
        {
            rememberUnanswered(unanswered);
            throw unanswered;
        }
    }
    private List<CabinetRealtime> download(ChannelIdentity channel, long testId, long startPosition, int count)
@@ -352,6 +384,35 @@
                Instant.now());
    }
    /**
     * 这台客户端有没有数据文件命令({@code inquiredf}/{@code download}/{@code downloadStepLayer})。
     *
     * <p>为什么必须先问一句再发:**对不支持这些命令的客户端发出去,代价不只是超时**。2026-10-12 实测
     * (客户端 7.6.0.1475):发一条 {@code inquiredf} 之后,该接口的 {@code inquire} 连续 8 分钟无应答
     * ({@code getdevinfo} 仍正常)——那等于干扰了那台上位机的对外接口。所以不支持的客户端
     * **一条都不发**,逐点回落到轮询槽位、工步层为空。
     *
     * <p>探测依据是 {@code inquire} 应答的字段:7.6 的应答不带 {@code cycle_id},8.0 的带;
     * 实测三个构建(7.6.0.1475 / 8.0.0.484 / 8.0.0.530)里"有没有 {@code cycle_id}"与
     * "有没有这三条数据文件命令"同向。探测本身读不到应答时**抛出**(不吞):那说明来源不可达,
     * 按契约要抛异常,不能被当成"该来源不按数据行接收"。
     */
    private boolean supportsDataFileCommands(ChannelIdentity channel)
    {
        Boolean known = dataFileCommandsSupported;
        if (known != null) return known;
        RealtimeSnapshot snapshot = inquire(Set.of(channel)).get(channel);
        boolean supported = snapshot != null && snapshot.cycleNo() != null;
        dataFileCommandsSupported = supported;
        return supported;
    }
    /** 探测说支持、真发出去却不作答:记下来不再反复试——反复发一条无应答的命令会持续干扰那台接口。 */
    private void rememberUnanswered(BtsTransportException failure)
    {
        if (failure.reason() == BtsTransportException.Reason.TIMEOUT) dataFileCommandsSupported = false;
    }
    /** 行的序号比水位大就是要的行;序号反而更小说明换了测试,此时按设备时间判定。 */
    private static boolean isAfter(SampleWatermark watermark, CabinetRealtime row)
    {