This describes a generic framework for discovering and using sensors and devices. It is composed primarily of two parts: a new SensorBackend
class for providing sensor values, and a SensorRegistry
class for registering backends. It also proposes several related interfaces: SensorReading
, SensorBackend
, SensorBackendConstructor
, and extensions to the Generic Sensor API to support backends.
This is a follow-up to my previous Sensor Discovery API proposal which should no longer be actively considered.
Naming things is considered one of the hardest problems in computer science, and this preliminary spec was no exception. I had to decide on a name for the API, whether or not to use Sensor to mean any device and not just a sensor, whether to create a separate Device interface and use a term that unifies Sensor and Device, and if so, what its name should be.
I eventually decided to use Sensor
as this document describes extensions and additions to the existing Generic Sensor API. I forsee names being changed or new interfaces being designed (such as Device
to encapsulate both sensor devices and actuator devices).
Next, I had to decide on a term for the interface between a Sensor and a Sensor Discovery API. Some potential candidates were: SensorDriver
, SensorSource
, SensorManager
, and SensorController
. After my previous proposal, this redesigned spec is designed to be less like low-level drivers and something more high-level and secure. I eventually choose the name SensorBackend
as the primary interface name.
SensorReading
provides the interface between a SensorBackend
and Sensor
. Simply put, a SensorReading
is the data provided by each Sensor instance. As such, it will be unique for every sensor. SensorReading
provides data to populate the members of the Sensor class the Backend provides.
There is only one property shared by all SensorReading
interfaces: timestamp
, which provides the time of the most recent sensor reading.
Some example SensorReading
definitions for current Sensor classes:
interface SensorReading {
timestamp: DOMHighResTimeStamp;
}
interface AccelerometerReading extends SensorReading {
x: number;
y: number;
z: number;
}
interface GyroscopeReading extends SensorReading {
x: number;
y: number;
z: number;
}
interface OrientationReading extends SensorReading {
quaternion: [number, number, number, number];
}
interface AmbientLightReading extends SensorReading {
illuminance: number;
}
A SensorReading
will be provided to a Sensor
instance using the setReading
method of the SensorBackend
class. If the reading value does not match the format expected by the sensor, an error should be thrown.
A SensorBackend
class provides data to a Sensor
using its setReading
class. When it does so, the Sensor
class will update its properties (x
, y
, z
, etc) and emit the "reading"
event.
This interface has two primary methods: setReading
and setError
. setReading
indicates to the Sensor
that a reading is available and should be handled accordingly. setError
indicates to the Sensor
that an error has occured and should be handled accordingly.
There is only one event: "activate"
, which tells the backend the sensor has started or stopped. The Sensor
class sends it to the SensorBackend
whenever the sensor has been activated or is no longer activated. There should also be a corresponding optional onactivate
member.
declare class SensorBackend<T extends SensorReading = SensorReading> extends EventTarget {
onactivate: () => void;
setReading(reading: T): void;
setError(error: SensorErrorEvent): void;
}
For this to work, the Generic Sensor API should be extended so that it and every interface that extends it should support an optional backend
parameter in the constructor parameter. It should also provide a setBackend
method to allow changing the backend that will provide SensorReading
values. This will allow existing global sensor classes such as Accelerometer
to continue functioning while also being compatible with the new Sensor Discovery features.
If setBackend
is used after a sensor has already been activated, it should throw an error. If backendName
has not been registered with SensorRegistry
when a backend is set, an error should be thrown. The backend
property is readonly and may only be changed by the Sensor.
There should be a new SensorType
for the type
property of the sensor class. type
is readonly and should only be set by the Sensor
class. It is used to differentiate different classes of sensors when constructing a backend.
type SensorType = "Accelerometer" | "Gyroscope" | "Battery" | DOMString;
interface SensorOptions {
frequency?: number | undefined;
backend?: DOMString | undefined;
}
declare class Sensor extends EventTarget {
readonly type: SensorType;
readonly backend: string;
constructor(options?: SensorOptions);
setBackend(backendName: string): void;
}
A SensorRegistry
handles registering SensorBackend
s and providing them to sensors.
There is only method in SensorRegistry
: register
, which registers an interface for constructing a backend. It also has only one readonly property: backend
, which contains a list of every backendName
which has been registered.
register
accepts a SensorBackendConstructor
interface which takes care of providing SensorBackend
instances on-demand when requested by the Sensor
. Its primary method is createBackend
, which returns a SensorBackend
for the SensorType
requested and throws an error if a backend cannot be constructed. There is also a backendTypes
method which returns the types
property of a SensorBackendConstructor
interface. When there is no backend matching the name given, it should return an empty array.
In addition, SensorBackendConstructor
also provides a readonly backends
array listing every backendName
that has been registered thus far. Applications may call backendTypes
for every value listed in this array.
Similar to CustomElements, there should be a global sensorRegistry
instance of SensorRegistry
.
interface SensorBackendConstructor {
readonly types: SensorType[];
createBackend(type: SensorType): Promise<SensorBackend>;
}
interface SensorRegistry {
readonly backends: string[];
backendTypes(backendName): string[];
register(backendName: string, backendConstructor: SensorBackendConstructor);
}
Here is an example of defining a custom Accelerometer backend and registering it.
class DummyAccelerometerBackend extends SensorBackend {
constructor(interval) {
this.interval = interval;
}
onactivate() {
this.timerId = setInterval(() => {
const reading = {
x: 0,
y: 9.5,
z: 0,
timestamp: performance.now()
};
this.setReading(reading);
}, this.interval);
}
}
class DummyAccelerometer extends SensorBackendConstructor {
constructor() {}
readonly types: string[] = ["Accelerometer"];
async createBackend(type: SensorType) {
if (sensorType === "Accelerometer") {
return new DummyAccelerometerBackend(100);
}
throw new Error(`Cannot create a backend for sensor type "${type}"`);
}
}
sensorRegistry.register("dummy", new DummyAccelerometer);
const accelerometer = new Accelerometer({ backend: "dummy" });
// OR
const accelerometer = new Accelerometer();
accelerometer.setBackend("dummy");
I admit I'm not a fan of the name SensorBackendConstructor
. I'd prefer something besides Constructor
.
The current proposal has SensorType
being passed as the only parameter to createBackend
, but it might be better to just pass the whole Sensor
instance.
Should SensorBackendConstructor
be an interface or an actual ctor passed to register()
?
Much of the API for this revised proposal comes from the excellent Qt Sensors module. It provided the inspiration for a much safer and more user-friendly Web API than the last proposal.