commit aab3397bf9bdcce09b6f85648bf2f13d56c6afec Author: Kohei Watanabe <kou029w@gmail.com> Date: Sun Oct 13 02:13:19 2019 +0900 create @notweb/gpio diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ccbe46 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/node_modules/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5376ef9 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7f734a1 --- /dev/null +++ b/README.md @@ -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) diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..4a82599 --- /dev/null +++ b/index.d.ts @@ -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 {}; diff --git a/index.js b/index.js new file mode 100644 index 0000000..a5a6f63 --- /dev/null +++ b/index.js @@ -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; diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..313766e --- /dev/null +++ b/index.ts @@ -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); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b7e224b --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7c4531f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "commonjs", + "moduleResolution": "node", + "declaration": true, + "strict": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..793c05a --- /dev/null +++ b/yarn.lock @@ -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==