# Generic Sensor API

> Accelerometer、Gyroscope、Magnetometer 等共用 Sensor 基类。构造选项、error 事件错误名、Chromium 采样频率上限与带回退的检测。

Generic Sensor API 是一个 `Sensor` 基接口（`start()`、`stop()`，以及 `reading`、`activate`、`error` 事件），由具体的设备传感器继承：`Accelerometer`、`LinearAccelerationSensor`、`GravitySensor`、`Gyroscope`、`AbsoluteOrientationSensor`、`RelativeOrientationSensor`、`Magnetometer` 和 `AmbientLightSensor`。页面构造某个具体类，调用 `start()`，然后在 `reading` 处理函数中从属性读取最新采样；读数只会投递给安全上下文中可见且获得焦点的文档。

支持情况是仅 Chromium 一家，内部还分两档。Chrome 67（Edge 79、Samsung Internet 9.0、Android WebView 67）无需标志即提供 `Accelerometer`、`LinearAccelerationSensor`、`Gyroscope`、`AbsoluteOrientationSensor` 与 `RelativeOrientationSensor`，Chrome 91 加入 `GravitySensor`。`Magnetometer` 与 `AmbientLightSensor` 自 Chrome 56 起存在，但只在 `chrome://flags/#enable-experimental-web-platform-features` 之后。Firefox 与 Safari 没有实现其中任何一个接口（BCD `api.Accelerometer`、`api.Magnetometer`、`api.AmbientLightSensor`）；跨内核获取运动数据的回退路径是 [DeviceMotionEvent](https://developer.mozilla.org/en-US/docs/Web/API/DeviceMotionEvent)。

## 语法

```js
new Accelerometer()
new Accelerometer(options)
new Gyroscope(options)
new AbsoluteOrientationSensor(options)
new Magnetometer(options)
new AmbientLightSensor(options)

sensor.start()
sensor.stop()
```

构造函数是同步的，返回一个处于 idle 状态的传感器。`start()` 与 `stop()` 返回 `undefined`；激活异步完成，以 `activate` 事件通知，之后 `activated` 为 `true`，第一份采样到达时 `hasReading` 变为 `true`，`timestamp` 携带该采样的 `DOMHighResTimeStamp`。运动类传感器暴露 `x`、`y`、`z`（加速度计为 m/s²，`Gyroscope` 为 rad/s，`Magnetometer` 为 µT）；方向类传感器暴露 `quaternion` 与 `populateMatrix(target)`；`AmbientLightSensor` 暴露以 lux 计的 `illuminance`。所有接口都带 `[SecureContext]`。

## 参数

每个构造函数接受一个可选的 `options` 字典。基类 `SensorOptions` 只有一个成员；运动与方向类传感器在其上扩展了 `referenceFrame`。

| 成员 | 类型 | 必填 | 适用于 | 说明 |
|---|---|---|---|---|
| `frequency` | `double`（Hz） | 否 | 所有传感器 | 期望的采样与上报频率。规范把它定为提示：浏览器可以把它钳制到平台传感器的范围内。Chromium 把加速度计、陀螺仪和方向传感器的上限定为 60 Hz（默认 10 Hz），`Magnetometer` 与 `AmbientLightSensor` 上限为 10 Hz（`sensor_traits.h`）。 |
| `referenceFrame` | `"device"` 或 `"screen"` | 否，默认 `"device"` | `Accelerometer`、`LinearAccelerationSensor`、`GravitySensor`、`Gyroscope`、`Magnetometer`、两种方向传感器 | 读数所用的坐标系。`"screen"` 让坐标轴随当前屏幕方向旋转，横放的平板仍把"上"报告为 y 轴正方向。 |

读数在抵达页面前已被量化：Accelerometer 规范把 `x`、`y`、`z` 四舍五入到最接近的 0.1 m/s²，这是一项反指纹措施，也决定了你能检测到的最小运动幅度。

## 异常

构造函数同步抛出；`start()` 从不抛出，而是通过 `error` 事件报告失败，`event.error` 即下表中的 `DOMException`。

| 异常 | 位置 | 条件 |
|---|---|---|
| `SecurityError` | 构造函数 | 文档不被允许使用该传感器的 policy-controlled feature（`accelerometer`、`gyroscope`、`magnetometer` 或 `ambient-light-sensor`；方向传感器需要 `accelerometer` 加 `gyroscope`，绝对方向传感器还需要 `magnetometer`）。跨源 iframe 需要在 `allow` 属性中列出它们。 |
| `NotSupportedError` | 构造函数 | `options` 含有该传感器类型不支持的键（「initialize a sensor object」步骤）。 |
| `NotAllowedError` | `error` 事件 | 传感器权限请求的结果为 `"denied"`。Chromium 把这些权限对应到 `navigator.permissions.query()` 中的 `accelerometer`、`gyroscope`、`magnetometer`、`ambient-light-sensor` 名称。 |
| `NotReadableError` | `error` 事件 | 「connect to sensor」失败：设备没有这种传感器、平台拒绝提供，或传感器在使用中停止。传感器回到 `"idle"` 并保持沉默，直到再次调用 `start()`。 |

`new Magnetometer()` 抛出的 `ReferenceError` 不是传感器错误，它表示当前浏览器没有定义这个构造函数，这是该做特性检测而不是 catch 的情形。

:::observed
Chrome 对超出范围的 `frequency` 做钳制而不是拒绝，并在 DevTools Console 以 Info 级别说明：`new Accelerometer({ frequency: 120 })` 打印 `Maximum allowed frequency value for this sensor type is 60 Hz.` 并以 60 Hz 运行。设备缺少对应硬件时，`error` 事件的 `event.error.message` 为 `Could not connect to a sensor`（名称 `NotReadableError`）；被平台拒绝的 `start()` 报告 `start() call has failed.`。三条字符串见 Chromium 的 [`sensor.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/sensor/sensor.cc) 与 [`sensor_proxy.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/sensor/sensor_proxy.cc)（chromium.googlesource.com）。
:::

## 示例

示例精确检测所需的构造函数，缺失时回退到 Firefox 与 Safari 都实现的 `DeviceMotionEvent`，两条路径都没有时退回静态 UI。

### 用 Accelerometer 检测摇一摇，并以 DeviceMotionEvent 回退

优先走加速度计路径，因为它按你选择的频率投递经过校准和量化的采样。构造函数缺失时，同一套阈值跑在 `devicemotion` 上，它报告的是未量化的 `accelerationIncludingGravity`。

```js
const SHAKE_THRESHOLD = 25; // m/s^2，合成幅值

function onShake(callback) {
  if ('Accelerometer' in window) {
    let sensor;
    try {
      sensor = new Accelerometer({ frequency: 30 });
    } catch (err) {
      console.warn(`${err.name}: ${err.message}`); // 被 Permissions Policy 拦下
      return false;
    }
    sensor.addEventListener('error', (event) => {
      console.warn(`${event.error.name}: ${event.error.message}`);
    });
    sensor.addEventListener('reading', () => {
      if (Math.hypot(sensor.x, sensor.y, sensor.z) > SHAKE_THRESHOLD) callback();
    });
    sensor.start();
    return true;
  }

  if ('DeviceMotionEvent' in window) {
    window.addEventListener('devicemotion', (event) => {
      const a = event.accelerationIncludingGravity;
      if (a && Math.hypot(a.x, a.y, a.z) > SHAKE_THRESHOLD) callback();
    });
    return true;
  }

  return false; // 没有运动数据：改为显示一个按钮
}

if (!onShake(() => document.body.classList.toggle('shaken'))) {
  document.querySelector('#shake-button').hidden = false;
}
```

iOS 13 及之后的 `devicemotion` 本身也受 `DeviceMotionEvent.requestPermission()` 门控，且必须在用户手势中调用；真实应用里这段回退分支应放在一次点击之后。

### 构造方向传感器前先查询权限

`AbsoluteOrientationSensor` 同时依赖三项权限。先查询它们，页面就能说明哪一项被拦，而不是等 `error` 事件里的 `NotAllowedError`。

```js
async function startCompass(onHeading) {
  if (!('AbsoluteOrientationSensor' in window)) return 'unsupported';

  const names = ['accelerometer', 'gyroscope', 'magnetometer'];
  const states = await Promise.all(
    names.map((name) => navigator.permissions.query({ name }).then((s) => s.state)),
  );
  if (states.includes('denied')) return 'denied';

  const sensor = new AbsoluteOrientationSensor({ frequency: 10, referenceFrame: 'screen' });
  sensor.addEventListener('reading', () => {
    const [qx, qy, qz, qw] = sensor.quaternion;
    const yaw = Math.atan2(2 * (qw * qz + qx * qy), 1 - 2 * (qy * qy + qz * qz));
    onHeading(((yaw * 180) / Math.PI + 360) % 360);
  });
  sensor.addEventListener('error', (event) => {
    if (event.error.name === 'NotReadableError') onHeading(null); // 此设备没有磁力计
  });
  sensor.start();
  return 'started';
}
```

返回字符串让调用方渲染三种状态：指南针、"请开启运动访问权限"的提示，或 API 不存在时正北朝上的普通地图。

## 另请参阅

- [Geolocation API](/zh/reference/capabilities/geolocation/)，另一项与位置相关、带 `PERMISSION_DENIED` 分支的权限
- [Idle Detection API](/zh/reference/capabilities/idle-detection/)，另一项由 Permissions API 门控的仅 Chromium 能力
- [Screen Wake Lock API](/zh/reference/capabilities/wake-lock/)，常与运动追踪界面搭配
- [Generic Sensor API: Sensor.start()](https://www.w3.org/TR/generic-sensor/#sensor-start)（w3.org）
- [Generic Sensor API: Mitigation strategies](https://www.w3.org/TR/generic-sensor/#mitigation-strategies)（w3.org）
- [Accelerometer: reading quantization algorithm](https://www.w3.org/TR/accelerometer/#accelerometer-reading-quantization-algorithm)（w3.org）
- [Sensors for the web](https://developer.chrome.com/docs/capabilities/web-apis/generic-sensor)（developer.chrome.com）