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/esm/rio.mjs DELETED
@@ -1,647 +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
- this.gpio = -1
137
- this.watch.edge = "stop"
138
- this.watch.callback = false
139
- this.watch.process !== null ? this.watch.process.kill("SIGTERM") : false
140
- Rio.instances.delete(this.gpio)
141
- log("gpio", this.gpio, "has been disabled")
142
- } else {
143
- log("gpio", this.gpio, "is already disabled")
144
- }
145
- }
146
-
147
- // ---------------------------------------------------------------
148
- // OUTPUT METHODS: set
149
- /** --------------------------------------------------------------
150
- * @function set
151
- * @description Set gpio to 1 or 0
152
- * Then invert when duration time is <> 0
153
- * @param {Number} value
154
- * @param {Number} duration ms
155
- */
156
- set(value = 0, duration = 0) {
157
-
158
- if (this.gpio === -1)
159
- throw new Error("This instance is disabled")
160
-
161
- if (this.type !== "out")
162
- throw new Error("Set is dedicated to 'out' instances")
163
-
164
- if ([0, 1].indexOf(value) === -1)
165
- throw new Error("Value must be either 0 or 1")
166
-
167
- if (duration === 0) {
168
- this.write(value)
169
- log("set gpio", this.gpio, "to", value)
170
- }
171
- // Optional timer
172
- else {
173
- this.write(value)
174
- log("set gpio", this.gpio, "to", value)
175
- setTimeout(() => {
176
- const valueStop = Math.abs(value - 1)
177
- this.write(valueStop)
178
- log("set gpio", this.gpio, "to", valueStop)
179
- }, duration)
180
- }
181
- }
182
-
183
- // ---------------------------------------------------------------
184
- // INPUT METHODS
185
- /** --------------------------------------------------------------
186
- * @function get
187
- * @description Get gpio value (0/1)
188
- */
189
- get() {
190
- if (this.gpio === -1)
191
- throw new Error("This instance is disabled")
192
-
193
- if (this.type !== "in")
194
- throw new Error("Get is dedicated to 'in' instances")
195
-
196
- const str = this.read()
197
-
198
- let result
199
- str.length === 0 ? result = -1 : result = parseInt(str.replace(/\r?\n|\r/, ""))
200
- log("get gpio", this.gpio, "value:", result)
201
- return result
202
- }
203
-
204
- /** --------------------------------------------------------------
205
- * @function monArgs
206
- * @description Utility to take care gpiomon arguments according to libgpiod version
207
- * @param {Array} gpios
208
- * @param {Object} opt
209
- * @return {Array}
210
- */
211
- #monArgs = (gpios, opt) => {
212
- let args = []
213
- switch (this.versionLib) {
214
- case "v1.6":
215
- args.push("-b") // line-buffered: output is flushed after every line
216
- args.push("--bias=" + opt.bias)
217
- args.push("--format=%o %e") // gpio, edge. Others: seconds(%s) and nanoseconds (%n)
218
- args.push(Rio.RPI_CHIP)
219
- return args.concat(gpios)
220
- case "v2.2":
221
- opt.bias === "disable" ? opt.bias = "disabled" : false
222
- gpios = gpios.map(gpio => "GPIO" + gpio)
223
- args.push("--bias")
224
- args.push(opt.bias)
225
- return args.concat(gpios)
226
- }
227
- }
228
-
229
- /** --------------------------------------------------------------
230
- * @function monData
231
- * @description Utility to take care of gpiomon stdout data
232
- * @param {String} data
233
- * @return {Object}
234
- */
235
- #monData = data => {
236
- switch (this.versionLib) {
237
- case "v1.6":
238
- const [gpioV1, edgeV1] = data.toString().trim().split(/\s+/)
239
- return {
240
- gpio: gpioV1,
241
- edge: edgeV1
242
- }
243
- case "v2.2":
244
- const [timeV2, edgeV2, gpioV2] = data.toString().trim().split(/\s+/)
245
- return {
246
- time: timeV2,
247
- gpio: gpioV2.substring(5, 7),
248
- edge: edgeV2
249
- }
250
- }
251
- }
252
- /** --------------------------------------------------------------
253
- * @function monCallback
254
- * @description Utility to trigger callback from gpiomon stdout data
255
- * @param {String} data
256
- */
257
- #monCallback = data => {
258
- const eventData = this.#monData(data)
259
- const now = new Date()
260
- const delta = Math.max(1, now - this.monitoredEvent.latest) // delta is always > 0ms
261
- const edge = eventData.edge === "1" ? "rising" : "falling"
262
-
263
- // Bounce detected
264
- if (delta <= this.bounceTime && edge === this.monitoredEvent.edge) {
265
- log("bounce detected on gpio", this.gpio, edge, delta + "ms")
266
- }
267
- // Trigger callback according to edge requirement
268
- else {
269
- if (this.watch.enabled.indexOf(edge) !== -1)
270
- this.watch.callback(edge, now)
271
- // log("monitored event on gpio", this.gpio, edge,)
272
- }
273
-
274
- // Update latest monitored event
275
- this.monitoredEvent.latest = now
276
- this.monitoredEvent.edge = edge
277
- }
278
-
279
- /** --------------------------------------------------------------
280
- * @function monitor
281
- * @description Wait for edge events
282
- * @param {String} edge {'stop', 'rising', 'falling' 'both'}
283
- * @param {Function} callback (edge, time)
284
- * @param {Object} opt - opt.bias {'disable','pull-up','pull-down'}
285
- * - opt.bounce 0-1000
286
- */
287
- monitor(edge, callback, opt) {
288
-
289
- // Default and clean parameters
290
- const defaultCallback = (edge, time) => {
291
- log("gpio", this.gpio, "event:", edge, time)
292
- }
293
- const defopt = {
294
- bias: "disable",
295
- bounce: 0
296
- }
297
-
298
- callback === undefined ? callback = defaultCallback : false
299
- opt = {...defopt, ...opt}
300
- opt.bounce < 0 ? opt.bounce = 0 : false
301
- opt.bounce > 1000 ? opt.bounce = 1000 : false
302
- this.bounceTime = opt.bounce
303
- this.monitoredEvent = {
304
- latest: new Date(null),
305
- edge: "none"
306
- }
307
-
308
- const biases = ["disable", "pull-up", "pull-down"]
309
- biases.indexOf(opt.bias) === -1 ? opt.bias = "disable" : false
310
-
311
- if (this.gpio === -1)
312
- throw new Error("This instance is disabled")
313
-
314
- if (this.type !== "in")
315
- throw new Error("Monitor is dedicated to 'in' instances")
316
-
317
- if (["stop", "rising", "falling", "both"].indexOf(edge) === -1)
318
- throw new Error("Edge " + edge + "is not supported")
319
-
320
- if (typeof callback !== "function")
321
- throw new Error("Callback is not a function")
322
-
323
- // First time the instance is monitored or has been stopped
324
- if (this.watch === "stop") {
325
-
326
- if (edge === "stop") {
327
- trap("cannot stop an unstarted monitoring on gpio", this.gpio)
328
- }
329
- // Edge is in ["rising", "falling", "both"]
330
- else {
331
- this.watch = {
332
- edge: edge,
333
- enabled: edge === "both" ? ["rising", "falling"] : [edge],
334
- callback: callback,
335
- process: spawn("gpiomon", this.#monArgs([this.gpio], opt))
336
- }
337
- this.watch.process.stdout.on("data", this.#monCallback)
338
- }
339
- }
340
- // Instance is already monitored
341
- else {
342
- // Stop: Kill process and delete monitored data
343
- if (edge === "stop") {
344
- this.watch.process !== null ? this.watch.process.kill("SIGTERM") : false
345
- this.watch.edge = "stop"
346
- this.watch.enabled = []
347
- this.watch.callback = false
348
- this.watch.process = null
349
- Rio.instances.set(this.gpio, this)
350
- log("monitoring of gpio", this.gpio, "has been stopped")
351
- }
352
-
353
- // Kill process, update edge and callback, define new process
354
- else {
355
- this.watch.process !== null ? this.watch.process.kill("SIGTERM") : false
356
- this.watch.edge = edge
357
- this.watch.enabled = edge === "both" ? ["rising", "falling"] : [edge]
358
- this.watch.callback = callback
359
- this.watch.process = spawn("gpiomon", this.#monArgs([this.gpio], opt))
360
- this.watch.process.stdout.on("data", this.#monCallback)
361
- }
362
- }
363
- }
364
-
365
- // ---------------------------------------------------------------
366
- // PWM METHODS
367
- // PLEASE NOTE - Commands to unlock a channel:
368
- // pwm channel 0: echo 0 > /sys/class/pwm/pwmchip0/unexport
369
- // pwm channel 0: echo 1 > /sys/class/pwm/pwmchip0/unexport
370
- /** --------------------------------------------------------------
371
- * @function pwmInit
372
- * @description Start PWM modulation
373
- * @param {Number} period in ns
374
- * @param {Number} duty cycle in ns
375
- * @param {Object} opt
376
- */
377
- pwmInit(period = 20000000, duty = 0, opt) {
378
-
379
- // Stop on wrong type
380
- if (this.type !== "pwm")
381
- throw new Error("This GPIO is not configured as PWM")
382
-
383
- // Stop disabled device
384
- if (this.gpio === -1)
385
- throw new Error("This instance is disabled")
386
-
387
- // Set default options
388
- const defopt = {
389
- dutyMin: 0,
390
- dutyMax: 1000000000, // 1s
391
- }
392
- opt = {...defopt, ...opt}
393
-
394
- this.period = period
395
- this.duty = duty
396
- this.dutyMin = Math.max(0, opt.dutyMin)
397
- this.dutyMax = Math.min(period, opt.dutyMax)
398
-
399
- // Check period
400
- if (this.period < 100 || this.period > 1000000000)
401
- throw new Error("PWM period is out of range (100ns - 1s")
402
-
403
- // Check duty cycle
404
- if (duty < this.dutyMin)
405
- throw new Error("PWM duty " + duty + " is lower than duty min " + this.dutyMin)
406
- if (duty > this.dutyMax)
407
- throw new Error("PWM duty" + duty + " is upper than duty max" + this.dutyMax)
408
-
409
- log("channel", this.channel, "normalized PWM data (period, duty, duty min, duty max):", this.period, this.duty, this.dutyMin, this.dutyMax)
410
-
411
- // Export channel, then set period, duty cycle and enable
412
- writeFileSync(this.pathPwm + "export", this.channel)
413
- this.exported = true
414
-
415
- try {
416
- wait(100)
417
- writeFileSync(this.pathChannel + "period", String(this.period))
418
- writeFileSync(this.pathChannel + "duty_cycle", String(this.duty))
419
- writeFileSync(this.pathChannel + "enable", "1")
420
- this.enabled = true
421
-
422
- } catch (err) {
423
- trap("pwm start error:", err)
424
- this.pwmStop()
425
- }
426
- }
427
-
428
- /** --------------------------------------------------------------
429
- * @function pwmStop
430
- * @description Stop PWM modulation
431
- */
432
- pwmStop() {
433
-
434
- if (this.type !== "pwm")
435
- throw new Error("This GPIO is not configured as PWM")
436
-
437
- // Disable signal then unexport channel
438
- writeFileSync(this.pathChannel + "enable", "0")
439
- this.enabled = false
440
- writeFileSync(this.pathPwm + "unexport", this.channel)
441
- this.exported = true
442
- }
443
-
444
- /** --------------------------------------------------------------
445
- * @function pwmDuty
446
- * @description Change PWM duty cycle
447
- * @parameter {Number} time
448
- */
449
- pwmDuty(time) {
450
-
451
- // Not PWM
452
- if (this.type !== "pwm")
453
- throw new Error("This GPIO is not configured as PWM")
454
-
455
- // Not initialized
456
- if (!this.enabled || !this.exported) {
457
- trap("PWM duty cycle cannot be update if port is not exported and enabled")
458
- return
459
- }
460
-
461
- // Out of range
462
- this.duty = time
463
- if (typeof this.duty !== "number" || this.duty < this.dutyMin) {
464
- this.duty = this.dutyMin
465
- trap("duty cycle forced to", this.dutyMin)
466
- }
467
- if (typeof this.duty !== "number" || this.duty > this.dutyMax) {
468
- this.duty = this.dutyMax
469
- trap("duty cycle forced to", this.dutyMax)
470
- }
471
-
472
- writeFileSync(this.pathChannel + "duty_cycle", this.duty.toString())
473
- }
474
- }
475
-
476
- // -------------------------------------------------------------------
477
- // STATIC METHODS
478
- /** ------------------------------------------------------------------
479
- * @function Rio.config
480
- * @description Return GPIO configuration
481
- * @param {Number} gpio
482
- * @return {String}
483
- */
484
- Rio.config = gpio => {
485
- const a = exeShell("pinctrl get " + gpio).trim().split(/\s+/)
486
- return a[a.length - 1].toLowerCase()
487
- }
488
- /** ------------------------------------------------------------------
489
- * @function Rio.configs
490
- * @description Return GPIO configuration for all supported GPIO
491
- * @return {Object}
492
- */
493
- Rio.configs = () => {
494
- let configs = {}
495
- Rio.RPI_GPIOS.forEach(gpio => {
496
- configs[gpio] = Rio.config(gpio)
497
- })
498
- log("gpio configs:", configs)
499
- return configs
500
- }
501
-
502
- /** ------------------------------------------------------------------
503
- * @function Rio.info
504
- * @description Exec gpioinfo
505
- * @return {Object}
506
- */
507
- Rio.info = () => {
508
- return exeFile("gpioinfo", [Rio.RPI_CHIP])
509
- }
510
-
511
- /** ------------------------------------------------------------------
512
- * @function Rio.detect
513
- * @description Exec gpiodetect
514
- * @return {Object}
515
- */
516
- Rio.detect = () => {
517
- return exeFile("gpiodetect")
518
- }
519
-
520
- /** ------------------------------------------------------------------
521
- * @function Rio.version
522
- * @description Return libgpiod version
523
- * @return {Object}
524
- */
525
- Rio.version = () => {
526
- return exeFile("gpiodetect", ["-v"]).split("\n").shift().substring(22).trim()
527
- }
528
-
529
- /** ------------------------------------------------------------------
530
- * @function Rio.stopMonitoring
531
- * @description Stop monitoring of all inputs
532
- */
533
- Rio.stopMonitoring = () => {
534
- Rio.instances.forEach((that) => {
535
- if (that.watch.process !== null) {
536
- that.watch.process.kill("SIGTERM")
537
- that.watch.edge = "stop"
538
- that.watch.callback = false
539
- that.watch.process = null
540
- log("monitoring of gpio", that.gpio, "has been stopped")
541
- }
542
- })
543
- }
544
-
545
- /** ------------------------------------------------------------------
546
- * @function Rio.cpuInfo
547
- * @description Read '/proc/cpuinfo' and store it for further call
548
- * @return {String}
549
- */
550
- Rio.cpuInfo = () => {
551
- if (typeof Rio.cpuInfo.value === "undefined") {
552
- try {
553
- Rio.cpuInfo.value = readFileSync("/proc/cpuinfo", "utf8")
554
- } catch (err) {
555
- trap("catched error opening '/proc/cpuinfo:", err)
556
- Rio.cpuInfo.value = ""
557
- }
558
- }
559
- return Rio.cpuInfo.value
560
- }
561
-
562
- /** ------------------------------------------------------------------
563
- * @function Rio.os
564
- * @description Return OS ID and name
565
- * @return {Object} {id: '12', name: 'Bookworm'}
566
- */
567
- Rio.os = () => {
568
- const strOs = exeShell("cat /etc/os-release")
569
- if (strOs.length > 0) {
570
- const lines = strOs.split("\n")
571
- const info = {}
572
- lines.forEach(line => {
573
- const [key, value] = line.split("=")
574
- if (key && value) {
575
- info[key] = value.replace(/"/g, "");
576
- }
577
- })
578
- let res = {
579
- id: info["VERSION_ID"],
580
- name: info["VERSION_CODENAME"]
581
- }
582
- res.name = res.name.substring(0, 1).toUpperCase() + res.name.substring(1);
583
- return res
584
- }
585
- }
586
- /** ------------------------------------------------------------------
587
- * @function Rio.isSystemSupported
588
- * @description Check if all hardware and software requirements are
589
- * satisfied to run the RPI-IO module
590
- * @return {Boolean}
591
- */
592
- Rio.isSystemSupported = () => {
593
-
594
- // Test hardware
595
- if (/Raspberry Pi/i.test(Rio.cpuInfo()) === false) {
596
- trap("not a Raspberry Pi hardware")
597
- return false
598
- }
599
- log("👍 Hardware is Raspberry Pi")
600
-
601
- // Test model
602
- const line = Rio.cpuInfo().split("\n").find(str => str.indexOf("Model") !== -1)
603
- const model = line.split(":")[1].trim()
604
- if (Rio.TESTED_MODELS.indexOf(model) === -1) {
605
- trap("model not tested:", model)
606
- return false
607
- }
608
- log("👍 ", model, "is supported")
609
-
610
- // Test OS
611
- const thisOs = Rio.os()
612
- if (Rio.TESTED_OS.indexOf(thisOs.name) === -1) {
613
- trap("OS not tested:", thisOs.name, thisOs.id)
614
- return false
615
- }
616
- log("👍 ", thisOs.name, "OS is supported")
617
-
618
- // Test libgpiod version x.y
619
- const gpiodVersion = Rio.version().substring(0, 4)
620
- const foundLib = Rio.TESTED_LIBGPIOD.find(testedVersion => {
621
- return gpiodVersion === testedVersion.substring(0, 4)
622
- })
623
- if (!foundLib) {
624
- trap("libgpiod version not tested:", gpiodVersion)
625
- return false
626
- }
627
- log("👍 libgpiod version", gpiodVersion + ".x", "is supported")
628
- log("👍 Systems requirements for rpi-io are met")
629
- return true
630
- }
631
- /** ------------------------------------------------------------------
632
- * @function Rio.logCfg
633
- */
634
- Rio.logCfg = logCfg
635
-
636
- /** ------------------------------------------------------------------
637
- * @function Rio.log
638
- */
639
- Rio.log = log
640
-
641
- /** ------------------------------------------------------------------
642
- * @function Rio.trap
643
- */
644
- Rio.trap = trap
645
- // -------------------------------------------------------------------
646
- // EoF
647
- // -------------------------------------------------------------------