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==