更新记录

1.0.0(2026-07-19)

首发。实时监控流图 / 示波器——每秒 10 万样本级实时滚动。

  • 双模式摄取sampled(定频,时间由样本计数推进)/ timed(自带毫秒时间戳);LE f32 二进制批推(pushSamples),坏字节/NaN/Inf/时间回退整批原子拒收
  • 示波器渲染:每像素列 min/max 包络带 + 中线;增量列缓存(绝对时间锚定)让滚动帧摆脱全量重扫;断流缺口诚实留白(相邻间隔 >4× 中位判断流)
  • 阈值告警:高/低双向虚线 + 越线段变色 + 边沿触发事件队列(1024 环形,drainAlarms 一次性取空)
  • 暂停 / 回看 / 缩放pause(冻结钟外供)/ resume / setReviewOffset;原生视图暂停态单指平移回看 + 双指捏合缩放时间窗
  • 快照 + 统计snapshot 出 PNG data-URI;getStats(count/min/max/avg/last,窗内无样本诚实返 null);getValuesAt 十字线近邻值;getMeta 元信息
  • <uni-nex-stream-view> 原生视图(App):时钟自驱滚动(running/paused prop 控制,不满足即停排帧省电)+ 手势热环路零桥接开销;

平台兼容性

uni-app x(5.14)

Chrome Safari Android iOS 鸿蒙 微信小程序
5.0 12 × ×

nex-stream 实时监控流图 / 示波器

示波器搬进 uni-app——每秒 10 万样本实时滚动。JS 图表库 setData 一秒刷几万点直接卡死的场景,本插件自研 环形缓冲 + 包络降采样原生渲染:现实配置(2 系列 × 1000Hz × 10s 窗)单帧 7.45ms,60fps 预算余量 2.2×(实测)。越线告警 + 暂停回看 + 快照留证——工业巡检 / 设备监控闭环三件套;断流诚实可见:时钟驱动滚动,数据停了图不撒谎(留白,不横线冒充)。

特性

  • 10 万样本/秒摄取:8 系列 × 合计 600 万样本环形缓冲;pushSamples 二进制批推(LE f32 字节,避 JSON 序列化开销),单批 1 万样本实测 0.312ms
  • 双摄取模式sampled(定频,如 ADC/音频,时间由样本计数自动推进)/ timed(自带毫秒时间戳,如传感器上报,断流缺口如实留白)
  • 示波器画法:每像素列 min/max 包络带 + 中线,百万级样本一屏尽收不糊成一坨;增量列缓存让滚动帧摆脱全量重扫(较无缓存 -71%
  • 阈值告警:高/低双向告警线虚线绘制 + 越线段变色 + 边沿触发事件(进入越线报一次,持续越线不刷屏);队列缓存 1024 条,组件每帧自动转发 @alarm
  • 暂停 / 回看 / 缩放pause() 冻结画面,原生视图单指平移回看历史、双指捏合缩放时间窗,resume() 跳回实时
  • 快照留证snapshot() 一次调用出 PNG data-URI,直接喂 <内置图像编解码 :src> 或上传存档
  • 实时统计角标:右上角每系列 min/max/avg 随帧刷新(getStats 亦可单独取,缓存命中路径 0.04ms
  • <uni-nex-stream-view> 原生视图(App):时钟自驱滚动 + 手势热环路全程零桥接开销(Android View / iOS UIView 直贴位图)
  • 三端同源:App(Android/iOS 原生绑定层) + H5 同一自研原生内核,15 个 API 三端全可用(无 App 专用阉割);H5 组件 <uni-nex-stream-canvas> rAF 帧循环
  • 自研 依赖链:无任何 C 依赖

快速上手

1. 建引擎 + 批量推流(三端同一套 API)

import {
  createStreamEngine, pushSamples, destroyStreamEngine,
  pause, resume, snapshot, getStats, streamVersion
} from '@/uni_modules/nex-stream'

// 建引擎:2 系列(CH1 定频 1000Hz 带告警线;CH2 自带时间戳)
const engineId = createStreamEngine(JSON.stringify({
  theme: 'dark',
  windowSec: 10,
  series: [
    { name: 'CH1', mode: 'sampled', sampleRate: 1000, thresholds: { high: 80, low: -10 } },
    { name: 'CH2', mode: 'timed', color: '#ee6666' }
  ]
}))

// ⚠️ 时间锚:建引擎时记一个 t0,此后一切时间入参都传「Date.now() − t0」(相对毫秒)。
// 引擎数据时间基是相对毫秒(sampled t 从 0 起步),直接传 Date.now() 壁钟原值 = 窗口与数据
// 永不相交 = 空图 / getStats count=0。这是本插件头号使用陷阱,务必照做(详见契约「时钟语义 ⚠️」)。
const t0 = Date.now()

// 推流姿势(推荐 ≥50ms 批推——见下方「推流最佳实践」)
const timer = setInterval(() => {
  const n = 50                       // 1000Hz × 50ms = 50 样本/批
  const buf = new ArrayBuffer(n * 4) // sampled 系列:每样本 1 个 f32(纯 v)
  const dv = new DataView(buf)
  for (let i = 0; i < n; i++) {
    dv.setFloat32(i * 4, readSensor(), true)  // true = little-endian,必须
  }
  pushSamples(engineId, 0, new Uint8Array(buf))
}, 50)

// 暂停回看 / 快照 / 统计——nowMs 一律传相对锚时间(Date.now() − t0),与数据时间基同基
pause(engineId, Date.now() - t0)          // 冻结画面(nowMs 外供——本插件 原生层不取系统时钟)
const png = snapshot(engineId, 750, 400, 2, Date.now() - t0)  // data:内置图像编解码/png;base64,...
const stats = JSON.parse(getStats(engineId, 0, Date.now() - t0)) // {count,min,max,avg,last}
resume(engineId)

// 页面卸载:先停定时器,再销毁引擎(JS 侧持有所有权,用完必调)
clearInterval(timer)
destroyStreamEngine(engineId)

timed 系列的字节是 [t, v] 对(每样本 2 个 f32,t=毫秒、须单调不减):dv.setFloat32(i*8, t, true); dv.setFloat32(i*8+4, v, true)⚠️ timed 的 t 也用相对锚时间(Date.now() − t0),绝不可传 Date.now() 原值——t 过桥是 f32,epoch 量级下 ulp≈131 秒,样本会全部坍缩(详见契约「时钟语义」timed 条款)。

2. App 端:<uni-nex-stream-view> 原生视图(时钟自驱 + 手势)

<template>
  <uni-nex-stream-view
    :engine-id="engineId" :running="running" :paused="paused" :gesture="true"
    :clock-epoch-ms="t0"
    style="width: 750rpx; height: 400rpx;"
    @alarm="onAlarm" @crosshair="onCrosshair" @viewportchange="onViewport" />
  <button @click="togglePause">{{ paused ? '恢复' : '暂停回看' }}</button>
</template>

<script>
  import { pause, resume } from '@/uni_modules/nex-stream'

  export default {
    // ⚠️ t0 = 建引擎时记的 Date.now()(时间锚);:clock-epoch-ms 把它交给组件,组件供时 = 壁钟 − t0,
    //    与数据时间基(相对毫秒)同基。不传锚 = 组件用壁钟 epoch 原值 = 空图(见「快速上手 §1」注释)。
    data() { return { engineId: 0, t0: -1, running: true, paused: false } },
    onHide() { this.running = false },   // 后台页必停自驱(省电,见 FAQ)
    onShow() { this.running = true },
    methods: {
      togglePause() {
        // pause 的 nowMs 同样传相对锚时间(Date.now() − t0),与组件 clockEpochMs 同锚
        if (this.paused) { resume(this.engineId) } else { pause(this.engineId, Date.now() - this.t0) }
        this.paused = !this.paused       // ⚠️ pause/resume 后必须同步翻转 paused prop
      },
      onAlarm(json: string) {
        // [{"seriesIndex":0,"kind":"high","t":1234.5,"v":85.2}, ...]——组件每帧自动转发
        // ⚠️ 挂了本组件就别再自调 drainAlarms()(队列一次性取空,两个消费者互抢)
      },
      onCrosshair(json: string) { /* running 态单击:[{"seriesIndex","name","t","v"}...] */ },
      onViewport(json: string) { /* 暂停态拖动/捏合结束:getMeta 元信息(含 windowSec/reviewOffsetMs)*/ }
    }
  }
</script>

手势(内置,gesture prop 可关):暂停态单指平移=回看历史、双指捏合=缩放时间窗;运行态单击=十字线取值(@crosshair)。运行态不支持平移/捏合(实时流不回看——先 pause())。

3. H5 端:<uni-nex-stream-canvas> 组件

<!-- H5 用 canvas 组件(rAF 帧循环);props/事件语义与原生视图对齐(无手势,简版) -->
<!-- :clock-epoch-ms 同原生视图(t0=建引擎时的 Date.now()),不传则组件 attach 时刻自记锚 -->
<uni-nex-stream-canvas :engine-id="engineId" :width-px="375" :height-px="200"
  :running="running" :paused="paused" :clock-epoch-ms="t0" @alarm="onAlarm" />
// H5 端 H5 端引擎 异步初始化:建引擎前等一次
// #ifdef H5
import { ensureReady } from '@/uni_modules/nex-stream'
await ensureReady()
// #endif

App 端请直接用 <uni-nex-stream-view> 原生视图(本 canvas 组件的 App 分支为占位,v1 不渲染)。H5 端回看/缩放走 API:pause / setReviewOffset / setWindow

推流最佳实践(≥50ms 批推)

别逐样本推——每次 pushSamples 都要过一次 JS↔原生桥,逐样本推 1000Hz = 每秒 1000 次过桥,桥开销远超数据本身。推荐姿势:

  • UTS/JS 定时器 ≥50ms 一批:1000Hz 传感器 = 每批 50 样本;10 万样本/秒满配 = 每批 5000 样本(实测该姿势单批 push 耗时 0.3ms 量级,10 万/s 稳定摄取)。
  • bytes 单拷贝ArrayBuffer + DataView.setFloat32(offset, v, true) 填好一批再 new Uint8Array(buf) 一次推入;不要逐样本 new。
  • 整批原子性:坏字节 / NaN / Inf / timed 时间回退会整批拒收(引擎状态一字不改)——收数据时先把无效值过滤掉再入批。
  • sampled 模式时间由推流驱动:时间戳按累计样本数推进(t = 累计数 × 1000 / sampleRate),推流跟不上实时曲线就会滞后于窗口右缘。跟不上时降 sampleRate 或改 timed 模式自带时间戳。

option DSL 速览

{
  "theme": "dark",            // light | dark(缺省 light)
  "windowSec": 30,            // 必填,滚动窗口秒数 (0, 300]
  "series": [                 // 必填,1~8 个
    {
      "name": "CH1",          // 必填,角标/十字线标注
      "mode": "sampled",      // 必填:sampled(定频)| timed(自带时间戳)
      "sampleRate": 10000,    // sampled 必填(Hz)
      "thresholds": { "high": 80.0, "low": -10.0 },  // 可选,任一方向可缺;high 必须 > low
      "color": "#5470c6"      // 可选,缺省按主题调色板
    }
  ],
  "yRange": null,             // [min,max] 固定量程;null=自动(窗口 min/max ±5%)
  "showStats": true           // 统计角标开关(缺省 true)
}

返回 JSON 形状(字段与 原生实现逐字一致,详见 docs/stream-plugin-contract.md):

// getStats —— 窗内无样本时数值字段为 null、count=0
{ "count": 5, "min": 10.0, "max": 90.0, "avg": 55.0, "last": 70.0 }
// drainAlarms / @alarm —— kind 小写;边沿触发(越线瞬间一条,持续越线不重复)
[{ "seriesIndex": 0, "kind": "high", "t": 1234.5, "v": 85.2 }]
// getValuesAt / @crosshair —— 无值可取的系列不出现在数组里
[{ "seriesIndex": 0, "name": "CH1", "t": 2400.0, "v": 29.0 }]
// getMeta / @viewportchange
{ "paused": false, "windowSec": 30.0, "seriesCount": 2, "sampleCounts": [36000, 120],
  "renderMs": 1.85, "reviewOffsetMs": 0.0 }

限制与防护(内核硬校验,超限报错不静默截断)

  • series ≤ 8 个;窗口 (0, 300] 秒;总样本护栏 600 万(环形按系列均分,超配额自动淘汰最旧——sampled 系列配额 = min(窗秒×采样率×1.2, 600万÷系列数)
  • 告警队列 1024 条环形覆盖(取走前最多缓存这么多)
  • 渲染尺寸:w/h 必须有限正数;物理像素(w×dpr)≤ 4096、dpr ∈ [1,4]
  • 推流字节:长度须对齐(sampled 4 字节/样本、timed 8 字节/样本)、NaN/Inf 拒收、timed 时间不可回退——违反整批拒收
  • 网络接入 / 数据持久化(快照 PNG 之外)/ FFT 频谱 / y 轴手势缩放:v1 不承诺(数据谁来推是使用方的事——WebSocket/BLE/传感器收到后 pushSamples

常见问题

  • 整张图都是空的 / getStatscount=0 头号陷阱是时间锚没配对。引擎数据时间基是相对毫秒(sampled t 从 0 起步),你若把 Date.now() 壁钟原值(≈1.78×10¹²)传给 nowMs,窗口 [epoch−win, epoch] 与数据 [0, elapsed] 永不相交 → 空图。三个具体坑:① 忘记减锚——建引擎时记 const t0 = Date.now(),所有 nowMs/tMs 入参传 Date.now() − t0,组件传 :clock-epoch-ms="t0";② timed 传了 Date.now() 原值——timed 的 t 过桥是 f32,epoch 量级 ulp≈131 秒、样本全坍缩,必须传相对锚 ms;③ 组件晚挂载用 auto 锚——组件缺省 clockEpochMs=-1 时在 attach 时刻自记锚,若引擎已推流一段时间才挂组件、且页面又直调 snapshot(nowMs),两个锚会错位,此时显式传 :clock-epoch-ms="t0" 统一。(详见契约「时钟语义 ⚠️ 时间锚配对」。)
  • 图中间怎么有一段空白? 这是断流缺口的诚实渲染timed 系列相邻样本间隔超过窗口中位间隔 4 倍即判断流、留白不连线(数据没了就是没了,不画横线冒充)。恢复推流后缺口保留在历史里,随窗口滚出。
  • 曲线越来越落后于右边缘? sampled 模式时间由累计样本数推进而非壁钟——推流慢于 sampleRate 声明值曲线就滞后。要么保证按率推满,要么改 timed 模式自带真实时间戳。
  • 会不会很耗电? 运行态原生视图 60fps 时钟自驱常驻重绘,确实吃电——后台页/不可见时必须停onHide 里把 running 置 false(或 pause()),onShow 恢复。不满足 running && !paused 时组件立即停排帧,零开销。v1 无降帧率选项(v1.1 候选)。
  • @alarm 没收到 / 少了? ① 告警是边沿触发:越线瞬间报一条,持续越线不重复报(不是每个越线样本一条)。② 队列一次性取空:挂了组件(<uni-nex-stream-view>/<uni-nex-stream-canvas>)就别再自调 drainAlarms()——组件每帧自动 drain 转发,你手动 drain 会把告警从组件嘴里抢走(反之亦然),两边各拿到一部分。裸用 API(不挂组件)时才手动 drain。③ 暂停期间不出帧,告警在队列里攒着(容量 1024),恢复后第一帧一次性转发。
  • 暂停了怎么画面还在动 / 手势不对? JS 侧调 pause()/resume()必须同步翻转组件的 paused prop——组件靠这个 prop 本地判定自驱与手势模式(不逐帧查询引擎),忘翻转就会状态错位。
  • JS 直调 setWindow 后手势缩放比例不对? 组件缓存窗口值用于手势换算,下次手势按下时才拉取最新真值——第一段拖动可能用旧值(自愈,非持久错误)。建议改窗走双指捏合手势,或改完让用户重新触摸。
  • 暂停态点一下怎么不出十字线? v1 已知边界:暂停态视图无法把触点换算成时间(引擎未暴露冻结时刻),单击不发 @crosshair;运行态单击正常。v1.1 候选修复。
  • Android 标准基座报"找不到 jna":原生插件须自定义基座(依赖 原生运行依赖,标准基座不含)。制作自定义基座后正常。
  • H5 端 getMeta().renderMs 恒为 0? H5 端拿不到高精度计时(技术限制),H5 侧用 Date.now() 前后各测一次即可。
  • sampled 和 timed 怎么选? 数据源定频且不带时间戳(ADC/音频/固定频率轮询)→ sampled(省一半带宽,每样本 4 字节);数据源自带时间戳或频率不稳(网络上报/BLE 通知/事件型)→ timed(断流缺口如实可见)。

平台说明

实现 说明
Android 原生扩展库(arm64-v8a / armeabi-v7a / x86_64)+ 原生绑定层 Kotlin 绑定 + 原生 View 视图 需自定义基座(云打包);时钟自驱+手势热环路零桥接开销
iOS 原生扩展库(device + simulator)+ 原生绑定层 Swift 绑定 + 原生 UIView 视图 宿主工程需开启"支持 Swift";绑定层已验,视图运行时适配下版本跟进
H5 同一自研原生内核编译 H5 端引擎(base64 内嵌,免静态资源配置)+ <uni-nex-stream-canvas> canvas 帧循环 首次调用前 await ensureReady();15 个 API 与 App 端完全一致

技术支持 / 问题反馈:插件评论区或仓库 issue。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件不采集任何数据。所有计算/处理均在本地完成,无任何网络请求、不发送数据到任何服务器。

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。