rpi-io 1.2.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/binding.gyp ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "targets": [
3
+ {
4
+ "target_name": "gpio",
5
+ "sources": [ "addon/gpio.c" ],
6
+ "include_dirs": [
7
+ "<!@(node -p \"require('node-addon-api').include\")"
8
+ ],
9
+ "libraries": [
10
+ "-lgpiod",
11
+ "-lpthread"
12
+ ],
13
+ "cflags": [
14
+ "-Wall",
15
+ "-Wno-implicit-function-declaration",
16
+ "-Wno-incompatible-pointer-types",
17
+ "-O2",
18
+ "-std=gnu99"
19
+ ],
20
+ "defines": [
21
+ "<!@(sh -c 'chmod +x script/detect-gpiod-version.sh 2>/dev/null; bash script/detect-gpiod-version.sh 2>/dev/null || echo LIBGPIOD_V2')"
22
+ ],
23
+ "conditions": [
24
+ ["OS=='linux'", {
25
+ "libraries": [
26
+ "-lgpiod",
27
+ "-lpthread"
28
+ ]
29
+ }]
30
+ ]
31
+ }
32
+ ]
33
+ }
package/esm/_util.mjs ADDED
@@ -0,0 +1,122 @@
1
+ // -------------------------------------------------------------------
2
+ // RPI-IO: Nodejs tools and utilities
3
+ // -------------------------------------------------------------------
4
+ import {execSync} from "child_process"
5
+ import {spawnSync} from "node:child_process"
6
+
7
+ // -------------------------------------------------------------------
8
+ // DEV UTILITIES
9
+ // -------------------------------------------------------------------
10
+ let debug = {
11
+ trace: false,
12
+ warn: true
13
+ }
14
+ /** ------------------------------------------------------------------
15
+ * @function traceCfg
16
+ * @description Set log configuration
17
+ * @param {Number} level 0: no log, 1: warn only, 2: warn and log
18
+ */
19
+ export const traceCfg = level => {
20
+ debug = {
21
+ trace: false,
22
+ warn: false
23
+ }
24
+ level > 1 ? debug.trace = true : false
25
+ level > 0 ? debug.warn = true : false
26
+ }
27
+
28
+ /** ------------------------------------------------------------------
29
+ * @function nowStr
30
+ * @description Return timestamp as formatted string
31
+ * @return {String}
32
+ */
33
+ const nowStr = () => {
34
+ const date = new Date()
35
+ return date.toLocaleTimeString() + "." + date.getMilliseconds().toLocaleString('en', {
36
+ minimumIntegerDigits: 3,
37
+ minimumFractionDigits: 0,
38
+ useGrouping: false
39
+ })
40
+ }
41
+
42
+ /** ------------------------------------------------------------------
43
+ * @function log
44
+ * @description Global customized timestamped console.log
45
+ * Requires global variable 'debug'
46
+ */
47
+ export const log = function () {
48
+ if (debug.trace)
49
+ console.log.apply(console, Array.prototype.concat.apply([nowStr(), "🔎 "], arguments))
50
+ }
51
+
52
+ /** ------------------------------------------------------------------
53
+ * @function warn
54
+ * @description Colored log for error messages ⚠️ nodejs only ~ CSS color #F50
55
+ */
56
+ export const warn = function () {
57
+ if (debug.warn)
58
+ console.log.apply(console, Array.prototype.concat.apply([nowStr(), "⚠️", "\x1b[38;2;255;80;0m", ...arguments, "\x1b[0m"]))
59
+ }
60
+
61
+ /** ------------------------------------------------------------------
62
+ * @function sleep
63
+ * @description Wait before continuing
64
+ * @param {Number} ms
65
+ * @param {Boolean} track
66
+ */
67
+ export const sleep = (ms, track = true) => {
68
+ track ? log("sleeping", ms, "ms") : false
69
+ return new Promise(resolve => setTimeout(resolve, ms));
70
+ }
71
+
72
+ /** ------------------------------------------------------------------
73
+ * @function wait
74
+ * @description Wait before continuing (sync blocking mode)
75
+ * @param {Number} ms
76
+ */
77
+ export const wait = ms => {
78
+ log("waiting", ms, "ms")
79
+ spawnSync("sleep", [ms / 1000])
80
+ }
81
+
82
+ /** ------------------------------------------------------------------
83
+ * @function ctrlC
84
+ * @description Intercept ctrl+c then exec callback
85
+ * @param {Function} callback
86
+ */
87
+ export const ctrlC = callback => {
88
+ process.on("SIGINT", () => {
89
+ log("ctrl+c pressed")
90
+ typeof callback === "function" ? callback() : false
91
+ process.exit(0)
92
+ })
93
+ }
94
+
95
+ /** ------------------------------------------------------------------
96
+ * @function lineConfig
97
+ * @description Return line configuration
98
+ * @param {Number} line
99
+ * @return {String}
100
+ */
101
+ export const lineConfig = line => {
102
+ const stdout = execSync("pinctrl get " + line, {stdio: "pipe", encoding: "utf8"}).trim().split(/\s+/)
103
+ return stdout[stdout.length - 1].toLowerCase()
104
+ }
105
+
106
+ /** ------------------------------------------------------------------
107
+ * @function lineNumber
108
+ * @param {Number} nth
109
+ * @return {Number}
110
+ */
111
+ export const lineNumber = nth => {
112
+ const arg = parseInt(process.argv[nth])
113
+ if (typeof arg !== "number" || arg !== arg) {
114
+ warn("Line number expected as argument")
115
+ return -1
116
+ }
117
+ return arg
118
+ }
119
+
120
+ // -------------------------------------------------------------------
121
+ // EoF
122
+ // -------------------------------------------------------------------
package/esm/main.mjs ADDED
@@ -0,0 +1,307 @@
1
+ // -------------------------------------------------------------------
2
+ // RPI-IO: Nodejs GPIO control module
3
+ // -------------------------------------------------------------------
4
+ import {createRequire} from "node:module"
5
+ import {writeFileSync} from "node:fs"
6
+ import {traceCfg, log, warn, sleep, ctrlC, lineConfig, lineNumber, wait} from "./_util.mjs"
7
+
8
+ export {traceCfg, log, warn, sleep, ctrlC, lineConfig, lineNumber}
9
+ // -------------------------------------------------------------------
10
+ // CONSTANTS + VARIABLES
11
+ // -------------------------------------------------------------------
12
+ const require = createRequire(import.meta.url)
13
+ const ADDON = require("../build/Release/gpio.node")
14
+ const CHIPNAME = "/dev/gpiochip0"
15
+ const RPi_GPIO_STD = [4, 5, 6, 16, 17, 20, 21, 22, 23, 24, 25, 26, 27]
16
+ const RPi_GPIO_PWM = [12, 13, 18, 19]
17
+ const RPI_GPIO_ALL = [...RPi_GPIO_STD, ...RPi_GPIO_PWM]
18
+ const RPI_CHIP = "gpiochip0"
19
+ const PWM_CHIP = "pwmchip0"
20
+
21
+ // -------------------------------------------------------------------
22
+ // CLASS RIO & METHODS
23
+ /** ------------------------------------------------------------------
24
+ * @class RIO
25
+ * @classDesc Generic class manage GPIO lines
26
+ * @return {String}
27
+ */
28
+ export class RIO {
29
+
30
+ static instances = new Map()
31
+
32
+ /** ------------------------------------------------------------------
33
+ * @method constructor
34
+ * @param {Number} line - BCM number
35
+ * @param {String} mode - "input", "output", "pwm"
36
+ * @param {Object} opt - misc options depending on mode
37
+ */
38
+ constructor(line, mode, opt) {
39
+
40
+ const defopt = {
41
+ // output
42
+ value: 0, // Initial value
43
+ // input, output
44
+ bias: "disable", // "disable", "pull-up", "pull-down"
45
+ // pwm
46
+ exportTime: 100,
47
+ period: 20000, // μs ~50Hz
48
+ dutyMin: 0, // μs
49
+ dutyMax: 20000 // µs
50
+ }
51
+ opt = {...defopt, ...opt}
52
+
53
+ if (RPI_GPIO_ALL.indexOf(line) === -1)
54
+ throw new Error("This line is not supported: " + line)
55
+
56
+ // line is already defined
57
+ if (RIO.instances.has(line))
58
+ throw new Error("This line is already defined: " + line)
59
+
60
+ this.line = line
61
+ this.handle = null
62
+ this.mode = mode
63
+ this.value = opt.value
64
+ this.bias = opt.bias
65
+ this.closed = false // Instance status
66
+ this.monitoring = false // Monitoring status
67
+ this.config = lineConfig(this.line) // Required for pwm
68
+ this.pwmExported = false
69
+ this.pwmEnabled = false
70
+
71
+ switch (this.mode) {
72
+ case "output":
73
+ this.handle = ADDON.openOutput(CHIPNAME, line, this.value, opt.bias)
74
+ break
75
+ case "input":
76
+ this.handle = ADDON.openInput(CHIPNAME, line, opt.bias)
77
+ break
78
+ case "pwm":
79
+ if (RPi_GPIO_PWM.indexOf(line) === -1)
80
+ throw new Error("This line is not supported for PWM: " + line)
81
+
82
+ if (this.config.indexOf("pwm") === -1)
83
+ throw new Error("This line is not setup as PWM (see README)")
84
+
85
+ if (opt.period < 0.1 || opt.period > 1000000)
86
+ throw new Error("PWM period is out of range (100ns - 1s)")
87
+
88
+ // Normalize PWM values in ns
89
+ this.period = opt.period * 1000
90
+ this.dutyMin = Math.max(0, opt.dutyMin * 1000)
91
+ this.dutyMax = Math.min(this.period, opt.dutyMax * 1000)
92
+
93
+ // PWM channel: 0 or 1
94
+ this.pwmChannel = this.config.slice(-1)
95
+ this.pwmPath = "/sys/class/pwm/" + PWM_CHIP + "/"
96
+ this.pwmPathChannel = this.pwmPath + "pwm" + this.pwmChannel + "/"
97
+
98
+ log("pwm channel", this.pwmChannel, "normalized data (period, duty min, duty max):", this.period, this.dutyMin, this.dutyMax)
99
+
100
+ // Export channel
101
+ writeFileSync(this.pwmPath + "export", this.pwmChannel)
102
+ this.pwmExported = true
103
+ wait(opt.exportTime) // Customization required for RPi Zero
104
+
105
+ // Set period, reset duty and enable
106
+ try {
107
+ writeFileSync(this.pwmPathChannel + "period", String(this.period))
108
+ writeFileSync(this.pwmPathChannel + "duty_cycle", String(this.dutyMin))
109
+ writeFileSync(this.pwmPathChannel + "enable", "1")
110
+ this.pwmEnabled = true
111
+
112
+ } catch (err) {
113
+ warn("pwm start error:", err)
114
+ warn("consider increasing option exportTime for this device ")
115
+ this.pwmStop()
116
+ }
117
+
118
+ break
119
+ default:
120
+ throw new Error("undefined mode")
121
+ }
122
+
123
+ // Everything OK => Add this to instance list
124
+ RIO.instances.set(line, this)
125
+ }
126
+
127
+ /** ------------------------------------------------------------------
128
+ * @method close
129
+ * @description To close line and free resources
130
+ */
131
+ close() {
132
+ if (this.closed) {
133
+ warn("this instance is already closed")
134
+ return
135
+ }
136
+
137
+
138
+ // Stop monitoring if active
139
+ if (this.monitoring)
140
+ this.monitoringStop()
141
+
142
+ // Free C resources and reset handle
143
+ if (this.handle) {
144
+ ADDON.close(this.handle)
145
+ this.handle = null
146
+ }
147
+
148
+ // Stop PWM if required
149
+ if (this.mode === "pwm")
150
+ this.pwmStop()
151
+
152
+ // Delete from instance list et reset flag
153
+ RIO.instances.delete(this.line)
154
+ this.closed = true
155
+ log("line", this.line, "is closed")
156
+ }
157
+
158
+ /** ------------------------------------------------------------------
159
+ * @method write
160
+ * @description Write value to GPIO line
161
+ * @param {Number} value
162
+ */
163
+ write(value) {
164
+
165
+ if (this.closed)
166
+ throw new Error("GPIO handle has been closed")
167
+
168
+ if (this.mode !== "output")
169
+ throw new Error("Cannot write to this GPIO mode:", this.mode)
170
+
171
+ if ([0, 1].indexOf(value) === -1)
172
+ throw new Error("Value must be either 0 or 1")
173
+
174
+ ADDON.write(this.handle, value)
175
+ }
176
+
177
+ /** ------------------------------------------------------------------
178
+ * @method read
179
+ * @description Read value from GPIO line
180
+ * @return {Number} 0,1
181
+ */
182
+ read() {
183
+ if (this.closed)
184
+ throw new Error("GPIO handle has been closed")
185
+
186
+ if (this.mode !== "input")
187
+ throw new Error("Cannot read from this GPIO mode:", this.mode)
188
+
189
+ return ADDON.read(this.handle)
190
+ }
191
+
192
+ /** ------------------------------------------------------------------
193
+ * @method monitoringStart
194
+ * @description Monitor input GPIO line events (rising/falling)
195
+ * @param {Function} callback 0,1
196
+ * @param {String} edge
197
+ * @param {Number} bounce
198
+ */
199
+ monitoringStart(callback, edge = "both", bounce = 0) {
200
+ if (this.closed)
201
+ throw new Error("GPIO handle has been closed")
202
+
203
+ if (this.mode !== "input")
204
+ throw new Error("Cannot read from this GPIO mode:", this.mode)
205
+
206
+ if (this.monitoring)
207
+ throw new Error("Monitoring already started")
208
+
209
+ bounce < 0 ? bounce = 0 : false
210
+ bounce > 1000 ? bounce = 1000 : false
211
+ this.latestEvent = {
212
+ time: new Date(null),
213
+ edge: "none"
214
+ }
215
+
216
+ ADDON.startMonitoring(this.handle, value => {
217
+ const evt = value === 1 ? "rising" : "falling"
218
+ const now = new Date()
219
+ const delta = Math.max(1, now - this.latestEvent.time) // delta is always > 0ms
220
+
221
+ // Bounce detected
222
+ if (delta <= bounce && evt === this.latestEvent.edge) {
223
+ log("bounce detected on gpio", this.line, evt, delta + "ms")
224
+ }
225
+ // Callback of required events
226
+ else {
227
+ if (typeof callback === "function" && (edge === "both" || edge === evt)) {
228
+ callback(evt)
229
+ }
230
+ }
231
+ // Update latest event
232
+ this.latestEvent = {
233
+ time: now,
234
+ edge: evt
235
+ }
236
+ })
237
+ this.monitoring = true
238
+ }
239
+
240
+ /** ------------------------------------------------------------------
241
+ * @method monitoringStop
242
+ * @description Stop event monitoring
243
+ */
244
+ monitoringStop() {
245
+ if (this.closed)
246
+ return
247
+
248
+ if (this.monitoring) {
249
+ ADDON.stopMonitoring(this.handle)
250
+ this.monitoring = false
251
+ }
252
+ }
253
+
254
+ /** --------------------------------------------------------------
255
+ * @method pwmStop
256
+ * @description Stop PWM modulation
257
+ */
258
+ pwmStop() {
259
+
260
+ if (this.mode !== "pwm")
261
+ throw new Error("This line is not configured as PWM")
262
+
263
+ if (this.pwmEnabled) {
264
+ writeFileSync(this.pwmPathChannel + "enable", "0")
265
+ this.pwmEnabled = false
266
+ }
267
+
268
+ if (this.pwmExported) {
269
+ writeFileSync(this.pwmPath + "unexport", this.pwmChannel)
270
+ this.pwmExported = false
271
+ }
272
+ }
273
+
274
+ /** --------------------------------------------------------------
275
+ * @method pwmDuty
276
+ * @description Change PWM duty cycle
277
+ * @param {Number} percent 0 <= percent <= 100
278
+ */
279
+ pwmDuty(percent) {
280
+
281
+ if (this.mode !== "pwm")
282
+ throw new Error("This line is not configured as PWM")
283
+
284
+ if (!this.pwmEnabled || !this.pwmExported)
285
+ throw new Error("Duty of PWM line " + this.line + " cannot be updated")
286
+
287
+ if (percent < 0 || percent > 100 || typeof percent !== "number") {
288
+ throw new Error("Duty value (%) of PWM line" + this.line + " is not valid")
289
+ }
290
+
291
+ writeFileSync(this.pwmPathChannel + "duty_cycle", (this.dutyMin + ((percent / 100) * (this.dutyMax - this.dutyMin))).toString())
292
+ }
293
+
294
+ /** ------------------------------------------------------------------
295
+ * @method RIO.closeAll
296
+ * @description Static method to close all instances
297
+ */
298
+ static closeAll() {
299
+ for (const [line, instance] of RIO.instances) {
300
+ instance.close()
301
+ }
302
+ }
303
+ }
304
+
305
+ // -------------------------------------------------------------------
306
+ // EoF
307
+ // -------------------------------------------------------------------
package/history.md CHANGED
@@ -1,3 +1,10 @@
1
+
2
+ 2.0.0 - Nov 26, 2025
3
+ ===================
4
+ * New hybrid architecture Javascript + C addon to improve performances
5
+ * New API
6
+ * Fixes
7
+
1
8
  1.2.2 - Nov 10, 2025
2
9
  ===================
3
10
  * Change utility API function name from `Rio.log` to `Rio.logCfg`.
package/package.json CHANGED
@@ -1,19 +1,24 @@
1
1
  {
2
2
  "name": "rpi-io",
3
- "version": "1.2.3",
3
+ "version": "2.0.0",
4
4
  "description": "Nodejs module to control Raspberry Pi GPIO",
5
5
  "keywords": [
6
6
  "raspberry",
7
+ "raspberry-pi",
7
8
  "gpio",
8
9
  "libgpiod",
9
10
  "nodejs",
10
11
  "esm",
12
+ "napi",
11
13
  "output",
12
14
  "input",
13
15
  "monitor",
14
16
  "pwm",
17
+ "linux",
15
18
  "bookworm",
16
- "trixie"
19
+ "trixie",
20
+ "iot",
21
+ "embedded"
17
22
  ],
18
23
  "homepage": "https://github.com/gdorbes/rpi-io#readme",
19
24
  "bugs": {
@@ -25,14 +30,56 @@
25
30
  },
26
31
  "license": "MIT",
27
32
  "author": "Guillaume DORBES",
28
- "type": "module",
29
33
  "engines": {
30
34
  "npm": ">=10.0.0",
31
35
  "node": ">=23.0.0"
32
36
  },
33
- "main": "./esm/rio.mjs",
34
- "exports": "./esm/rio.mjs",
37
+ "main": "esm/main.mjs",
38
+ "exports": "./esm/main.mjs",
39
+ "type": "module",
35
40
  "scripts": {
36
- "test": "node ./scr/test.js"
41
+ "preinstall": "chmod +x script/*.sh 2>/dev/null || true",
42
+ "install": "node-gyp rebuild",
43
+ "postinstall": "echo 'Installation complete!'",
44
+ "clean": "node-gyp clean",
45
+ "configure": "node-gyp configure",
46
+ "//---- Shell scripts ----": "To check environment and libgpiod version",
47
+ "check": "bash script/check-env.sh",
48
+ "detect-version": "bash script/detect-gpiod-version.sh",
49
+ "//---- Javascript scripts ----": "Misc examples and tests",
50
+ "line-write": "node ./test/write.js",
51
+ "line-read": "node ./test/read.js",
52
+ "line-pwm-led": "node ./test/pwm-led.js",
53
+ "line-pwm-motor": "node ./test/pwm-motor.js",
54
+ "benchmark-write": "node ./test/benchmark-write.js",
55
+ "benchmark-read": "node ./test/benchmark-read.js",
56
+ "benchmark-pwm": "node ./test/benchmark-pwm.js",
57
+ "test-close": "node ./test/close-all.js",
58
+ "test-instance": "node ./test/duplicate-error.js",
59
+ "test-line": "node ./test/line-configuration.js"
60
+ },
61
+ "os": [
62
+ "linux"
63
+ ],
64
+ "cpu": [
65
+ "arm",
66
+ "arm64"
67
+ ],
68
+ "dependencies": {
69
+ "node-addon-api": "^7.0.0"
70
+ },
71
+ "devDependencies": {
72
+ "node-gyp": "^10.0.0"
73
+ },
74
+ "gypfile": true,
75
+ "binary": {
76
+ "napi_versions": [
77
+ 3,
78
+ 4,
79
+ 5,
80
+ 6,
81
+ 7,
82
+ 8
83
+ ]
37
84
  }
38
- }
85
+ }