mirror of
https://github.com/chirimen-oh/node-web-gpio.git
synced 2025-04-01 10:55:20 +00:00
create @notweb/gpio
This commit is contained in:
commit
aab3397bf9
9 changed files with 508 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/node_modules/
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 Kohei Watanabe
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
27
README.md
Normal file
27
README.md
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# @notweb/gpio
|
||||||
|
|
||||||
|
GPIO access with Node.js
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { requestGPIOAccess } = require("@notweb/gpio");
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const gpioAccess = await requestGPIOAccess();
|
||||||
|
const port = gpioAccess.ports.get(26);
|
||||||
|
|
||||||
|
await port.export("out");
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
port.write(value);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1e3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Document
|
||||||
|
|
||||||
|
[Web GPIO API](http://browserobo.github.io/WebGPIO)
|
57
index.d.ts
vendored
Normal file
57
index.d.ts
vendored
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
/// <reference types="node" />
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
declare type PortNumber = number;
|
||||||
|
declare type PortName = string;
|
||||||
|
declare type PinName = string;
|
||||||
|
declare type DirectionMode = "in" | "out";
|
||||||
|
declare type GPIOValue = 0 | 1;
|
||||||
|
interface GPIOChangeEvent {
|
||||||
|
readonly value: GPIOValue;
|
||||||
|
readonly port: GPIOPort;
|
||||||
|
}
|
||||||
|
interface GPIOChangeEventHandler {
|
||||||
|
(event: GPIOChangeEvent): void;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Not a specification in Web GPIO API.
|
||||||
|
*/
|
||||||
|
interface GPIOPortChangeEventHandler {
|
||||||
|
(event: GPIOChangeEvent["value"]): void;
|
||||||
|
}
|
||||||
|
export declare class GPIOAccess extends EventEmitter {
|
||||||
|
private readonly _ports;
|
||||||
|
onchange: GPIOChangeEventHandler | undefined;
|
||||||
|
constructor(ports?: GPIOPortMap);
|
||||||
|
readonly ports: GPIOPortMap;
|
||||||
|
/**
|
||||||
|
* Unexport all exported GPIO ports.
|
||||||
|
*/
|
||||||
|
unexportAll(): Promise<void>;
|
||||||
|
}
|
||||||
|
export declare class GPIOPortMap extends Map<PortNumber, GPIOPort> {
|
||||||
|
}
|
||||||
|
export declare class GPIOPort extends EventEmitter {
|
||||||
|
private readonly _portNumber;
|
||||||
|
private readonly _pollingInterval;
|
||||||
|
private _direction;
|
||||||
|
private _exported;
|
||||||
|
private _value;
|
||||||
|
private _timeout;
|
||||||
|
onchange: GPIOPortChangeEventHandler | undefined;
|
||||||
|
constructor(portNumber: PortNumber);
|
||||||
|
readonly portNumber: PortNumber;
|
||||||
|
readonly portName: PortName;
|
||||||
|
readonly pinName: PinName;
|
||||||
|
readonly direction: DirectionMode;
|
||||||
|
readonly exported: boolean;
|
||||||
|
export(direction: DirectionMode): Promise<void>;
|
||||||
|
unexport(): Promise<void>;
|
||||||
|
read(): Promise<GPIOValue>;
|
||||||
|
write(value: GPIOValue): Promise<void>;
|
||||||
|
}
|
||||||
|
export declare class InvalidAccessError extends Error {
|
||||||
|
}
|
||||||
|
export declare class OperationError extends Error {
|
||||||
|
}
|
||||||
|
export declare function requestGPIOAccess(): Promise<GPIOAccess>;
|
||||||
|
export {};
|
142
index.js
Normal file
142
index.js
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
const events_1 = require("events");
|
||||||
|
const fs_1 = require("fs");
|
||||||
|
/**
|
||||||
|
* Interval of file system polling, in milliseconds.
|
||||||
|
*/
|
||||||
|
const PollingInterval = 100;
|
||||||
|
const Uint16Max = 65535;
|
||||||
|
function parseUint16(string) {
|
||||||
|
const n = Number.parseInt(string, 10);
|
||||||
|
if (0 <= n && n <= Uint16Max)
|
||||||
|
return n;
|
||||||
|
else
|
||||||
|
throw new RangeError(`Must be between 0 and ${Uint16Max}.`);
|
||||||
|
}
|
||||||
|
class GPIOAccess extends events_1.EventEmitter {
|
||||||
|
constructor(ports) {
|
||||||
|
super();
|
||||||
|
this._ports = ports == null ? new GPIOPortMap() : ports;
|
||||||
|
this._ports.forEach(port => port.on("change", value => {
|
||||||
|
const event = { value, port };
|
||||||
|
this.emit("change", event);
|
||||||
|
}));
|
||||||
|
this.on("change", (event) => {
|
||||||
|
if (this.onchange !== undefined)
|
||||||
|
this.onchange(event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
get ports() {
|
||||||
|
return this._ports;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Unexport all exported GPIO ports.
|
||||||
|
*/
|
||||||
|
async unexportAll() {
|
||||||
|
await Promise.all([...this.ports.values()].map(port => port.exported ? port.unexport() : undefined));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.GPIOAccess = GPIOAccess;
|
||||||
|
class GPIOPortMap extends Map {
|
||||||
|
}
|
||||||
|
exports.GPIOPortMap = GPIOPortMap;
|
||||||
|
class GPIOPort extends events_1.EventEmitter {
|
||||||
|
constructor(portNumber) {
|
||||||
|
super();
|
||||||
|
this._portNumber = portNumber;
|
||||||
|
this._pollingInterval = PollingInterval;
|
||||||
|
this._direction = new OperationError("Unknown direction.");
|
||||||
|
this._exported = new OperationError("Unknown export.");
|
||||||
|
this.on("change", (value) => {
|
||||||
|
if (this.onchange !== undefined)
|
||||||
|
this.onchange(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
get portNumber() {
|
||||||
|
return this._portNumber;
|
||||||
|
}
|
||||||
|
get portName() {
|
||||||
|
// NOTE: Unknown portName.
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
get pinName() {
|
||||||
|
// NOTE: Unknown pinName.
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
get direction() {
|
||||||
|
if (this._direction instanceof OperationError)
|
||||||
|
throw this._direction;
|
||||||
|
return this._direction;
|
||||||
|
}
|
||||||
|
get exported() {
|
||||||
|
if (this._exported instanceof OperationError)
|
||||||
|
throw this._exported;
|
||||||
|
return this._exported;
|
||||||
|
}
|
||||||
|
async export(direction) {
|
||||||
|
if (!/^(in|out)$/.test(direction)) {
|
||||||
|
throw new InvalidAccessError(`Must be "in" or "out".`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
clearInterval(this._timeout);
|
||||||
|
await fs_1.promises.writeFile(`/sys/class/gpio/export`, parseUint16(this.portNumber.toString()).toString());
|
||||||
|
await fs_1.promises.writeFile(`/sys/class/gpio/${parseUint16(this.portNumber.toString())}/direction`, direction);
|
||||||
|
if (direction === "in") {
|
||||||
|
this._timeout = setInterval(this.read.bind(this), this._pollingInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
throw new OperationError(error);
|
||||||
|
}
|
||||||
|
this._direction = direction;
|
||||||
|
this._exported = true;
|
||||||
|
}
|
||||||
|
async unexport() {
|
||||||
|
clearInterval(this._timeout);
|
||||||
|
try {
|
||||||
|
await fs_1.promises.writeFile(`/sys/class/gpio/unexport`, parseUint16(this.portNumber.toString()).toString());
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
throw new OperationError(error);
|
||||||
|
}
|
||||||
|
this._exported = false;
|
||||||
|
}
|
||||||
|
async read() {
|
||||||
|
try {
|
||||||
|
const buffer = await fs_1.promises.readFile(`/sys/class/gpio/${parseUint16(this.portNumber.toString())}/value`);
|
||||||
|
const value = parseUint16(buffer.toString());
|
||||||
|
if (this._value !== value) {
|
||||||
|
this._value = value;
|
||||||
|
this.emit("change", value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
throw new OperationError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async write(value) {
|
||||||
|
try {
|
||||||
|
await fs_1.promises.writeFile(`/sys/class/gpio/${parseUint16(this.portNumber.toString())}/value`, parseUint16(value.toString()).toString());
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
throw new OperationError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.GPIOPort = GPIOPort;
|
||||||
|
class InvalidAccessError extends Error {
|
||||||
|
}
|
||||||
|
exports.InvalidAccessError = InvalidAccessError;
|
||||||
|
class OperationError extends Error {
|
||||||
|
}
|
||||||
|
exports.OperationError = OperationError;
|
||||||
|
async function requestGPIOAccess() {
|
||||||
|
const ports = new GPIOPortMap([...Array(Uint16Max + 1).keys()].map(portNumber => [
|
||||||
|
portNumber,
|
||||||
|
new GPIOPort(portNumber)
|
||||||
|
]));
|
||||||
|
return new GPIOAccess(ports);
|
||||||
|
}
|
||||||
|
exports.requestGPIOAccess = requestGPIOAccess;
|
213
index.ts
Normal file
213
index.ts
Normal file
|
@ -0,0 +1,213 @@
|
||||||
|
import { EventEmitter } from "events";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interval of file system polling, in milliseconds.
|
||||||
|
*/
|
||||||
|
const PollingInterval = 100;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not a specification in Web GPIO API.
|
||||||
|
*/
|
||||||
|
interface GPIOPortChangeEventHandler {
|
||||||
|
(event: GPIOChangeEvent["value"]): 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", value => {
|
||||||
|
const event: GPIOChangeEvent = { value, port };
|
||||||
|
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() {
|
||||||
|
await Promise.all(
|
||||||
|
[...this.ports.values()].map(port =>
|
||||||
|
port.exported ? port.unexport() : undefined
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 _value: GPIOValue | undefined;
|
||||||
|
private _timeout: ReturnType<typeof setInterval> | undefined;
|
||||||
|
onchange: GPIOPortChangeEventHandler | undefined;
|
||||||
|
|
||||||
|
constructor(portNumber: PortNumber) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this._portNumber = portNumber;
|
||||||
|
this._pollingInterval = PollingInterval;
|
||||||
|
this._direction = new OperationError("Unknown direction.");
|
||||||
|
this._exported = new OperationError("Unknown export.");
|
||||||
|
|
||||||
|
this.on("change", (value: GPIOChangeEvent["value"]): void => {
|
||||||
|
if (this.onchange !== undefined) this.onchange(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get portNumber(): PortNumber {
|
||||||
|
return this._portNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
get portName(): PortName {
|
||||||
|
// NOTE: Unknown portName.
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (!/^(in|out)$/.test(direction)) {
|
||||||
|
throw new InvalidAccessError(`Must be "in" or "out".`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
clearInterval(this._timeout as any);
|
||||||
|
await fs.writeFile(
|
||||||
|
`/sys/class/gpio/export`,
|
||||||
|
parseUint16(this.portNumber.toString()).toString()
|
||||||
|
);
|
||||||
|
await fs.writeFile(
|
||||||
|
`/sys/class/gpio/${parseUint16(this.portNumber.toString())}/direction`,
|
||||||
|
direction
|
||||||
|
);
|
||||||
|
if (direction === "in") {
|
||||||
|
this._timeout = setInterval(
|
||||||
|
this.read.bind(this),
|
||||||
|
this._pollingInterval
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new OperationError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._direction = direction;
|
||||||
|
this._exported = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async unexport() {
|
||||||
|
clearInterval(this._timeout as any);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.writeFile(
|
||||||
|
`/sys/class/gpio/unexport`,
|
||||||
|
parseUint16(this.portNumber.toString()).toString()
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw new OperationError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._exported = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async read() {
|
||||||
|
try {
|
||||||
|
const buffer = await fs.readFile(
|
||||||
|
`/sys/class/gpio/${parseUint16(this.portNumber.toString())}/value`
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = parseUint16(buffer.toString()) as GPIOValue;
|
||||||
|
|
||||||
|
if (this._value !== value) {
|
||||||
|
this._value = value;
|
||||||
|
this.emit("change", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
} catch (error) {
|
||||||
|
throw new OperationError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async write(value: GPIOValue) {
|
||||||
|
try {
|
||||||
|
await fs.writeFile(
|
||||||
|
`/sys/class/gpio/${parseUint16(this.portNumber.toString())}/value`,
|
||||||
|
parseUint16(value.toString()).toString()
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw new OperationError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InvalidAccessError extends Error {}
|
||||||
|
|
||||||
|
export class OperationError extends Error {}
|
||||||
|
|
||||||
|
export async function requestGPIOAccess(): Promise<GPIOAccess> {
|
||||||
|
const ports = new GPIOPortMap(
|
||||||
|
[...Array(Uint16Max + 1).keys()].map(portNumber => [
|
||||||
|
portNumber,
|
||||||
|
new GPIOPort(portNumber)
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
return new GPIOAccess(ports);
|
||||||
|
}
|
22
package.json
Normal file
22
package.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"name": "@notweb/gpio",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "GPIO access with Node.js",
|
||||||
|
"main": "index.js",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/kou029w/notweb-gpio.git"
|
||||||
|
},
|
||||||
|
"author": "Kohei Watanabe <kou029w@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^12.7.12",
|
||||||
|
"typescript": "^3.6.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc"
|
||||||
|
}
|
||||||
|
}
|
12
tsconfig.json
Normal file
12
tsconfig.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "esnext",
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"declaration": true,
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true
|
||||||
|
}
|
||||||
|
}
|
13
yarn.lock
Normal file
13
yarn.lock
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@types/node@^12.7.12":
|
||||||
|
version "12.7.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.12.tgz#7c6c571cc2f3f3ac4a59a5f2bd48f5bdbc8653cc"
|
||||||
|
integrity sha512-KPYGmfD0/b1eXurQ59fXD1GBzhSQfz6/lKBxkaHX9dKTzjXbK68Zt7yGUxUsCS1jeTy/8aL+d9JEr+S54mpkWQ==
|
||||||
|
|
||||||
|
typescript@^3.6.4:
|
||||||
|
version "3.6.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.4.tgz#b18752bb3792bc1a0281335f7f6ebf1bbfc5b91d"
|
||||||
|
integrity sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==
|
Loading…
Add table
Reference in a new issue