mirror of
https://github.com/chirimen-oh/node-web-gpio.git
synced 2025-01-18 08:05:07 +00:00
8fc977510d
Ref. #41 Co-authored-by: akihiko kigure <akihiko.kigure@lifewood-m1.local> Co-authored-by: bot <bot@example>
377 lines
8.6 KiB
TypeScript
377 lines
8.6 KiB
TypeScript
import { EventEmitter } from 'events';
|
|
import { promises as fs } from 'fs';
|
|
import * as path from 'path';
|
|
|
|
/**
|
|
* Interval of file system polling, in milliseconds.
|
|
*/
|
|
const PollingInterval = 100;
|
|
|
|
/**
|
|
* GPIO パス
|
|
*/
|
|
const SysfsGPIOPath = '/sys/class/gpio';
|
|
|
|
/**
|
|
* GPIO ポートマップサイズ
|
|
*/
|
|
const GPIOPortMapSizeMax = 1024;
|
|
|
|
/**
|
|
* Uint16 Max サイズ
|
|
*/
|
|
const Uint16Max = 65535;
|
|
|
|
/**
|
|
*
|
|
* Uint16型変換処理
|
|
* @param parseString 変換文字列
|
|
* @return Uint16型変換値
|
|
*/
|
|
function parseUint16(parseString: string) {
|
|
const n = Number.parseInt(parseString, 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';
|
|
|
|
/** GPIO 値 0: LOW / 1: HIGH */
|
|
type GPIOValue = 0 | 1;
|
|
|
|
/**
|
|
* GPIO チェンジイベント
|
|
*/
|
|
interface GPIOChangeEvent {
|
|
/** 入出力値 */
|
|
readonly value: GPIOValue;
|
|
/** ポート */
|
|
readonly port: GPIOPort;
|
|
}
|
|
|
|
/**
|
|
* GPIO チェンジイベントハンドラ
|
|
*/
|
|
interface GPIOChangeEventHandler {
|
|
/** イベント */
|
|
(event: GPIOChangeEvent): void;
|
|
}
|
|
|
|
/**
|
|
* GPIO
|
|
*/
|
|
export class GPIOAccess extends EventEmitter {
|
|
/** ポート */
|
|
private readonly _ports: GPIOPortMap;
|
|
/** GPIO チェンジイベントハンドラ */
|
|
onchange: GPIOChangeEventHandler | undefined;
|
|
|
|
/**
|
|
* Creates an instance of GPIOAccess.
|
|
* @param ports ポート番号
|
|
*/
|
|
constructor(ports?: GPIOPortMap) {
|
|
super();
|
|
|
|
this._ports = ports == null ? new GPIOPortMap() : ports;
|
|
this._ports.forEach((port) =>
|
|
port.on('change', (event) => {
|
|
this.emit('change', event);
|
|
})
|
|
);
|
|
|
|
this.on('change', (event: GPIOChangeEvent): void => {
|
|
if (this.onchange !== undefined) this.onchange(event);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* ポート情報取得処理
|
|
* @return 現在のポート情報
|
|
*/
|
|
get ports(): GPIOPortMap {
|
|
return this._ports;
|
|
}
|
|
|
|
/**
|
|
* Unexport all exported GPIO ports.
|
|
* 全てのポート開放をする
|
|
* @return ポート開放結果
|
|
*/
|
|
async unexportAll(): Promise<void> {
|
|
await Promise.all(
|
|
[...this.ports.values()].map((port) =>
|
|
port.exported ? port.unexport() : undefined
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Different from Web GPIO API specification.
|
|
*/
|
|
export class GPIOPortMap extends Map<PortNumber, GPIOPort> {}
|
|
|
|
/**
|
|
* GPIO ポート
|
|
*/
|
|
export class GPIOPort extends EventEmitter {
|
|
/** ポート番号 */
|
|
private readonly _portNumber: PortNumber;
|
|
/** ポーリング間隔 */
|
|
private readonly _pollingInterval: number;
|
|
/** 入出力方向 */
|
|
private _direction: DirectionMode | OperationError;
|
|
/** エクスポート */
|
|
private _exported: boolean | OperationError;
|
|
/** エクスポートリトライ回数 */
|
|
private _exportRetry: number;
|
|
/** 入出力値 */
|
|
private _value: GPIOValue | undefined;
|
|
/** タイムアウト値 */
|
|
private _timeout: ReturnType<typeof setInterval> | undefined;
|
|
/** GPIO チェンジイベントハンドラ */
|
|
onchange: GPIOChangeEventHandler | undefined;
|
|
|
|
/**
|
|
* Creates an instance of GPIOPort.
|
|
* @param portNumber ポート番号
|
|
*/
|
|
constructor(portNumber: PortNumber) {
|
|
super();
|
|
|
|
this._portNumber = parseUint16(portNumber.toString());
|
|
this._pollingInterval = PollingInterval;
|
|
this._direction = new OperationError('Unknown direction.');
|
|
this._exported = new OperationError('Unknown export.');
|
|
this._exportRetry = 0;
|
|
|
|
this.on('change', (event: GPIOChangeEvent): void => {
|
|
if (this.onchange !== undefined) this.onchange(event);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* ポート番号取得処理
|
|
* @return 現在のポート番号
|
|
*/
|
|
get portNumber(): PortNumber {
|
|
return this._portNumber;
|
|
}
|
|
|
|
/**
|
|
* ポート名取得処理
|
|
* @return 現在のポート名
|
|
*/
|
|
get portName(): PortName {
|
|
return `gpio${this.portNumber}`;
|
|
}
|
|
|
|
/**
|
|
* ピン名取得処理
|
|
* @return 現在のピン名
|
|
*/
|
|
get pinName(): PinName {
|
|
// NOTE: Unknown pinName.
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* GPIO 入出力方向 getter
|
|
* @return 現在のGPIO 入出力方向
|
|
*/
|
|
get direction(): DirectionMode {
|
|
if (this._direction instanceof OperationError) throw this._direction;
|
|
return this._direction;
|
|
}
|
|
|
|
/**
|
|
* GPIO export の有無 getter
|
|
* @return 現在のGPIO 出力
|
|
*/
|
|
get exported(): boolean {
|
|
if (this._exported instanceof OperationError) throw this._exported;
|
|
return this._exported;
|
|
}
|
|
|
|
/**
|
|
* GPIO 出力処理
|
|
* @param direction GPIO 入出力方向
|
|
* @return export 処理の完了
|
|
*/
|
|
async export(direction: DirectionMode): Promise<void> {
|
|
if (!/^(in|out)$/.test(direction)) {
|
|
throw new InvalidAccessError(`Must be "in" or "out".`);
|
|
}
|
|
|
|
try {
|
|
await fs.access(path.join(SysfsGPIOPath, this.portName));
|
|
this._exported = true;
|
|
} catch {
|
|
this._exported = false;
|
|
}
|
|
|
|
try {
|
|
clearInterval(this._timeout as ReturnType<typeof setInterval>);
|
|
if (!this.exported) {
|
|
await fs.writeFile(
|
|
path.join(SysfsGPIOPath, 'export'),
|
|
String(this.portNumber)
|
|
);
|
|
}
|
|
await fs.writeFile(
|
|
path.join(SysfsGPIOPath, this.portName, 'direction'),
|
|
direction
|
|
);
|
|
if (direction === 'in') {
|
|
this._timeout = setInterval(
|
|
// eslint-disable-next-line
|
|
this.read.bind(this),
|
|
this._pollingInterval
|
|
);
|
|
}
|
|
} catch (error: any) {
|
|
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);
|
|
}
|
|
}
|
|
|
|
this._direction = direction;
|
|
this._exported = true;
|
|
}
|
|
|
|
/**
|
|
* Unexport exported GPIO ports.
|
|
* ポート開放をする
|
|
* @return ポート開放処理の完了
|
|
*/
|
|
async unexport(): Promise<void> {
|
|
clearInterval(this._timeout as ReturnType<typeof setInterval>);
|
|
|
|
try {
|
|
await fs.writeFile(
|
|
path.join(SysfsGPIOPath, 'unexport'),
|
|
String(this.portNumber)
|
|
);
|
|
} catch (error: any) {
|
|
throw new OperationError(error);
|
|
}
|
|
|
|
this._exported = false;
|
|
}
|
|
|
|
/**
|
|
* 入力値読み取り処理
|
|
* @return 読み取り処理の完了
|
|
*/
|
|
async read(): Promise<GPIOValue> {
|
|
if (!(this.exported && this.direction === 'in')) {
|
|
throw new InvalidAccessError(
|
|
`The exported must be true and value of direction must be "in".`
|
|
);
|
|
}
|
|
|
|
try {
|
|
const buffer = await fs.readFile(
|
|
path.join(SysfsGPIOPath, this.portName, 'value')
|
|
);
|
|
|
|
const value = parseUint16(buffer.toString()) as GPIOValue;
|
|
|
|
if (this._value !== value) {
|
|
this._value = value;
|
|
this.emit('change', { value, port: this });
|
|
}
|
|
|
|
return value;
|
|
} catch (error: any) {
|
|
throw new OperationError(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 出力値書き込み処理
|
|
* @return 読み取り処理の完了
|
|
*/
|
|
async write(value: GPIOValue): Promise<void> {
|
|
if (!(this.exported && this.direction === 'out')) {
|
|
throw new InvalidAccessError(
|
|
`The exported must be true and value of direction must be "out".`
|
|
);
|
|
}
|
|
|
|
try {
|
|
await fs.writeFile(
|
|
path.join(SysfsGPIOPath, this.portName, 'value'),
|
|
parseUint16(value.toString()).toString()
|
|
);
|
|
} catch (error: any) {
|
|
throw new OperationError(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 無効なアクセスエラー
|
|
*/
|
|
export class InvalidAccessError extends Error {
|
|
/**
|
|
* Creates an instance of InvalidAccessError.
|
|
* @param message エラーメッセージ
|
|
*/
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = this.constructor.name;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 操作エラー
|
|
*/
|
|
export class OperationError extends Error {
|
|
/**
|
|
* Creates an instance of OperationError.
|
|
* @param message エラーメッセージ
|
|
*/
|
|
constructor(message: string) {
|
|
super(message);
|
|
this.name = this.constructor.name;
|
|
}
|
|
}
|
|
|
|
// Web GPIOの仕様に基づく意図的なasync関数の使用なので、ルールを無効化
|
|
// eslint-disable-next-line
|
|
export async function requestGPIOAccess(): Promise<GPIOAccess> {
|
|
const ports = new GPIOPortMap(
|
|
[...Array(GPIOPortMapSizeMax).keys()].map((portNumber) => [
|
|
portNumber,
|
|
new GPIOPort(portNumber),
|
|
])
|
|
);
|
|
|
|
return new GPIOAccess(ports);
|
|
}
|
|
|
|
/**
|
|
* 待機 関数
|
|
* @param ms スリープ時間(ミリ秒)
|
|
* @return 待機完了
|
|
*/
|
|
function sleep(ms: number) {
|
|
return new Promise((resolve) => {
|
|
return setTimeout(resolve, ms);
|
|
});
|
|
}
|