rpi-io 1.2.4 → 2.0.1

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/esm/rio.mjs DELETED
@@ -1,675 +0,0 @@
1
- // -------------------------------------------------------------------
2
- // JS GPIO API: IN, OUT, PWM
3
- // Source: https://libgpiod.readthedocs.io/en/stable/gpio_tools.html
4
- // Details: man gpiodetect
5
- // -------------------------------------------------------------------
6
- import {spawn} from "child_process";
7
- import {writeFileSync, readFileSync} from "fs"
8
- import {exeFile, exeShell, wait} from "./utl.mjs"
9
- import {logCfg, log, trap} from "./dev.mjs"
10
-
11
- // -------------------------------------------------------------------
12
- // CLASS RIO AND FURTHER
13
- /** ------------------------------------------------------------------
14
- * @class RIO
15
- * @classDesc Raspberry IO
16
- * @param {Number} gpio
17
- */
18
- export class Rio {
19
-
20
- // ---------------------------------------------------------------
21
- // CONSTANTS AND VARIABLES AND FUNCTIONS
22
- // ---------------------------------------------------------------
23
- // Tested system environments: model, os, libgpiod
24
- static TESTED_MODELS = ["Raspberry Pi 5 Model B Rev 1.0"]
25
- static TESTED_OS = ["Bookworm", "Trixie"]
26
- static TESTED_LIBGPIOD = ["v1.6.3", "v2.2.1"]
27
-
28
- // Lists of supported GPIOs. Reference GPIO: pinout.xyz
29
- static RPi_GPIO_CORE = [4, 5, 6, 16, 17, 20, 21, 22, 23, 24, 25, 26, 27]
30
- static RPi_GPIO_PWM = [12, 13, 18, 19]
31
- static RPI_GPIOS = [...Rio.RPi_GPIO_CORE, ...Rio.RPi_GPIO_PWM]
32
- static RPI_CHIP = "gpiochip0"
33
- static PWM_CHIP = "pwmchip0"
34
- static TYPES = ["in", "out", "pwm"]
35
-
36
- // Map of instances
37
- static instances = new Map()
38
-
39
- // ---------------------------------------------------------------
40
- // GENERIC METHODS: constructor, disable
41
- /** --------------------------------------------------------------
42
- * @function constructor
43
- * @description
44
- * @param {Number} gpio
45
- * @param {String} type - {'in', 'out', 'pwm'}
46
- * @return {Object}
47
- */
48
- constructor(gpio, type) {
49
-
50
- this.gpio = gpio
51
- this.gpioStr = this.gpio.toString()
52
- this.gpioName = "GPIO" + this.gpioStr
53
- this.type = type
54
- this.os = Rio.os().name
55
- this.versionLib = Rio.version().substring(0, 4)
56
- // Monitoring properties
57
- this.watch = {
58
- edge: "stop",
59
- callback: false,
60
- process: null
61
- }
62
-
63
- // OS is not supported
64
- if (Rio.TESTED_OS.indexOf(this.os) === -1)
65
- throw new Error("This OS is not supported: " + this.os)
66
-
67
- // libgpiod is not supported
68
- const foundLib = Rio.TESTED_LIBGPIOD.find(testedVersion => {
69
- return this.versionLib === testedVersion.substring(0, 4)
70
- })
71
- if (!foundLib)
72
- throw new Error("This version of libgpiod is not supported: " + this.versionLib)
73
-
74
- // Unknown type
75
- if (Rio.TYPES.indexOf(type) === -1)
76
- throw new Error("This type is not supported: " + gpio)
77
-
78
- // gpio is not supported
79
- if (Rio.RPI_GPIOS.indexOf(gpio) === -1)
80
- throw new Error("This gpio is not supported: " + gpio)
81
-
82
- // gpio is already defined
83
- if (Rio.instances.has(gpio))
84
- throw new Error("This gpio is already defined: " + gpio)
85
-
86
- // this.read, this.write, monArgs depending on gpiolib version
87
- switch (this.versionLib) {
88
- case "v1.6":
89
- this.write = (value) => {
90
- exeFile("gpioset", [Rio.RPI_CHIP, this.gpioStr + "=" + value])
91
- }
92
- this.read = () => {
93
- return exeFile("gpioget", [Rio.RPI_CHIP, this.gpioStr])
94
- }
95
- break
96
- case "v2.2":
97
- this.write = (value, time = 0) => {
98
- exeFile("gpioset", ["-t" + time, this.gpioName + "=" + value])
99
- }
100
- this.read = () => {
101
- return exeFile("gpioget", ["--numeric", this.gpioName])
102
- }
103
- break
104
- }
105
-
106
- // pwm specific cases
107
- if (type === "pwm") {
108
-
109
- // gpio is not supported for PWM
110
- if (Rio.RPi_GPIO_PWM.indexOf(gpio) === -1) {
111
- throw new Error("PWM gpio must be one of " + Rio.RPi_GPIO_PWM)
112
- }
113
-
114
- // gpio is not setup for PWM
115
- const thisConfig = Rio.config(gpio)
116
- if (thisConfig.indexOf("pwm") === -1)
117
- throw new Error("This gpio is not setup as PWM (see README)")
118
-
119
- this.channel = thisConfig.slice(-1)
120
- this.pwm = Rio.PWM_CHIP
121
- this.pathPwm = "/sys/class/pwm/" + this.pwm + "/"
122
- this.pathChannel = this.pathPwm + "pwm" + this.channel + "/"
123
- }
124
- // Everything OK => Add this to instance list
125
- Rio.instances.set(gpio, this)
126
- log("new gpio instance created:", this.gpio)
127
- }
128
-
129
- /** --------------------------------------------------------------
130
- * @function disable
131
- * @description Remove instance from Rio.instances
132
- * Set this.gpio = -1
133
- */
134
- disable() {
135
- if (Rio.instances.has(this.gpio)) {
136
-
137
- // Stop watch process
138
- if (this.type === "in") {
139
- this.watch.edge = "stop"
140
- this.watch.callback = false
141
- if (this.watch.process !== null) {
142
- this.watch.process.kill("SIGTERM")
143
- log("monitoring of gpio", this.gpio, "has been stopped")
144
- }
145
- }
146
-
147
-
148
- // Disable signal then unexport channel
149
- if (this.type === "pwm") {
150
- writeFileSync(this.pathChannel + "enable", "0")
151
- this.enabled = false
152
- writeFileSync(this.pathPwm + "unexport", this.channel)
153
- this.exported = true
154
- }
155
-
156
- // Delete entry in instance Map
157
- Rio.instances.delete(this.gpio)
158
- log("gpio", this.gpio, "has been disabled")
159
- this.gpio = -1
160
- } else {
161
- log("gpio", this.gpio, "is already disabled")
162
- }
163
- }
164
-
165
- // ---------------------------------------------------------------
166
- // OUTPUT METHODS: set
167
- /** --------------------------------------------------------------
168
- * @function set
169
- * @description Set gpio to 1 or 0
170
- * Then invert when duration time is <> 0
171
- * @param {Number} value
172
- * @param {Number} duration ms
173
- */
174
- set(value = 0, duration = 0) {
175
-
176
- if (this.gpio === -1)
177
- throw new Error("This instance is disabled")
178
-
179
- if (this.type !== "out")
180
- throw new Error("Set is dedicated to 'out' instances")
181
-
182
- if ([0, 1].indexOf(value) === -1)
183
- throw new Error("Value must be either 0 or 1")
184
-
185
- if (duration === 0) {
186
- this.write(value)
187
- log("set gpio", this.gpio, "to", value)
188
- }
189
- // Optional timer
190
- else {
191
- this.write(value)
192
- log("set gpio", this.gpio, "to", value)
193
- setTimeout(() => {
194
- const valueStop = Math.abs(value - 1)
195
- this.write(valueStop)
196
- log("set gpio", this.gpio, "to", valueStop)
197
- }, duration)
198
- }
199
- }
200
-
201
- // ---------------------------------------------------------------
202
- // INPUT METHODS
203
- /** --------------------------------------------------------------
204
- * @function get
205
- * @description Get gpio value (0/1)
206
- */
207
- get() {
208
- if (this.gpio === -1)
209
- throw new Error("This instance is disabled")
210
-
211
- if (this.type !== "in")
212
- throw new Error("Get is dedicated to 'in' instances")
213
-
214
- const str = this.read()
215
-
216
- let result
217
- str.length === 0 ? result = -1 : result = parseInt(str.replace(/\r?\n|\r/, ""))
218
- log("get gpio", this.gpio, "value:", result)
219
- return result
220
- }
221
-
222
- /** --------------------------------------------------------------
223
- * @function monArgs
224
- * @description Utility to take care gpiomon arguments according to libgpiod version
225
- * @param {Array} gpios
226
- * @param {Object} opt
227
- * @return {Array}
228
- */
229
- #monArgs = (gpios, opt) => {
230
- let args = []
231
- switch (this.versionLib) {
232
- case "v1.6":
233
- args.push("-b") // line-buffered: output is flushed after every line
234
- args.push("--bias=" + opt.bias)
235
- args.push("--format=%o %e") // gpio, edge. Others: seconds(%s) and nanoseconds (%n)
236
- args.push(Rio.RPI_CHIP)
237
- return args.concat(gpios)
238
- case "v2.2":
239
- opt.bias === "disable" ? opt.bias = "disabled" : false
240
- gpios = gpios.map(gpio => "GPIO" + gpio)
241
- args.push("--bias")
242
- args.push(opt.bias)
243
- return args.concat(gpios)
244
- }
245
- }
246
-
247
- /** --------------------------------------------------------------
248
- * @function monData
249
- * @description Utility to take care of gpiomon stdout data
250
- * @param {String} data
251
- * @return {Object}
252
- */
253
- #monData = data => {
254
- switch (this.versionLib) {
255
- case "v1.6":
256
- const [gpioV1, edgeV1] = data.toString().trim().split(/\s+/)
257
- return {
258
- gpio: gpioV1,
259
- edge: edgeV1
260
- }
261
- case "v2.2":
262
- const [timeV2, edgeV2, gpioV2] = data.toString().trim().split(/\s+/)
263
- return {
264
- time: timeV2,
265
- gpio: gpioV2.substring(5, 7),
266
- edge: edgeV2
267
- }
268
- }
269
- }
270
- /** --------------------------------------------------------------
271
- * @function monCallback
272
- * @description Utility to trigger callback from gpiomon stdout data
273
- * @param {String} data
274
- */
275
- #monCallback = data => {
276
- const eventData = this.#monData(data)
277
- const now = new Date()
278
- const delta = Math.max(1, now - this.monitoredEvent.latest) // delta is always > 0ms
279
- const edge = eventData.edge === "1" ? "rising" : "falling"
280
-
281
- // Bounce detected
282
- if (delta <= this.bounceTime && edge === this.monitoredEvent.edge) {
283
- log("bounce detected on gpio", this.gpio, edge, delta + "ms")
284
- }
285
- // Trigger callback according to edge requirement
286
- else {
287
- if (this.watch.enabled.indexOf(edge) !== -1)
288
- this.watch.callback(edge, now)
289
- // log("monitored event on gpio", this.gpio, edge,)
290
- }
291
-
292
- // Update latest monitored event
293
- this.monitoredEvent.latest = now
294
- this.monitoredEvent.edge = edge
295
- }
296
-
297
- /** --------------------------------------------------------------
298
- * @function monitor
299
- * @description Wait for edge events
300
- * @param {String} edge {'stop', 'rising', 'falling' 'both'}
301
- * @param {Function} callback (edge, time)
302
- * @param {Object} opt - opt.bias {'disable','pull-up','pull-down'}
303
- * - opt.bounce 0-1000
304
- */
305
- monitor(edge, callback, opt) {
306
-
307
- // Default and clean parameters
308
- const defaultCallback = (edge, time) => {
309
- log("gpio", this.gpio, "event:", edge, time)
310
- }
311
- const defopt = {
312
- bias: "disable",
313
- bounce: 0
314
- }
315
-
316
- callback === undefined ? callback = defaultCallback : false
317
- opt = {...defopt, ...opt}
318
- opt.bounce < 0 ? opt.bounce = 0 : false
319
- opt.bounce > 1000 ? opt.bounce = 1000 : false
320
- this.bounceTime = opt.bounce
321
- this.monitoredEvent = {
322
- latest: new Date(null),
323
- edge: "none"
324
- }
325
-
326
- const biases = ["disable", "pull-up", "pull-down"]
327
- biases.indexOf(opt.bias) === -1 ? opt.bias = "disable" : false
328
-
329
- if (this.gpio === -1)
330
- throw new Error("This instance is disabled")
331
-
332
- if (this.type !== "in")
333
- throw new Error("Monitor is dedicated to 'in' instances")
334
-
335
- if (["stop", "rising", "falling", "both"].indexOf(edge) === -1)
336
- throw new Error("Edge " + edge + "is not supported")
337
-
338
- if (typeof callback !== "function")
339
- throw new Error("Callback is not a function")
340
-
341
- // First time the instance is monitored or has been stopped
342
- if (this.watch === "stop") {
343
-
344
- if (edge === "stop") {
345
- trap("cannot stop an unstarted monitoring on gpio", this.gpio)
346
- }
347
- // Edge is in ["rising", "falling", "both"]
348
- else {
349
- this.watch = {
350
- edge: edge,
351
- enabled: edge === "both" ? ["rising", "falling"] : [edge],
352
- callback: callback,
353
- process: spawn("gpiomon", this.#monArgs([this.gpio], opt))
354
- }
355
- this.watch.process.stdout.on("data", this.#monCallback)
356
- }
357
- }
358
- // Instance is already monitored
359
- else {
360
- // Stop: Kill process and delete monitored data
361
- if (edge === "stop") {
362
- this.watch.process !== null ? this.watch.process.kill("SIGTERM") : false
363
- this.watch.edge = "stop"
364
- this.watch.enabled = []
365
- this.watch.callback = false
366
- this.watch.process = null
367
- Rio.instances.set(this.gpio, this)
368
- log("monitoring of gpio", this.gpio, "has been stopped")
369
- }
370
-
371
- // Kill process, update edge and callback, define new process
372
- else {
373
- this.watch.process !== null ? this.watch.process.kill("SIGTERM") : false
374
- this.watch.edge = edge
375
- this.watch.enabled = edge === "both" ? ["rising", "falling"] : [edge]
376
- this.watch.callback = callback
377
- this.watch.process = spawn("gpiomon", this.#monArgs([this.gpio], opt))
378
- this.watch.process.stdout.on("data", this.#monCallback)
379
- }
380
- }
381
- }
382
-
383
- // ---------------------------------------------------------------
384
- // PWM METHODS
385
- // PLEASE NOTE - Commands to unlock a channel:
386
- // pwm channel 0: echo 0 > /sys/class/pwm/pwmchip0/unexport
387
- // pwm channel 0: echo 1 > /sys/class/pwm/pwmchip0/unexport
388
- /** --------------------------------------------------------------
389
- * @function pwmInit
390
- * @description Start PWM modulation
391
- * @param {Number} period in ns
392
- * @param {Number} duty cycle in ns
393
- * @param {Object} opt
394
- */
395
- pwmInit(period = 20000000, duty = 0, opt) {
396
-
397
- // Stop on wrong type
398
- if (this.type !== "pwm")
399
- throw new Error("This GPIO is not configured as PWM")
400
-
401
- // Stop disabled device
402
- if (this.gpio === -1)
403
- throw new Error("This instance is disabled")
404
-
405
- // Set default options
406
- const defopt = {
407
- dutyMin: 0,
408
- dutyMax: 1000000000, // 1s
409
- }
410
- opt = {...defopt, ...opt}
411
-
412
- this.period = period
413
- this.duty = duty
414
- this.dutyMin = Math.max(0, opt.dutyMin)
415
- this.dutyMax = Math.min(period, opt.dutyMax)
416
-
417
- // Check period
418
- if (this.period < 100 || this.period > 1000000000)
419
- throw new Error("PWM period is out of range (100ns - 1s")
420
-
421
- // Check duty cycle
422
- if (duty < this.dutyMin)
423
- throw new Error("PWM duty " + duty + " is lower than duty min " + this.dutyMin)
424
- if (duty > this.dutyMax)
425
- throw new Error("PWM duty" + duty + " is upper than duty max" + this.dutyMax)
426
-
427
- log("channel", this.channel, "normalized PWM data (period, duty, duty min, duty max):", this.period, this.duty, this.dutyMin, this.dutyMax)
428
-
429
- // Export channel, then set period, duty cycle and enable
430
- writeFileSync(this.pathPwm + "export", this.channel)
431
- this.exported = true
432
-
433
- try {
434
- wait(100)
435
- writeFileSync(this.pathChannel + "period", String(this.period))
436
- writeFileSync(this.pathChannel + "duty_cycle", String(this.duty))
437
- writeFileSync(this.pathChannel + "enable", "1")
438
- this.enabled = true
439
-
440
- } catch (err) {
441
- trap("pwm start error:", err)
442
- this.pwmStop()
443
- }
444
- }
445
-
446
- /** --------------------------------------------------------------
447
- * @function pwmStop
448
- * @description Stop PWM modulation
449
- */
450
- pwmStop() {
451
-
452
- if (this.type !== "pwm")
453
- throw new Error("This GPIO is not configured as PWM")
454
-
455
- // Disable signal then unexport channel
456
- writeFileSync(this.pathChannel + "enable", "0")
457
- this.enabled = false
458
- writeFileSync(this.pathPwm + "unexport", this.channel)
459
- this.exported = true
460
- }
461
-
462
- /** --------------------------------------------------------------
463
- * @function pwmDuty
464
- * @description Change PWM duty cycle
465
- * @parameter {Number} time
466
- */
467
- pwmDuty(time) {
468
-
469
- // Not PWM
470
- if (this.type !== "pwm")
471
- throw new Error("This GPIO is not configured as PWM")
472
-
473
- // Not initialized
474
- if (!this.enabled || !this.exported) {
475
- trap("PWM duty cycle cannot be update if port is not exported and enabled")
476
- return
477
- }
478
-
479
- // Out of range
480
- this.duty = time
481
- if (typeof this.duty !== "number" || this.duty < this.dutyMin) {
482
- this.duty = this.dutyMin
483
- trap("duty cycle forced to", this.dutyMin)
484
- }
485
- if (typeof this.duty !== "number" || this.duty > this.dutyMax) {
486
- this.duty = this.dutyMax
487
- trap("duty cycle forced to", this.dutyMax)
488
- }
489
-
490
- writeFileSync(this.pathChannel + "duty_cycle", this.duty.toString())
491
- }
492
- }
493
-
494
- // -------------------------------------------------------------------
495
- // STATIC METHODS
496
- /** ------------------------------------------------------------------
497
- * @function Rio.config
498
- * @description Return GPIO configuration
499
- * @param {Number} gpio
500
- * @return {String}
501
- */
502
- Rio.config = gpio => {
503
- const a = exeShell("pinctrl get " + gpio).trim().split(/\s+/)
504
- return a[a.length - 1].toLowerCase()
505
- }
506
- /** ------------------------------------------------------------------
507
- * @function Rio.configs
508
- * @description Return GPIO configuration for all supported GPIO
509
- * @return {Object}
510
- */
511
- Rio.configs = () => {
512
- let configs = {}
513
- Rio.RPI_GPIOS.forEach(gpio => {
514
- configs[gpio] = Rio.config(gpio)
515
- })
516
- log("gpio configs:", configs)
517
- return configs
518
- }
519
-
520
- /** ------------------------------------------------------------------
521
- * @function Rio.info
522
- * @description Exec gpioinfo
523
- * @return {Object}
524
- */
525
- Rio.info = () => {
526
- return exeFile("gpioinfo", [Rio.RPI_CHIP])
527
- }
528
-
529
- /** ------------------------------------------------------------------
530
- * @function Rio.detect
531
- * @description Exec gpiodetect
532
- * @return {Object}
533
- */
534
- Rio.detect = () => {
535
- return exeFile("gpiodetect")
536
- }
537
-
538
- /** ------------------------------------------------------------------
539
- * @function Rio.version
540
- * @description Return libgpiod version
541
- * @return {Object}
542
- */
543
- Rio.version = () => {
544
- return exeFile("gpiodetect", ["-v"]).split("\n").shift().substring(22).trim()
545
- }
546
-
547
- /** ------------------------------------------------------------------
548
- * @function Rio.stopMonitoring
549
- * @description Stop monitoring of all inputs
550
- */
551
- Rio.stopMonitoring = () => {
552
- Rio.instances.forEach((that) => {
553
- that.watch.edge = "stop"
554
- that.watch.callback = false
555
- if (that.watch.process !== null) {
556
- that.watch.process.kill("SIGTERM")
557
- log("monitoring of gpio", that.gpio, "has been stopped")
558
- }
559
- })
560
- }
561
-
562
- /** ------------------------------------------------------------------
563
- * @function Rio.disableAll
564
- * @description Disable all instances
565
- */
566
- Rio.disableAll = () => {
567
- Rio.instances.forEach((that) => {
568
- that.disable()
569
- })
570
- }
571
-
572
- /** ------------------------------------------------------------------
573
- * @function Rio.cpuInfo
574
- * @description Read '/proc/cpuinfo' and store it for further call
575
- * @return {String}
576
- */
577
- Rio.cpuInfo = () => {
578
- if (typeof Rio.cpuInfo.value === "undefined") {
579
- try {
580
- Rio.cpuInfo.value = readFileSync("/proc/cpuinfo", "utf8")
581
- } catch (err) {
582
- trap("catched error opening '/proc/cpuinfo:", err)
583
- Rio.cpuInfo.value = ""
584
- }
585
- }
586
- return Rio.cpuInfo.value
587
- }
588
-
589
- /** ------------------------------------------------------------------
590
- * @function Rio.os
591
- * @description Return OS ID and name
592
- * @return {Object} {id: '12', name: 'Bookworm'}
593
- */
594
- Rio.os = () => {
595
- const strOs = exeShell("cat /etc/os-release")
596
- if (strOs.length > 0) {
597
- const lines = strOs.split("\n")
598
- const info = {}
599
- lines.forEach(line => {
600
- const [key, value] = line.split("=")
601
- if (key && value) {
602
- info[key] = value.replace(/"/g, "");
603
- }
604
- })
605
- let res = {
606
- id: info["VERSION_ID"],
607
- name: info["VERSION_CODENAME"]
608
- }
609
- res.name = res.name.substring(0, 1).toUpperCase() + res.name.substring(1);
610
- return res
611
- }
612
- }
613
- /** ------------------------------------------------------------------
614
- * @function Rio.isSystemSupported
615
- * @description Check if all hardware and software requirements are
616
- * satisfied to run the RPI-IO module
617
- * @return {Boolean}
618
- */
619
- Rio.isSystemSupported = () => {
620
-
621
- // Test hardware
622
- if (/Raspberry Pi/i.test(Rio.cpuInfo()) === false) {
623
- trap("not a Raspberry Pi hardware")
624
- return false
625
- }
626
- log("👍 Hardware is Raspberry Pi")
627
-
628
- // Test model
629
- const line = Rio.cpuInfo().split("\n").find(str => str.indexOf("Model") !== -1)
630
- const model = line.split(":")[1].trim()
631
- if (Rio.TESTED_MODELS.indexOf(model) === -1) {
632
- trap("model not tested:", model)
633
- return false
634
- }
635
- log("👍 ", model, "is supported")
636
-
637
- // Test OS
638
- const thisOs = Rio.os()
639
- if (Rio.TESTED_OS.indexOf(thisOs.name) === -1) {
640
- trap("OS not tested:", thisOs.name, thisOs.id)
641
- return false
642
- }
643
- log("👍 ", thisOs.name, "OS is supported")
644
-
645
- // Test libgpiod version x.y
646
- const gpiodVersion = Rio.version().substring(0, 4)
647
- const foundLib = Rio.TESTED_LIBGPIOD.find(testedVersion => {
648
- return gpiodVersion === testedVersion.substring(0, 4)
649
- })
650
- if (!foundLib) {
651
- trap("libgpiod version not tested:", gpiodVersion)
652
- return false
653
- }
654
- log("👍 libgpiod version", gpiodVersion + ".x", "is supported")
655
- log("👍 Systems requirements for rpi-io are met")
656
- return true
657
- }
658
- /** ------------------------------------------------------------------
659
- * @function Rio.logCfg
660
- */
661
- Rio.logCfg = logCfg
662
-
663
- /** ------------------------------------------------------------------
664
- * @function Rio.log
665
- */
666
- Rio.log = log
667
-
668
- /** ------------------------------------------------------------------
669
- * @function Rio.trap
670
- */
671
- Rio.trap = trap
672
-
673
- // -------------------------------------------------------------------
674
- // EoF
675
- // -------------------------------------------------------------------