node-web-gpio/index.ts

261 lines
6.3 KiB
TypeScript
Raw Normal View History

2019-10-13 02:13:19 +09:00
import { EventEmitter } from "events";
import { promises as fs } from "fs";
2019-10-15 23:26:29 +09:00
import * as path from "path";
2019-10-13 02:13:19 +09:00
/**
* Interval of file system polling, in milliseconds.
*/
const PollingInterval = 100;
2019-10-15 23:26:29 +09:00
const SysfsGPIOPath = "/sys/class/gpio";
2019-10-18 01:29:58 +09:00
const GPIOPortMapSizeMax = 1024;
2019-10-13 02:13:19 +09:00
const Uint16Max = 65535;
function parseUint16(string: string) {
const n = Number.parseInt(string, 10);
if (0 <= n && n <= Uint16Max) return n;
else throw new RangeError(`Must be between 0 and ${Uint16Max}.`);
}
type PortNumber = number;
type PortName = string;
type PinName = string;
type DirectionMode = "in" | "out";
type GPIOValue = 0 | 1;
interface GPIOChangeEvent {
readonly value: GPIOValue;
readonly port: GPIOPort;
}
interface GPIOChangeEventHandler {
(event: GPIOChangeEvent): void;
}
export class GPIOAccess extends EventEmitter {
private readonly _ports: GPIOPortMap;
onchange: GPIOChangeEventHandler | undefined;
constructor(ports?: GPIOPortMap) {
super();
this._ports = ports == null ? new GPIOPortMap() : ports;
this._ports.forEach(port =>
port.on("change", event => {
2019-10-13 02:13:19 +09:00
this.emit("change", event);
})
);
this.on("change", (event: GPIOChangeEvent): void => {
if (this.onchange !== undefined) this.onchange(event);
});
}
get ports(): GPIOPortMap {
return this._ports;
}
/**
* Unexport all exported GPIO ports.
*/
async unexportAll(): Promise<void> {
2019-10-13 02:13:19 +09:00
await Promise.all(
[...this.ports.values()].map(port =>
port.exported ? port.unexport() : undefined
)
);
}
}
2019-10-16 00:27:50 +09:00
/**
* Different from Web GPIO API specification.
*/
2019-10-13 02:13:19 +09:00
export class GPIOPortMap extends Map<PortNumber, GPIOPort> {}
export class GPIOPort extends EventEmitter {
private readonly _portNumber: PortNumber;
private readonly _pollingInterval: number;
private _direction: DirectionMode | OperationError;
private _exported: boolean | OperationError;
private _exportRetry: number;
2019-10-13 02:13:19 +09:00
private _value: GPIOValue | undefined;
private _timeout: ReturnType<typeof setInterval> | undefined;
onchange: GPIOChangeEventHandler | undefined;
2019-10-13 02:13:19 +09:00
constructor(portNumber: PortNumber) {
super();
2019-10-15 20:56:02 +09:00
this._portNumber = parseUint16(portNumber.toString());
2019-10-13 02:13:19 +09:00
this._pollingInterval = PollingInterval;
this._direction = new OperationError("Unknown direction.");
this._exported = new OperationError("Unknown export.");
this._exportRetry = 0;
2019-10-13 02:13:19 +09:00
this.on("change", (event: GPIOChangeEvent): void => {
if (this.onchange !== undefined) this.onchange(event);
2019-10-13 02:13:19 +09:00
});
}
get portNumber(): PortNumber {
return this._portNumber;
}
get portName(): PortName {
2019-10-18 01:49:23 +09:00
return `gpio${this.portNumber}`;
2019-10-13 02:13:19 +09:00
}
get pinName(): PinName {
// NOTE: Unknown pinName.
return "";
}
get direction(): DirectionMode {
if (this._direction instanceof OperationError) throw this._direction;
return this._direction;
}
get exported(): boolean {
if (this._exported instanceof OperationError) throw this._exported;
return this._exported;
}
async export(direction: DirectionMode): Promise<void> {
2019-10-13 02:13:19 +09:00
if (!/^(in|out)$/.test(direction)) {
throw new InvalidAccessError(`Must be "in" or "out".`);
}
2019-10-15 23:26:29 +09:00
try {
2019-10-18 01:49:23 +09:00
await fs.access(path.join(SysfsGPIOPath, this.portName));
2019-10-15 23:26:29 +09:00
this._exported = true;
} catch {
this._exported = false;
}
2019-10-13 02:13:19 +09:00
try {
clearInterval(this._timeout as ReturnType<typeof setInterval>);
2019-10-15 23:26:29 +09:00
if (!this.exported) {
await fs.writeFile(
path.join(SysfsGPIOPath, "export"),
String(this.portNumber)
);
}
2019-10-13 02:13:19 +09:00
await fs.writeFile(
2019-10-18 01:49:23 +09:00
path.join(SysfsGPIOPath, this.portName, "direction"),
2019-10-13 02:13:19 +09:00
direction
);
if (direction === "in") {
this._timeout = setInterval(
// eslint-disable-next-line
2019-10-13 02:13:19 +09:00
this.read.bind(this),
this._pollingInterval
);
}
} catch (error) {
if ( this._exportRetry == 0 ){
await sleep(100);
console.warn("May be the first time port access. Retry..");
++ this._exportRetry;
await this.export(direction);
} else {
throw new OperationError(error);
}
2019-10-13 02:13:19 +09:00
}
this._direction = direction;
this._exported = true;
}
async unexport(): Promise<void> {
clearInterval(this._timeout as ReturnType<typeof setInterval>);
2019-10-13 02:13:19 +09:00
try {
2019-10-15 23:26:29 +09:00
await fs.writeFile(
path.join(SysfsGPIOPath, "unexport"),
String(this.portNumber)
);
2019-10-13 02:13:19 +09:00
} catch (error) {
throw new OperationError(error);
}
this._exported = false;
}
async read(): Promise<GPIOValue> {
2019-10-15 23:26:29 +09:00
if (!(this.exported && this.direction === "in")) {
throw new InvalidAccessError(
`The exported must be true and value of direction must be "in".`
);
}
2019-10-13 02:13:19 +09:00
try {
const buffer = await fs.readFile(
2019-10-18 01:49:23 +09:00
path.join(SysfsGPIOPath, this.portName, "value")
2019-10-13 02:13:19 +09:00
);
const value = parseUint16(buffer.toString()) as GPIOValue;
if (this._value !== value) {
this._value = value;
this.emit("change", { value, port: this });
2019-10-13 02:13:19 +09:00
}
return value;
} catch (error) {
throw new OperationError(error);
}
}
async write(value: GPIOValue): Promise<void> {
2019-10-15 23:26:29 +09:00
if (!(this.exported && this.direction === "out")) {
throw new InvalidAccessError(
`The exported must be true and value of direction must be "out".`
);
}
2019-10-13 02:13:19 +09:00
try {
await fs.writeFile(
2019-10-18 01:49:23 +09:00
path.join(SysfsGPIOPath, this.portName, "value"),
2019-10-13 02:13:19 +09:00
parseUint16(value.toString()).toString()
);
} catch (error) {
throw new OperationError(error);
}
}
}
2019-10-18 21:24:18 +09:00
export class InvalidAccessError extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
}
}
export class OperationError extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
}
}
2019-10-13 02:13:19 +09:00
// Web GPIOの仕様に基づく意図的なasync関数の使用なので、ルールを無効化
// eslint-disable-next-line
2019-10-13 02:13:19 +09:00
export async function requestGPIOAccess(): Promise<GPIOAccess> {
const ports = new GPIOPortMap(
2019-10-18 01:29:58 +09:00
[...Array(GPIOPortMapSizeMax).keys()].map(portNumber => [
2019-10-13 02:13:19 +09:00
portNumber,
new GPIOPort(portNumber)
])
);
return new GPIOAccess(ports);
}
function sleep(ms: number) {
return new Promise((resolve) => {
return setTimeout(resolve, ms);
});
}