-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.ts
394 lines (348 loc) · 9.26 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
import { EventEmitter } from 'node:events';
import { promises as fs } from 'node:fs';
import * as os from 'node:os';
import * as path from 'node: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;
// biome-ignore lint/style/noUselessElse:
else throw new RangeError(`Must be between 0 and ${Uint16Max}.`);
}
/**
* GPIO0 オフセット
* @see {@link https://github.com/raspberrypi/linux/issues/6037}
*/
const GpioOffset =
process.platform === 'linux' && 6.6 <= Number(os.release().match(/\d+\.\d+/))
? 512
: 0;
/** ポート番号 */
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 {
/** イベント */
// biome-ignore lint/style/useShorthandFunctionType:
(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;
// biome-ignore lint/complexity/noForEach:
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()) + GpioOffset;
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,
);
}
// biome-ignore lint/suspicious/noExplicitAny:
} catch (error: any) {
if (this._exportRetry < 10) {
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),
);
// biome-ignore lint/suspicious/noExplicitAny:
} 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;
// biome-ignore lint/suspicious/noExplicitAny:
} 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(),
);
// biome-ignore lint/suspicious/noExplicitAny:
} 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);
});
}