UNPKG

rpi-io

Version:

Nodejs module to control Raspberry Pi GPIO

553 lines (489 loc) 19 kB
// ------------------------------------------------------------------- // JS GPIO API: IN, OUT, PWM // Source: https://libgpiod.readthedocs.io/en/stable/gpio_tools.html // Details: man gpiodetect // ------------------------------------------------------------------- import {spawn} from "child_process"; import {writeFileSync, readFileSync} from "fs" import {exeFile, exeShell, wait} from "./utl.mjs" import {cfgLog, log, trap} from "./dev.mjs" // ------------------------------------------------------------------- // CLASS RIO AND FURTHER /** ------------------------------------------------------------------ * @class RIO * @classDesc Raspberry IO * @param {Number} gpio */ export class Rio { // --------------------------------------------------------------- // CONSTANTS AND VARIABLES AND FUNCTIONS // --------------------------------------------------------------- // Tested system environments: model, os, libgpiod static TESTED_MODELS = ["Raspberry Pi 5 Model B Rev 1.0"] static TESTED_OS = ["Bookworm", "Trixie"] static TESTED_LIBGPIOD = ["v1.6.3", "v2.2.1"] // Lists of supported GPIOs. Reference GPIO: pinout.xyz static RPi_GPIO_CORE = [4, 5, 6, 16, 17, 20, 21, 22, 23, 24, 25, 26, 27] static RPi_GPIO_PWM = [12, 13, 18, 19] static RPI_GPIOS = [...Rio.RPi_GPIO_CORE, ...Rio.RPi_GPIO_PWM] static RPI_CHIP = "gpiochip0" static PWM_CHIP = "pwmchip0" static TYPES = ["in", "out", "pwm"] // List of used GPIOs static used_gpios = [] // Object list of monitored inputs static monitored = {} // Current monitoring process static monitoringProcess = null // --------------------------------------------------------------- // GENERIC METHODS: constructor, disable /** -------------------------------------------------------------- * @function constructor * @description * @param {Number} gpio * @param {String} type - {'in', 'out', 'pwm'} * @return {Object} */ constructor(gpio, type) { this.chip = Rio.RPI_CHIP this.gpio = gpio this.gpioStr = this.gpio.toString() this.gpioName = "GPIO" + this.gpioStr this.type = type this.os = Rio.os().name this.versionLib = Rio.version().substring(0, 4) // OS is not supported if (Rio.TESTED_OS.indexOf(this.os) === -1) throw new Error("This OS is not supported: " + this.os) // libgpiod is not supported const foundLib = Rio.TESTED_LIBGPIOD.find(testedVersion => { return this.versionLib === testedVersion.substring(0, 4) }) if (!foundLib) throw new Error("This version of libgpiod is not supported: " + this.versionLib) // Unknown type if (Rio.TYPES.indexOf(type) === -1) throw new Error("This type is not supported: " + gpio) // gpio is not supported if (Rio.RPI_GPIOS.indexOf(gpio) === -1) throw new Error("This gpio is not supported: " + gpio) // gpio is already used if (Rio.used_gpios.indexOf(gpio) !== -1) throw new Error("This gpio is already used: " + gpio) // this.read, this.write, monArgs depending on gpiolib version switch (this.versionLib) { case "v1.6": this.write = (value, time = 0) => { exeFile("gpioset", [Rio.RPI_CHIP, this.gpioStr + "=" + value]) } this.read = () => { return exeFile("gpioget", [Rio.RPI_CHIP, this.gpioStr]) } this.monArgs = gpios => { let args = [] args.push("-b") // line-buffered: output is flushed after every line args.push("--format=%o %e") // gpio, edge. Others: seconds(%s) and nanoseconds (%n) args.push(Rio.RPI_CHIP) args = args.concat(gpios) return args } this.monData = data => { const [gpio, edge] = data.toString().trim().split(/\s+/) return { gpio: gpio, edge: edge } } break case "v2.2": this.write = (value, time = 0) => { exeFile("gpioset", ["-t" + time, this.gpioName + "=" + value]) } this.read = () => { return exeFile("gpioget", ["--numeric", this.gpioName]) } this.monArgs = gpios => { gpios = gpios.map(gpio => "GPIO" + gpio) let args = [] args = args.concat(gpios) return args } this.monData = data => { const [time, edge, gpio] = data.toString().trim().split(/\s+/) return { gpio: gpio.substring(5,7), edge: edge } } break } // pwm specific cases if (type === "pwm") { // gpio is not supported for PWM if (Rio.RPi_GPIO_PWM.indexOf(gpio) === -1) { throw new Error("PWM gpio must be one of " + Rio.RPi_GPIO_PWM) } // gpio is not setup for PWM const thisConfig = Rio.config(gpio) if (thisConfig.indexOf("pwm") === -1) throw new Error("This gpio is not setup as PWM (see README)") this.channel = thisConfig.slice(-1) this.pwm = Rio.PWM_CHIP this.pathPwm = "/sys/class/pwm/" + this.pwm + "/" this.pathChannel = this.pathPwm + "pwm" + this.channel + "/" } } /** -------------------------------------------------------------- * @function disable * @description Remove instance from Rio.used_gpios * Set this.gpio = -1 */ disable() { const i = Rio.used_gpios.indexOf(this.gpio) if (this.gpio === -1 || i === -1) { log("gpio", this.gpio, "is already disabled") } else { Rio.used_gpios.splice(i, 1) log("gpio", this.gpio, "has been disabled") this.gpio = -1 } } // --------------------------------------------------------------- // OUTPUT METHODS: set /** -------------------------------------------------------------- * @function set * @description Set gpio to 1 or 0 * Then invert when duration time is <> 0 * @param {Number} value * @param {Number} duration ms */ set(value = 0, duration = 0) { if (this.gpio === -1) throw new Error("This instance is disabled") if (this.type !== "out") throw new Error("Set is dedicated to 'out' instances") if ([0, 1].indexOf(value) === -1) throw new Error("Value must be either 0 or 1") if (duration === 0) { this.write(value) log("set gpio", this.gpio, "to", value) } // Optional timer else { this.write(value) log("set gpio", this.gpio, "to", value) setTimeout(() => { const valueStop = Math.abs(value - 1) this.write(valueStop) log("set gpio", this.gpio, "to", valueStop) }, duration) } } // --------------------------------------------------------------- // INPUT METHODS /** -------------------------------------------------------------- * @function get * @description Get gpio value (0/1) */ get() { if (this.gpio === -1) throw new Error("This instance is disabled") if (this.type !== "in") throw new Error("Get is dedicated to 'in' instances") const str = this.read() let result str.length === 0 ? result = -1 : result = parseInt(str.replace(/\r?\n|\r/, "")) log("get gpio", this.gpio, "value:", result) return result } /** -------------------------------------------------------------- * @function monitor * @description Wait for edge events * @param {String} edge {'stop', 'rising', 'falling' 'both'} * @param {Function} callback (edge, time) */ monitor(edge, callback) { if (this.gpio === -1) throw new Error("This instance is disabled") if (this.type !== "in") throw new Error("Monitor is dedicated to 'in' instances") // Update monitoring list Rio.monitored[this.gpio] = { edge: edge, callback: callback } // Suspend current monitoring if any if (Rio.monitoringProcess !== null) { Rio.monitoringProcess.kill("SIGTERM") Rio.monitoringProcess = null } // Default stop command => Remove current gpio from monitoring list if (["rising", "falling", "both"].indexOf(edge) === -1) { delete Rio.monitored[this.gpio] log("gpio", this.gpio, "is no longer being monitored") } // If monitored list is not empty => relaunch gpiomon const list = Object.keys(Rio.monitored) if (list.length > 0) { Rio.monitoringProcess = spawn("gpiomon", this.monArgs(list)) Rio.monitoringProcess.stdout.on("data", data => { // Retrieve event data const evd = this.monData(data) const that = Rio.monitored[parseInt(evd.gpio)] if (typeof that.callback === "function") { if (evd.edge === "1") { if (that.edge === "rising" || that.edge === "both") that.callback("rising", new Date()) } else { if (that.edge === "falling" || that.edge === "both") that.callback("falling", new Date()) } } }) Rio.monitoringProcess.stderr.on("data", data => { trap("gpiomon stderr:", Buffer.from(data).toString()) }) } else { log("no more gpio to monitor") } } // --------------------------------------------------------------- // PWM METHODS // PLEASE NOTE - Commands to unlock a channel: // pwm channel 0: echo 0 > /sys/class/pwm/pwmchip0/unexport // pwm channel 0: echo 1 > /sys/class/pwm/pwmchip0/unexport /** -------------------------------------------------------------- * @function pwmInit * @description Start PWM modulation * @param {Number} period in ns * @param {Number} duty cycle in ns * @param {Object} opt */ pwmInit(period = 20000000, duty = 0, opt) { // Stop on wrong type if (this.type !== "pwm") throw new Error("This GPIO is not configured as PWM") // Stop disabled device if (this.gpio === -1) throw new Error("This instance is disabled") // Set default options const defopt = { dutyMin: 0, dutyMax: 1000000000, // 1s } opt = {...defopt, ...opt} this.period = period this.duty = duty this.dutyMin = Math.max(0, opt.dutyMin) this.dutyMax = Math.min(period, opt.dutyMax) // Check period if (this.period < 100 || this.period > 1000000000) throw new Error("PWM period is out of range (100ns - 1s") // Check duty cycle if (duty < this.dutyMin) throw new Error("PWM duty", duty, " is lower than duty min", this.dutyMin) if (duty > this.dutyMax) throw new Error("PWM duty", duty, " is upper than duty max", this.dutyMax) log("channel", this.channel, "normalized PWM data (period, duty, duty min, duty max):", this.period, this.duty, this.dutyMin, this.dutyMax) // Export channel, then set period, duty cycle and enable writeFileSync(this.pathPwm + "export", this.channel) this.exported = true try { wait(100) writeFileSync(this.pathChannel + "period", String(this.period)) writeFileSync(this.pathChannel + "duty_cycle", String(this.duty)) writeFileSync(this.pathChannel + "enable", "1") this.enabled = true } catch (err) { trap("pwm start error:", err) this.pwmStop() } } /** -------------------------------------------------------------- * @function pwmStop * @description Stop PWM modulation */ pwmStop() { if (this.type !== "pwm") throw new Error("This GPIO is not configured as PWM") // Disable signal then unexport channel writeFileSync(this.pathChannel + "enable", "0") this.enabled = false writeFileSync(this.pathPwm + "unexport", this.channel) this.exported = true } /** -------------------------------------------------------------- * @function pwmDuty * @description Change PWM duty cycle * @parameter {Number} time */ pwmDuty(time) { // Not PWM if (this.type !== "pwm") throw new Error("This GPIO is not configured as PWM") // Not initialized if (!this.enabled || !this.exported) { trap("PWM duty cycle cannot be update if port is not exported and enabled") return } // Out of range this.duty = time if (typeof this.duty !== "number" || this.duty < this.dutyMin) { this.duty = this.dutyMin trap("duty cycle forced to", this.dutyMin) } if (typeof this.duty !== "number" || this.duty > this.dutyMax) { this.duty = this.dutyMax trap("duty cycle forced to", this.dutyMax) } writeFileSync(this.pathChannel + "duty_cycle", this.duty.toString()) } } // ------------------------------------------------------------------- // STATIC METHODS /** ------------------------------------------------------------------ * @function Rio.config * @description Return GPIO configuration * @param {Number} gpio * @return {String} */ Rio.config = gpio => { const a = exeShell("pinctrl get " + gpio).trim().split(/\s+/) return a[a.length - 1].toLowerCase() } /** ------------------------------------------------------------------ * @function Rio.configs * @description Return GPIO configuration for all supported GPIO * @return {Object} */ Rio.configs = () => { let configs = {} Rio.RPI_GPIOS.forEach(gpio => { configs[gpio] = Rio.config(gpio) }) log("gpio configs:", configs) return configs } /** ------------------------------------------------------------------ * @function Rio.info * @description Exec gpioinfo * @return {Object} */ Rio.info = () => { return exeFile("gpioinfo", [Rio.RPI_CHIP]) } /** ------------------------------------------------------------------ * @function Rio.detect * @description Exec gpiodetect * @return {Object} */ Rio.detect = () => { return exeFile("gpiodetect") } /** ------------------------------------------------------------------ * @function Rio.version * @description Return libgpiod version * @return {Object} */ Rio.version = () => { return exeFile("gpiodetect", ["-v"]).split("\n").shift().substring(22).trim() } /** ------------------------------------------------------------------ * @function Rio.stopMonitoring * @description Stop monitoring of all inputs */ Rio.stopMonitoring = () => { Rio.monitoringProcess !== null ? Rio.monitoringProcess.kill("SIGTERM") : false Rio.monitored = {} Rio.monitoringProcess = null log("gpio monitoring stopped") } /** ------------------------------------------------------------------ * @function Rio.cpuInfo * @description Read '/proc/cpuinfo' and store it for further call * @return {String} */ Rio.cpuInfo = () => { if (typeof Rio.cpuInfo.value === "undefined") { try { Rio.cpuInfo.value = readFileSync("/proc/cpuinfo", "utf8") } catch (err) { trap("catched error opening '/proc/cpuinfo:", err) Rio.cpuInfo.value = "" } } return Rio.cpuInfo.value } /** ------------------------------------------------------------------ * @function Rio.os * @description Return OS ID and name * @return {Object} {id: '12', name: 'Bookworm'} */ Rio.os = () => { const strOs = exeShell("cat /etc/os-release") if (strOs.length > 0) { const lines = strOs.split("\n") const info = {} lines.forEach(line => { const [key, value] = line.split("=") if (key && value) { info[key] = value.replace(/"/g, ""); } }) let res = { id: info["VERSION_ID"], name: info["VERSION_CODENAME"] } res.name = res.name.substring(0, 1).toUpperCase() + res.name.substring(1); return res } } /** ------------------------------------------------------------------ * @function Rio.isSystemSupported * @description Check if all hardware and software requirements are * satisfied to run the RPI-IO module * @return {Boolean} */ Rio.isSystemSupported = () => { const info = Rio.cpuInfo() // Test hardware if (/Raspberry Pi/i.test(Rio.cpuInfo()) === false) { trap("not a Raspberry Pi hardware") return false } log("👍 Hardware is Raspberry Pi") // Test model const line = Rio.cpuInfo().split("\n").find(str => str.indexOf("Model") !== -1) const model = line.split(":")[1].trim() if (Rio.TESTED_MODELS.indexOf(model) === -1) { trap("model not tested:", model) return false } log("👍 ", model, "is supported") // Test OS const thisOs = Rio.os() if (Rio.TESTED_OS.indexOf(thisOs.name) === -1) { trap("OS not tested:", thisOs.name, thisOs.id) return false } log("👍 ", thisOs.name, "OS is supported") // Test libgpiod version x.y const gpiodVersion = Rio.version().substring(0, 4) const foundLib = Rio.TESTED_LIBGPIOD.find(testedVersion => { return gpiodVersion === testedVersion.substring(0, 4) }) if (!foundLib) { trap("libgpiod version not tested:", gpiodVersion) return false } log("👍 libgpiod version", gpiodVersion + ".x", "is supported") log("👍 Systems requirements for rpi-io are met") return true } /** ------------------------------------------------------------------ * @function Rio.log */ Rio.log = cfgLog // ------------------------------------------------------------------- // EoF // -------------------------------------------------------------------