You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

实习BLE扫描应用时间戳严重偏移问题求助

BLE扫描应用时间戳年份偏移问题排查与解决

问题背景

实习开发BLE扫描应用,用于收集BLE协议相关数据,扫描结束后生成包含设备地址、RSSI、检测时间戳、位置(适配移动车辆场景)的CSV文件。但多部手机测试均出现时间戳年份严重偏移(如显示2002、2011年),无法定位问题。

扫描结果回调代码

public void onScanResult(int callbackType, ScanResult result) {
    super.onScanResult(callbackType, result);
    String deviceAddress = result.getDevice().getAddress();
    long timestamp = System.currentTimeMillis();
    if (mDeviceTimestamps.containsKey(deviceAddress)) {
        // 忽略3秒内重复检测的设备
        if (timestamp - mDeviceTimestamps.get(deviceAddress) < 3000) {
            return;
        }
    }
    mDeviceTimestamps.put(deviceAddress, timestamp);
    Log.d(TAG, "New Device: " + deviceAddress + ", " + result.getTimestampNanos()/1000);
    mScanResults.add(result);
}

CSV生成代码

private void generateCsvFile(List<ScanResult> scanResults) {
    if (scanResults.isEmpty()) {
        return;
    }

    // 创建CSV目录
    File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
    if (!dir.exists()) {
        if (!dir.mkdirs()) {
            Log.e(TAG, "generateCsvFile: failed to create directory");
            return;
        }
    }
    String filename = "scan_" + new Date().getTime() + ".csv";
    File file = new File(dir, filename);

    try {
        FileWriter fileWriter = new FileWriter(file, true);
        CSVWriter csvWriter = new CSVWriter(fileWriter);

        // 写入表头
        String[] headerRow = {"Device Name", "Device Address", "RSSI", "Timestamp", "Latitude", "Longitude"};
        csvWriter.writeNext(headerRow);
        String[] headerRow2 = {"Number of devices"};
        csvWriter.writeNext(headerRow2);

        // 统计唯一设备数
        Set<String> uniqueDeviceAddresses = new HashSet<>();
        for (ScanResult result : scanResults) {
            uniqueDeviceAddresses.add(result.getDevice().getAddress());
        }
        String[] numUniqueDevicesRow = {"Number of Unique Devices:", String.valueOf(uniqueDeviceAddresses.size()), "", "", "", ""};
        csvWriter.writeNext(numUniqueDevicesRow);

        // 遍历扫描结果写入CSV
        for (ScanResult result : scanResults) {
            if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.BLUETOOTH_CONNECT}, REQUEST_BLUETOOTH_CONNECT_PERMISSION);
                return;
            }
            String deviceName = result.getDevice().getName();
            if (deviceName == null) {
                deviceName = "n/a";
            }
            String deviceAddress = result.getDevice().getAddress();
            int rssi = result.getRssi();
            long timestampMillis = result.getTimestampNanos() / 1000000; // 纳秒转毫秒
            String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date(timestampMillis));

            // 匹配扫描时间对应的位置
            Location loc = null;
            for (int i = mlocationUpdates.size() - 1; i >= 0; i--) {
                Location update = mlocationUpdates.get(i);
                if (update.getTime() <= timestampMillis) {
                    loc = update;
                    break;
                }
            }

            double lat = (loc != null) ? loc.getLatitude() : 0.0;
            double lon = (loc != null) ? loc.getLongitude() : 0.0;

            String[] row = {deviceName, deviceAddress, String.valueOf(rssi), timestamp, String.valueOf(lat), String.valueOf(lon)};
            csvWriter.writeNext(row);
        }

        csvWriter.close();
        fileWriter.close();
        Toast.makeText(this, "CSV file generated successfully!", Toast.LENGTH_SHORT).show();
    } catch (IOException e) {
        Log.e(TAG, "Error writing CSV file", e);
        Toast.makeText(this, "Error writing CSV file", Toast.LENGTH_SHORT).show();
        e.printStackTrace();
    }
}

问题根源

CSV生成代码中使用的result.getTimestampNanos(),返回的不是从1970年开始的标准Unix时间戳,而是设备启动以来的纳秒运行时长(类似SystemClock.elapsedRealtimeNanos())。直接将该值转成毫秒后传给Date类,会被解析成1970年加上这个运行时长,若设备运行时间较短(如刚重启),就会出现年份偏移的情况。

而你在onScanResult中已经正确获取了标准Unix时间戳System.currentTimeMillis(),但仅用于去重判断,未将其与ScanResult绑定存储,导致生成CSV时错误使用了非标准时间戳。

修复方案

方案1:存储扫描时的标准时间戳(推荐)

创建自定义数据类,将ScanResult和对应的标准时间戳绑定存储:

  1. 定义扫描数据类:
public class ScanData {
    private ScanResult scanResult;
    private long timestampMillis;

    public ScanData(ScanResult scanResult, long timestampMillis) {
        this.scanResult = scanResult;
        this.timestampMillis = timestampMillis;
    }

    // Getter方法
    public ScanResult getScanResult() { return scanResult; }
    public long getTimestampMillis() { return timestampMillis; }
}
  1. 修改扫描回调的存储逻辑:
// 将原List<ScanResult>替换为List<ScanData>
private List<ScanData> mScanResults = new ArrayList<>();

public void onScanResult(int callbackType, ScanResult result) {
    super.onScanResult(callbackType, result);
    String deviceAddress = result.getDevice().getAddress();
    long timestamp = System.currentTimeMillis();
    if (mDeviceTimestamps.containsKey(deviceAddress)) {
        if (timestamp - mDeviceTimestamps.get(deviceAddress) < 3000) {
            return;
        }
    }
    mDeviceTimestamps.put(deviceAddress, timestamp);
    Log.d(TAG, "New Device: " + deviceAddress + ", " + timestamp);
    mScanResults.add(new ScanData(result, timestamp)); // 存储绑定后的扫描数据
}
  1. 修改CSV生成方法:
private void generateCsvFile(List<ScanData> scanDataList) {
    if (scanDataList.isEmpty()) {
        return;
    }

    // 目录创建、文件初始化等代码不变

    try {
        // 表头写入、唯一设备数统计等代码不变

        // 遍历绑定后的扫描数据
        for (ScanData data : scanDataList) {
            ScanResult result = data.getScanResult();
            long timestampMillis = data.getTimestampMillis(); // 使用正确的标准时间戳
            
            // 设备名称、地址、RSSI获取代码不变
            
            String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date(timestampMillis));
            
            // 位置匹配代码不变
            
            String[] row = {deviceName, deviceAddress, String.valueOf(rssi), timestamp, String.valueOf(lat), String.valueOf(lon)};
            csvWriter.writeNext(row);
        }

        // 资源关闭、提示代码不变
    } catch (IOException e) {
        // 异常处理代码不变
    }
}

方案2:计算标准时间戳(不推荐,精度易受影响)

若不想修改存储结构,可通过系统启动时间推导标准时间戳:

// 在CSV生成时替换原timestampMillis计算逻辑
long elapsedRealtimeMillis = result.getTimestampNanos() / 1000000;
long bootTimeMillis = System.currentTimeMillis() - SystemClock.elapsedRealtime();
long timestampMillis = bootTimeMillis + elapsedRealtimeMillis;
String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date(timestampMillis));

此方法依赖系统时间与运行时长的差值,若系统时间在运行中被修改,会导致时间戳不准确,仅作为临时替代方案。


内容的提问来源于stack exchange,提问作者habascus

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 09:07:12