rpi-io 1.0.0 → 1.1.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/LICENSE +21 -0
- package/README.md +429 -0
- package/esm/dev.mjs +59 -0
- package/esm/rio.mjs +553 -0
- package/esm/utl.mjs +100 -0
- package/history.md +8 -0
- package/package.json +12 -5
- package/scr/test.js +162 -0
- package/rpi-io-1.0.0.tgz +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Guillaume DORBES
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
# rpi-io
|
|
2
|
+
    
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
**rpi-io** is a lite [ESM](https://nodejs.org/api/esm.html#modules-ecmascript-modules) module for **Node.js** to control **Raspberry Pi** GPIO: access (in, out), input event detection and [PWM](https://en.wikipedia.org/wiki/Pulse-width_modulation) peripheral control.
|
|
6
|
+
|
|
7
|
+
**rpi-io** supports only recent versions of Raspberry and related OS and middlewares :
|
|
8
|
+
|
|
9
|
+
- Raspberry Pi models: *RPi 5,* *RPi 4B* and *RPi Zero 2.*
|
|
10
|
+
- Raspberry Pi OS (64-bit): Debian *Bookworm* and *Trixie*.
|
|
11
|
+
- Middlewares (installed by default with OS distribution):
|
|
12
|
+
- Input/Output - [libgpiod](https://libgpiod.readthedocs.io/en/stable/) v1.6.3 (*Bookworm*) and v2.2.1 (*Trixie*)
|
|
13
|
+
- PWM - [sysfs](https://en.wikipedia.org/wiki/Sysfs) interface.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
````shell
|
|
18
|
+
npm install rpi-io
|
|
19
|
+
````
|
|
20
|
+
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
By default rpi-io does not require specific configuration.
|
|
24
|
+
|
|
25
|
+
If you want to use [hardware-base PWM](https://pinout.xyz/pinout/pwm) peripherals, some configuration is required:
|
|
26
|
+
|
|
27
|
+
- Open the file `/boot/firmware/config.txt` and add the required [dtoverlay](https://www.raspberrypi.com/documentation/computers/configuration.html#part3.1) configuration for PWM as showned in the examples below.
|
|
28
|
+
|
|
29
|
+
```shell
|
|
30
|
+
# Examples of PWM configuration to add to/boot/firmware/config.txt
|
|
31
|
+
# Default one-channel config: GPIO 18 as channel 0
|
|
32
|
+
[all]
|
|
33
|
+
dtoverlay=pwm
|
|
34
|
+
# Default two-channel config: GPIO 18 as channel 0 and GPIO 19 as channel 1
|
|
35
|
+
[all]
|
|
36
|
+
dtoverlay=pwm-2chan
|
|
37
|
+
# Custom two-channel config: GPIO 12 as channel 0 and GPIO 13 as channel 1
|
|
38
|
+
[all]
|
|
39
|
+
dtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- Reboot the Raspberry Pi.
|
|
43
|
+
- Test the PWM configuration.
|
|
44
|
+
|
|
45
|
+
```shell
|
|
46
|
+
# Testing PWM configuration after reboot
|
|
47
|
+
pinctrl get 12
|
|
48
|
+
# 12: a0 pd | lo // GPIO12 = PWM0_CHAN0
|
|
49
|
+
pinctrl get 13
|
|
50
|
+
# 13: a0 pd | lo // GPIO13 = PWM0_CHAN1
|
|
51
|
+
pinctrl get 18
|
|
52
|
+
# 18: no pd | -- // GPIO18 = none
|
|
53
|
+
pinctrl get 19
|
|
54
|
+
# 19: no pd | -- // GPIO19 = none
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
PLEASE NOTE: In all this document, GPIOs numbers are the BCM ones as defined in https://pinout.xyz/.
|
|
60
|
+
|
|
61
|
+
### *OUT* operations
|
|
62
|
+
|
|
63
|
+
#### LED control
|
|
64
|
+
|
|
65
|
+

|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
// Import rpi-io module
|
|
69
|
+
import {Rio} from "rpi-io";
|
|
70
|
+
// Define instance for OUT operation on some GPIO
|
|
71
|
+
const led = new Rio(17, "out");
|
|
72
|
+
// Turn the led on
|
|
73
|
+
led.set(1);
|
|
74
|
+
// Turn the led off
|
|
75
|
+
led.set(0);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### Active buzzer beep
|
|
79
|
+
|
|
80
|
+
`set` method has an optional *time* parameter to invert the command after some delay (ms). It may be useful to beep a buzzer.
|
|
81
|
+
|
|
82
|
+

|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
// Import rpi-io module
|
|
86
|
+
import {Rio} from "rpi-io";
|
|
87
|
+
// Define instance for OUT operation on some GPIO
|
|
88
|
+
const buzzer = new Rio(23, "out");
|
|
89
|
+
// Turn the buzzer on for 3 seconds
|
|
90
|
+
buzzer.set(1, 3000);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### *IN* operations
|
|
94
|
+
|
|
95
|
+
#### Getting button status and listening to input events
|
|
96
|
+
|
|
97
|
+

|
|
98
|
+
|
|
99
|
+
```javascript
|
|
100
|
+
// Import rpi-io module
|
|
101
|
+
import {Rio} from "rpi-io";
|
|
102
|
+
|
|
103
|
+
// Define instance for IN operation on some GPIO
|
|
104
|
+
const button = new Rio(18, "in");
|
|
105
|
+
|
|
106
|
+
// Get the instant value of the button
|
|
107
|
+
button.get(); // Return 0 (opened) or 1 (closed)
|
|
108
|
+
|
|
109
|
+
// Monitor button events ("rising, "falling" or "both")
|
|
110
|
+
// and receive results in callback function
|
|
111
|
+
button.monitor("both", (event) => {
|
|
112
|
+
// event.edge = "rising" or "falling"
|
|
113
|
+
// event.time = timestamp
|
|
114
|
+
console.log("event:", event.edge, event.time)
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### PWM operations with servo motor in hardware mode
|
|
119
|
+
|
|
120
|
+
REMINDER: PWM peripherals used in hardware mode need some specific [configuration](#Configuration).
|
|
121
|
+
|
|
122
|
+

|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
// Import rpi-io module
|
|
127
|
+
import {Rio} from "rpi-io";
|
|
128
|
+
|
|
129
|
+
// Define instance for PWM operation on some PWM GPIO
|
|
130
|
+
const servoMotor = new Rio(13, "pwm");
|
|
131
|
+
|
|
132
|
+
// PWM servo motor parameters depends motor specifications.
|
|
133
|
+
// - period = 20,000,000 nano-seconds = 20 milli-seconds ~ frequency = 50 Hz
|
|
134
|
+
// - duty cycle on start = 500,000 nano-seconds = 0.5 milli-seconds
|
|
135
|
+
// - dutyMin = 0.5 ms ~ 0°
|
|
136
|
+
// - dutyMax = 2.5 ms ~ 180°
|
|
137
|
+
servoMotor.pwmInit(20000000, 500000, {
|
|
138
|
+
dutyMin: 500000,
|
|
139
|
+
dutyMax: 2500000
|
|
140
|
+
});
|
|
141
|
+
console.log("servo position = 0°");
|
|
142
|
+
|
|
143
|
+
// Wait 2s then change duty cyle to 1.5 ms
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
servo.pwmDuty(1500000);
|
|
146
|
+
console.log("servo position = 90°");
|
|
147
|
+
}, 2000);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## API
|
|
151
|
+
|
|
152
|
+
The **rpi-io** API is based on the class `Rio` with instance methods and static functions.
|
|
153
|
+
|
|
154
|
+
### Constructor and generic methods
|
|
155
|
+
|
|
156
|
+
#### Constructor - Rio(gpio, type)
|
|
157
|
+
|
|
158
|
+
##### Example
|
|
159
|
+
```javascript
|
|
160
|
+
import {Rio} from "rpi-io";
|
|
161
|
+
const myOutput = new Rio(17, 'out');
|
|
162
|
+
```
|
|
163
|
+
##### Parameters
|
|
164
|
+
- **gpio** *{Number}* Must be one of the GPIO number as defined in [pinout.xyz](https://pinout.xyz).
|
|
165
|
+
- **type** *{String}* Must be one of the following values: 'in', 'out', 'pwm'.
|
|
166
|
+
|
|
167
|
+
##### Detected errors
|
|
168
|
+
- Operating system version is not supported
|
|
169
|
+
- *libgpiod* version is not supported
|
|
170
|
+
- **gpio** is not defined as GPIO in [pinout.xyz](https://pinout.xyz).
|
|
171
|
+
- **gpio** is already used by another instance.
|
|
172
|
+
- **type** value is not recognized.
|
|
173
|
+
- **type**='pwm' and **gpio** is not properly configured.
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
#### Method - disable()
|
|
178
|
+
To remove an instance from the list.
|
|
179
|
+
|
|
180
|
+
##### Example
|
|
181
|
+
```javascript
|
|
182
|
+
myOutput.disable();
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Methods for instances where type='out'
|
|
186
|
+
#### set(value, duration)
|
|
187
|
+
|
|
188
|
+
To set some *out* GPIO to 0 or 1.
|
|
189
|
+
|
|
190
|
+
##### Example
|
|
191
|
+
```javascript
|
|
192
|
+
import {Rio} from "rpi-io";
|
|
193
|
+
const myOutput = new Rio(17, 'out');
|
|
194
|
+
myOutput.set(1, 3000);
|
|
195
|
+
```
|
|
196
|
+
##### Parameters
|
|
197
|
+
- **value** *{Number}* 0 or 1
|
|
198
|
+
- **duration** *{Number}* Time in ms. When defined, at the end of the defined time the output is inverted.
|
|
199
|
+
|
|
200
|
+
##### Detected errors
|
|
201
|
+
- Instance type is not 'out'.
|
|
202
|
+
- Instance is disabled or undefined.
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
### Methods for instances where type='in'
|
|
207
|
+
|
|
208
|
+
#### get()
|
|
209
|
+
|
|
210
|
+
To read instant value of some *in* GPIO.
|
|
211
|
+
|
|
212
|
+
##### Example
|
|
213
|
+
|
|
214
|
+
```javascript
|
|
215
|
+
import {Rio} from "rpi-io";
|
|
216
|
+
const myInput = new Rio(18, 'in');
|
|
217
|
+
let currentValue = myInput.get();
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
##### Returned value
|
|
221
|
+
|
|
222
|
+
0 or 1
|
|
223
|
+
|
|
224
|
+
##### Detected errors
|
|
225
|
+
|
|
226
|
+
- Instance type is not 'in'.
|
|
227
|
+
- Instance is disabled or undefined.
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
#### monitor(edge, callback)
|
|
232
|
+
|
|
233
|
+
To monitor activities on some *in* GPIO and receive events in a callback function.
|
|
234
|
+
|
|
235
|
+
##### Example
|
|
236
|
+
|
|
237
|
+
```javascript
|
|
238
|
+
import {Rio} from "rpi-io";
|
|
239
|
+
const myButton = new Rio(18, 'in');
|
|
240
|
+
myButton.monitor("both", event => {
|
|
241
|
+
console.log("event:", event.edge, event.time)
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
##### Parameters
|
|
246
|
+
|
|
247
|
+
- **edge** *{String}* 'rising', 'falling' or 'both'. Any other value stops instance monitoring.
|
|
248
|
+
- **callback** *{Function}* Callback function with one Object parameter for input events on instance e.g. `event = {edge: 'rising,' time: 'Thu Oct 30 2025 18:12:35 GMT+0100'}`
|
|
249
|
+
|
|
250
|
+
##### Detected errors
|
|
251
|
+
|
|
252
|
+
- Instance type is not 'in'.
|
|
253
|
+
- Instance is disabled or undefined.
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
### Methods for instances where type='pwm'
|
|
258
|
+
#### pwmInit(period, duty, options)
|
|
259
|
+
|
|
260
|
+
To initialize some *pwm* GPIO according to connected device specifications.
|
|
261
|
+
|
|
262
|
+
##### Example
|
|
263
|
+
|
|
264
|
+
```javascript
|
|
265
|
+
import {Rio} from "rpi-io";
|
|
266
|
+
const servoMotor = new Rio(13, "pwm");
|
|
267
|
+
servoMotor.pwmInit(20000000, 500000, {
|
|
268
|
+
dutyMin: 500000,
|
|
269
|
+
dutyMax: 2500000
|
|
270
|
+
});
|
|
271
|
+
console.log("servo position = 0°");
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
##### Parameters
|
|
275
|
+
|
|
276
|
+
All time parameters are defined in nano-seconds (ns).
|
|
277
|
+
|
|
278
|
+
- **period** *{Number}* Time period in which [duty cycle](https://en.wikipedia.org/wiki/Pulse-width_modulation) varies.
|
|
279
|
+
- **duty** *{Number}* Initial duty cycle.
|
|
280
|
+
- **options** *{Object}*
|
|
281
|
+
- **dutyMin** *{Number}* Default = 0. Minimum duty cycle (ns) as defined in connected device specifications.
|
|
282
|
+
- **dutyMax** *{Number}* Default = 1,000,000,000. Maximum duty cycle (ns) as defined in connected device specifications.
|
|
283
|
+
|
|
284
|
+
##### Detected errors
|
|
285
|
+
|
|
286
|
+
- Instance type is not 'pwm'.
|
|
287
|
+
- Instance is disabled or undefined.
|
|
288
|
+
- **period** is out-of-range [100, 1,000,000,000]
|
|
289
|
+
- **duty** < **options.dutyMin**
|
|
290
|
+
- **duty** > **options.dutyMax**
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
#### pwmStop()
|
|
295
|
+
|
|
296
|
+
To stop PWM modulation on some *pwm* GPIO.
|
|
297
|
+
|
|
298
|
+
##### Example
|
|
299
|
+
|
|
300
|
+
```javascript
|
|
301
|
+
servoMotor.pwmStop()
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
##### Detected errors
|
|
305
|
+
|
|
306
|
+
- Instance type is not 'pwm'.
|
|
307
|
+
- Instance is disabled or undefined.
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
#### pwmDuty(time)
|
|
312
|
+
|
|
313
|
+
To update duty cycle of some *pwm* GPIO.
|
|
314
|
+
|
|
315
|
+
##### Example
|
|
316
|
+
|
|
317
|
+
```javascript
|
|
318
|
+
servo.pwmDuty(1500000)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
##### Parameters
|
|
322
|
+
|
|
323
|
+
- **time** *{Number}* Duty cycle (ns)
|
|
324
|
+
|
|
325
|
+
##### Detected errors
|
|
326
|
+
|
|
327
|
+
- Instance type is not 'pwm'.
|
|
328
|
+
- PWM instance is not properly initialized.
|
|
329
|
+
- **time** is out of range [dutyMin, dutyMax]
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
### Utility functions
|
|
334
|
+
#### Rio.config(gpio)
|
|
335
|
+
|
|
336
|
+
To return configuration of some GPIO
|
|
337
|
+
|
|
338
|
+
##### Example
|
|
339
|
+
|
|
340
|
+
```javascript
|
|
341
|
+
import {Rio} from "rpi-io";
|
|
342
|
+
console.log(Rio.config(13))
|
|
343
|
+
// '13': 'pwm0_chan1'
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
#### Rio.info()
|
|
347
|
+
|
|
348
|
+
Javascript equivalent of command `gpioinfo`.
|
|
349
|
+
|
|
350
|
+
##### Example
|
|
351
|
+
|
|
352
|
+
```javascript
|
|
353
|
+
import {Rio} from "rpi-io";
|
|
354
|
+
console.log(Rio.info())
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
#### Rio.detect()
|
|
358
|
+
|
|
359
|
+
Javascript equivalent of command `gpiodetect`.
|
|
360
|
+
|
|
361
|
+
##### Example
|
|
362
|
+
|
|
363
|
+
```javascript
|
|
364
|
+
import {Rio} from "rpi-io";
|
|
365
|
+
console.log(Rio.detect())
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
#### Rio.version()
|
|
369
|
+
|
|
370
|
+
To retrieve libgpiod version.
|
|
371
|
+
|
|
372
|
+
##### Example
|
|
373
|
+
|
|
374
|
+
```javascript
|
|
375
|
+
import {Rio} from "rpi-io";
|
|
376
|
+
console.log(Rio.version())
|
|
377
|
+
// 'v1.6.3',
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
#### Rio.os()
|
|
381
|
+
|
|
382
|
+
To retrieve Rapsberry Pi OS info.
|
|
383
|
+
|
|
384
|
+
##### Example
|
|
385
|
+
|
|
386
|
+
```javascript
|
|
387
|
+
import {Rio} from "rpi-io";
|
|
388
|
+
console.log(Rio.os())
|
|
389
|
+
// {id: '12', name: 'Bookworm'}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
#### Rio.isSystemSupported()
|
|
393
|
+
|
|
394
|
+
To check if device is supported by **rpi-io** (device, model, os, middleware). It returns *true/false* and displays details in console.
|
|
395
|
+
|
|
396
|
+
#### Rio.log(level)
|
|
397
|
+
To set the level of console used in the module:
|
|
398
|
+
|
|
399
|
+
- 0: no console
|
|
400
|
+
- 1: console only for errors and warnings (default).
|
|
401
|
+
- 2: more logged data
|
|
402
|
+
|
|
403
|
+
##### Example
|
|
404
|
+
|
|
405
|
+
```javascript
|
|
406
|
+
import {Rio} from "rpi-io";
|
|
407
|
+
Rio.log(0)
|
|
408
|
+
Rio.isSystemSupported() // Nothing displayed in console
|
|
409
|
+
Rio.log(2)
|
|
410
|
+
Rio.isSystemSupported() // See console below
|
|
411
|
+
// 08:15:16.506 🔎 👍 Hardware is Raspberry Pi
|
|
412
|
+
// 08:15:16.539 🔎 👍 Raspberry Pi 5 Model B Rev 1.0 is supported
|
|
413
|
+
// 08:15:16.543 🔎 👍 Bookworm OS is supported
|
|
414
|
+
// 08:15:16.545 🔎 👍 libgpiod version v1.6.x is supported
|
|
415
|
+
// 08:15:16.545 🔎 👍 Systems requirements for rpi-io are met
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
## Benchmark
|
|
420
|
+
|
|
421
|
+
The following table summarizes GPIO *set* average times with rpi-io v1.1.0 and various hardwares and OS.
|
|
422
|
+
|
|
423
|
+
| | RPi 5B | RPi 4B | RPi Zero2 |
|
|
424
|
+
| -------- | ------- | ---------- | ---------- |
|
|
425
|
+
| Bookworm | 1.37 ms | 4.56 ms | 9.07 ms |
|
|
426
|
+
| Trixie | 3.82 ms | not tested | not tested |
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
package/esm/dev.mjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// -------------------------------------------------------------------
|
|
2
|
+
// RPi GPIO CONTROL - DEV TOOLS
|
|
3
|
+
// -------------------------------------------------------------------
|
|
4
|
+
let debug = {
|
|
5
|
+
log: false,
|
|
6
|
+
trap: true
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// -------------------------------------------------------------------
|
|
10
|
+
// FUNCTIONS
|
|
11
|
+
/** ------------------------------------------------------------------
|
|
12
|
+
* @function now
|
|
13
|
+
* @description Return timestamp as formatted string
|
|
14
|
+
* @return {String}
|
|
15
|
+
*/
|
|
16
|
+
export const now = () => {
|
|
17
|
+
const date = new Date()
|
|
18
|
+
return date.toLocaleTimeString() + "." + date.getMilliseconds().toLocaleString('en', {
|
|
19
|
+
minimumIntegerDigits: 3,
|
|
20
|
+
minimumFractionDigits: 0,
|
|
21
|
+
useGrouping: false
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** ------------------------------------------------------------------
|
|
26
|
+
* @function log
|
|
27
|
+
* @description Global customized timestamped console.log
|
|
28
|
+
* Requires global variable 'debug'
|
|
29
|
+
*/
|
|
30
|
+
export const log = function () {
|
|
31
|
+
if (debug.log)
|
|
32
|
+
console.log.apply(console, Array.prototype.concat.apply([now(), "🔎 "], arguments))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** ------------------------------------------------------------------
|
|
36
|
+
* @function trap
|
|
37
|
+
* @description Colored log for error messages ⚠️ nodejs only ~ CSS color #F50
|
|
38
|
+
*/
|
|
39
|
+
export const trap = function () {
|
|
40
|
+
if (debug.trap)
|
|
41
|
+
console.log.apply(console, Array.prototype.concat.apply([now(), "⚠️", "\x1b[38;2;255;80;0m", ...arguments, "\x1b[0m"]))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** ------------------------------------------------------------------
|
|
45
|
+
* @function cfgLog
|
|
46
|
+
* @description Set log configuration
|
|
47
|
+
* @param {Number} level 0: no log, 1: trap only, 2: trap and log
|
|
48
|
+
*/
|
|
49
|
+
export const cfgLog = level => {
|
|
50
|
+
debug = {
|
|
51
|
+
log: false,
|
|
52
|
+
trap: false
|
|
53
|
+
}
|
|
54
|
+
level > 1 ? debug.log = true : false
|
|
55
|
+
level > 0 ? debug.trap = true : false
|
|
56
|
+
}
|
|
57
|
+
// -------------------------------------------------------------------
|
|
58
|
+
// EoF
|
|
59
|
+
// -------------------------------------------------------------------
|
package/esm/rio.mjs
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
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 {cfgLog, 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
|
+
// List of used GPIOs
|
|
37
|
+
static used_gpios = []
|
|
38
|
+
// Object list of monitored inputs
|
|
39
|
+
static monitored = {}
|
|
40
|
+
// Current monitoring process
|
|
41
|
+
static monitoringProcess = null
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------
|
|
44
|
+
// GENERIC METHODS: constructor, disable
|
|
45
|
+
/** --------------------------------------------------------------
|
|
46
|
+
* @function constructor
|
|
47
|
+
* @description
|
|
48
|
+
* @param {Number} gpio
|
|
49
|
+
* @param {String} type - {'in', 'out', 'pwm'}
|
|
50
|
+
* @return {Object}
|
|
51
|
+
*/
|
|
52
|
+
constructor(gpio, type) {
|
|
53
|
+
|
|
54
|
+
this.chip = Rio.RPI_CHIP
|
|
55
|
+
this.gpio = gpio
|
|
56
|
+
this.gpioStr = this.gpio.toString()
|
|
57
|
+
this.gpioName = "GPIO" + this.gpioStr
|
|
58
|
+
this.type = type
|
|
59
|
+
this.os = Rio.os().name
|
|
60
|
+
this.versionLib = Rio.version().substring(0, 4)
|
|
61
|
+
|
|
62
|
+
// OS is not supported
|
|
63
|
+
if (Rio.TESTED_OS.indexOf(this.os) === -1)
|
|
64
|
+
throw new Error("This OS is not supported: " + this.os)
|
|
65
|
+
|
|
66
|
+
// libgpiod is not supported
|
|
67
|
+
const foundLib = Rio.TESTED_LIBGPIOD.find(testedVersion => {
|
|
68
|
+
return this.versionLib === testedVersion.substring(0, 4)
|
|
69
|
+
})
|
|
70
|
+
if (!foundLib)
|
|
71
|
+
throw new Error("This version of libgpiod is not supported: " + this.versionLib)
|
|
72
|
+
|
|
73
|
+
// Unknown type
|
|
74
|
+
if (Rio.TYPES.indexOf(type) === -1)
|
|
75
|
+
throw new Error("This type is not supported: " + gpio)
|
|
76
|
+
|
|
77
|
+
// gpio is not supported
|
|
78
|
+
if (Rio.RPI_GPIOS.indexOf(gpio) === -1)
|
|
79
|
+
throw new Error("This gpio is not supported: " + gpio)
|
|
80
|
+
|
|
81
|
+
// gpio is already used
|
|
82
|
+
if (Rio.used_gpios.indexOf(gpio) !== -1)
|
|
83
|
+
throw new Error("This gpio is already used: " + gpio)
|
|
84
|
+
|
|
85
|
+
// this.read, this.write, monArgs depending on gpiolib version
|
|
86
|
+
switch (this.versionLib) {
|
|
87
|
+
case "v1.6":
|
|
88
|
+
this.write = (value, time = 0) => {
|
|
89
|
+
exeFile("gpioset", [Rio.RPI_CHIP, this.gpioStr + "=" + value])
|
|
90
|
+
}
|
|
91
|
+
this.read = () => {
|
|
92
|
+
return exeFile("gpioget", [Rio.RPI_CHIP, this.gpioStr])
|
|
93
|
+
}
|
|
94
|
+
this.monArgs = gpios => {
|
|
95
|
+
let args = []
|
|
96
|
+
args.push("-b") // line-buffered: output is flushed after every line
|
|
97
|
+
args.push("--format=%o %e") // gpio, edge. Others: seconds(%s) and nanoseconds (%n)
|
|
98
|
+
args.push(Rio.RPI_CHIP)
|
|
99
|
+
args = args.concat(gpios)
|
|
100
|
+
return args
|
|
101
|
+
}
|
|
102
|
+
this.monData = data => {
|
|
103
|
+
const [gpio, edge] = data.toString().trim().split(/\s+/)
|
|
104
|
+
return {
|
|
105
|
+
gpio: gpio,
|
|
106
|
+
edge: edge
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
break
|
|
110
|
+
case "v2.2":
|
|
111
|
+
this.write = (value, time = 0) => {
|
|
112
|
+
exeFile("gpioset", ["-t" + time, this.gpioName + "=" + value])
|
|
113
|
+
}
|
|
114
|
+
this.read = () => {
|
|
115
|
+
return exeFile("gpioget", ["--numeric", this.gpioName])
|
|
116
|
+
}
|
|
117
|
+
this.monArgs = gpios => {
|
|
118
|
+
gpios = gpios.map(gpio => "GPIO" + gpio)
|
|
119
|
+
let args = []
|
|
120
|
+
args = args.concat(gpios)
|
|
121
|
+
return args
|
|
122
|
+
}
|
|
123
|
+
this.monData = data => {
|
|
124
|
+
const [time, edge, gpio] = data.toString().trim().split(/\s+/)
|
|
125
|
+
return {
|
|
126
|
+
gpio: gpio.substring(5,7),
|
|
127
|
+
edge: edge
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// pwm specific cases
|
|
134
|
+
if (type === "pwm") {
|
|
135
|
+
|
|
136
|
+
// gpio is not supported for PWM
|
|
137
|
+
if (Rio.RPi_GPIO_PWM.indexOf(gpio) === -1) {
|
|
138
|
+
throw new Error("PWM gpio must be one of " + Rio.RPi_GPIO_PWM)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// gpio is not setup for PWM
|
|
142
|
+
const thisConfig = Rio.config(gpio)
|
|
143
|
+
if (thisConfig.indexOf("pwm") === -1)
|
|
144
|
+
throw new Error("This gpio is not setup as PWM (see README)")
|
|
145
|
+
|
|
146
|
+
this.channel = thisConfig.slice(-1)
|
|
147
|
+
this.pwm = Rio.PWM_CHIP
|
|
148
|
+
this.pathPwm = "/sys/class/pwm/" + this.pwm + "/"
|
|
149
|
+
this.pathChannel = this.pathPwm + "pwm" + this.channel + "/"
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** --------------------------------------------------------------
|
|
154
|
+
* @function disable
|
|
155
|
+
* @description Remove instance from Rio.used_gpios
|
|
156
|
+
* Set this.gpio = -1
|
|
157
|
+
*/
|
|
158
|
+
disable() {
|
|
159
|
+
const i = Rio.used_gpios.indexOf(this.gpio)
|
|
160
|
+
if (this.gpio === -1 || i === -1) {
|
|
161
|
+
log("gpio", this.gpio, "is already disabled")
|
|
162
|
+
} else {
|
|
163
|
+
Rio.used_gpios.splice(i, 1)
|
|
164
|
+
log("gpio", this.gpio, "has been disabled")
|
|
165
|
+
this.gpio = -1
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------
|
|
170
|
+
// OUTPUT METHODS: set
|
|
171
|
+
/** --------------------------------------------------------------
|
|
172
|
+
* @function set
|
|
173
|
+
* @description Set gpio to 1 or 0
|
|
174
|
+
* Then invert when duration time is <> 0
|
|
175
|
+
* @param {Number} value
|
|
176
|
+
* @param {Number} duration ms
|
|
177
|
+
*/
|
|
178
|
+
set(value = 0, duration = 0) {
|
|
179
|
+
|
|
180
|
+
if (this.gpio === -1)
|
|
181
|
+
throw new Error("This instance is disabled")
|
|
182
|
+
|
|
183
|
+
if (this.type !== "out")
|
|
184
|
+
throw new Error("Set is dedicated to 'out' instances")
|
|
185
|
+
|
|
186
|
+
if ([0, 1].indexOf(value) === -1)
|
|
187
|
+
throw new Error("Value must be either 0 or 1")
|
|
188
|
+
|
|
189
|
+
if (duration === 0) {
|
|
190
|
+
this.write(value)
|
|
191
|
+
log("set gpio", this.gpio, "to", value)
|
|
192
|
+
}
|
|
193
|
+
// Optional timer
|
|
194
|
+
else {
|
|
195
|
+
this.write(value)
|
|
196
|
+
log("set gpio", this.gpio, "to", value)
|
|
197
|
+
setTimeout(() => {
|
|
198
|
+
const valueStop = Math.abs(value - 1)
|
|
199
|
+
this.write(valueStop)
|
|
200
|
+
log("set gpio", this.gpio, "to", valueStop)
|
|
201
|
+
}, duration)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------
|
|
206
|
+
// INPUT METHODS
|
|
207
|
+
/** --------------------------------------------------------------
|
|
208
|
+
* @function get
|
|
209
|
+
* @description Get gpio value (0/1)
|
|
210
|
+
*/
|
|
211
|
+
get() {
|
|
212
|
+
if (this.gpio === -1)
|
|
213
|
+
throw new Error("This instance is disabled")
|
|
214
|
+
|
|
215
|
+
if (this.type !== "in")
|
|
216
|
+
throw new Error("Get is dedicated to 'in' instances")
|
|
217
|
+
|
|
218
|
+
const str = this.read()
|
|
219
|
+
|
|
220
|
+
let result
|
|
221
|
+
str.length === 0 ? result = -1 : result = parseInt(str.replace(/\r?\n|\r/, ""))
|
|
222
|
+
log("get gpio", this.gpio, "value:", result)
|
|
223
|
+
return result
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** --------------------------------------------------------------
|
|
227
|
+
* @function monitor
|
|
228
|
+
* @description Wait for edge events
|
|
229
|
+
* @param {String} edge {'stop', 'rising', 'falling' 'both'}
|
|
230
|
+
* @param {Function} callback (edge, time)
|
|
231
|
+
*/
|
|
232
|
+
monitor(edge, callback) {
|
|
233
|
+
|
|
234
|
+
if (this.gpio === -1)
|
|
235
|
+
throw new Error("This instance is disabled")
|
|
236
|
+
|
|
237
|
+
if (this.type !== "in")
|
|
238
|
+
throw new Error("Monitor is dedicated to 'in' instances")
|
|
239
|
+
|
|
240
|
+
// Update monitoring list
|
|
241
|
+
Rio.monitored[this.gpio] = {
|
|
242
|
+
edge: edge,
|
|
243
|
+
callback: callback
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Suspend current monitoring if any
|
|
247
|
+
if (Rio.monitoringProcess !== null) {
|
|
248
|
+
Rio.monitoringProcess.kill("SIGTERM")
|
|
249
|
+
Rio.monitoringProcess = null
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Default stop command => Remove current gpio from monitoring list
|
|
253
|
+
if (["rising", "falling", "both"].indexOf(edge) === -1) {
|
|
254
|
+
delete Rio.monitored[this.gpio]
|
|
255
|
+
log("gpio", this.gpio, "is no longer being monitored")
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// If monitored list is not empty => relaunch gpiomon
|
|
259
|
+
const list = Object.keys(Rio.monitored)
|
|
260
|
+
if (list.length > 0) {
|
|
261
|
+
Rio.monitoringProcess = spawn("gpiomon", this.monArgs(list))
|
|
262
|
+
Rio.monitoringProcess.stdout.on("data", data => {
|
|
263
|
+
// Retrieve event data
|
|
264
|
+
const evd = this.monData(data)
|
|
265
|
+
const that = Rio.monitored[parseInt(evd.gpio)]
|
|
266
|
+
if (typeof that.callback === "function") {
|
|
267
|
+
if (evd.edge === "1") {
|
|
268
|
+
if (that.edge === "rising" || that.edge === "both")
|
|
269
|
+
that.callback("rising", new Date())
|
|
270
|
+
} else {
|
|
271
|
+
if (that.edge === "falling" || that.edge === "both")
|
|
272
|
+
that.callback("falling", new Date())
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
Rio.monitoringProcess.stderr.on("data", data => {
|
|
277
|
+
trap("gpiomon stderr:", Buffer.from(data).toString())
|
|
278
|
+
})
|
|
279
|
+
} else {
|
|
280
|
+
log("no more gpio to monitor")
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ---------------------------------------------------------------
|
|
285
|
+
// PWM METHODS
|
|
286
|
+
// PLEASE NOTE - Commands to unlock a channel:
|
|
287
|
+
// pwm channel 0: echo 0 > /sys/class/pwm/pwmchip0/unexport
|
|
288
|
+
// pwm channel 0: echo 1 > /sys/class/pwm/pwmchip0/unexport
|
|
289
|
+
/** --------------------------------------------------------------
|
|
290
|
+
* @function pwmInit
|
|
291
|
+
* @description Start PWM modulation
|
|
292
|
+
* @param {Number} period in ns
|
|
293
|
+
* @param {Number} duty cycle in ns
|
|
294
|
+
* @param {Object} opt
|
|
295
|
+
*/
|
|
296
|
+
pwmInit(period = 20000000, duty = 0, opt) {
|
|
297
|
+
|
|
298
|
+
// Stop on wrong type
|
|
299
|
+
if (this.type !== "pwm")
|
|
300
|
+
throw new Error("This GPIO is not configured as PWM")
|
|
301
|
+
|
|
302
|
+
// Stop disabled device
|
|
303
|
+
if (this.gpio === -1)
|
|
304
|
+
throw new Error("This instance is disabled")
|
|
305
|
+
|
|
306
|
+
// Set default options
|
|
307
|
+
const defopt = {
|
|
308
|
+
dutyMin: 0,
|
|
309
|
+
dutyMax: 1000000000, // 1s
|
|
310
|
+
}
|
|
311
|
+
opt = {...defopt, ...opt}
|
|
312
|
+
|
|
313
|
+
this.period = period
|
|
314
|
+
this.duty = duty
|
|
315
|
+
this.dutyMin = Math.max(0, opt.dutyMin)
|
|
316
|
+
this.dutyMax = Math.min(period, opt.dutyMax)
|
|
317
|
+
|
|
318
|
+
// Check period
|
|
319
|
+
if (this.period < 100 || this.period > 1000000000)
|
|
320
|
+
throw new Error("PWM period is out of range (100ns - 1s")
|
|
321
|
+
|
|
322
|
+
// Check duty cycle
|
|
323
|
+
if (duty < this.dutyMin)
|
|
324
|
+
throw new Error("PWM duty", duty, " is lower than duty min", this.dutyMin)
|
|
325
|
+
if (duty > this.dutyMax)
|
|
326
|
+
throw new Error("PWM duty", duty, " is upper than duty max", this.dutyMax)
|
|
327
|
+
|
|
328
|
+
log("channel", this.channel, "normalized PWM data (period, duty, duty min, duty max):", this.period, this.duty, this.dutyMin, this.dutyMax)
|
|
329
|
+
|
|
330
|
+
// Export channel, then set period, duty cycle and enable
|
|
331
|
+
writeFileSync(this.pathPwm + "export", this.channel)
|
|
332
|
+
this.exported = true
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
wait(100)
|
|
336
|
+
writeFileSync(this.pathChannel + "period", String(this.period))
|
|
337
|
+
writeFileSync(this.pathChannel + "duty_cycle", String(this.duty))
|
|
338
|
+
writeFileSync(this.pathChannel + "enable", "1")
|
|
339
|
+
this.enabled = true
|
|
340
|
+
|
|
341
|
+
} catch (err) {
|
|
342
|
+
trap("pwm start error:", err)
|
|
343
|
+
this.pwmStop()
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** --------------------------------------------------------------
|
|
348
|
+
* @function pwmStop
|
|
349
|
+
* @description Stop PWM modulation
|
|
350
|
+
*/
|
|
351
|
+
pwmStop() {
|
|
352
|
+
|
|
353
|
+
if (this.type !== "pwm")
|
|
354
|
+
throw new Error("This GPIO is not configured as PWM")
|
|
355
|
+
|
|
356
|
+
// Disable signal then unexport channel
|
|
357
|
+
writeFileSync(this.pathChannel + "enable", "0")
|
|
358
|
+
this.enabled = false
|
|
359
|
+
writeFileSync(this.pathPwm + "unexport", this.channel)
|
|
360
|
+
this.exported = true
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** --------------------------------------------------------------
|
|
364
|
+
* @function pwmDuty
|
|
365
|
+
* @description Change PWM duty cycle
|
|
366
|
+
* @parameter {Number} time
|
|
367
|
+
*/
|
|
368
|
+
pwmDuty(time) {
|
|
369
|
+
|
|
370
|
+
// Not PWM
|
|
371
|
+
if (this.type !== "pwm")
|
|
372
|
+
throw new Error("This GPIO is not configured as PWM")
|
|
373
|
+
|
|
374
|
+
// Not initialized
|
|
375
|
+
if (!this.enabled || !this.exported) {
|
|
376
|
+
trap("PWM duty cycle cannot be update if port is not exported and enabled")
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Out of range
|
|
381
|
+
this.duty = time
|
|
382
|
+
if (typeof this.duty !== "number" || this.duty < this.dutyMin) {
|
|
383
|
+
this.duty = this.dutyMin
|
|
384
|
+
trap("duty cycle forced to", this.dutyMin)
|
|
385
|
+
}
|
|
386
|
+
if (typeof this.duty !== "number" || this.duty > this.dutyMax) {
|
|
387
|
+
this.duty = this.dutyMax
|
|
388
|
+
trap("duty cycle forced to", this.dutyMax)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
writeFileSync(this.pathChannel + "duty_cycle", this.duty.toString())
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// -------------------------------------------------------------------
|
|
396
|
+
// STATIC METHODS
|
|
397
|
+
/** ------------------------------------------------------------------
|
|
398
|
+
* @function Rio.config
|
|
399
|
+
* @description Return GPIO configuration
|
|
400
|
+
* @param {Number} gpio
|
|
401
|
+
* @return {String}
|
|
402
|
+
*/
|
|
403
|
+
Rio.config = gpio => {
|
|
404
|
+
const a = exeShell("pinctrl get " + gpio).trim().split(/\s+/)
|
|
405
|
+
return a[a.length - 1].toLowerCase()
|
|
406
|
+
}
|
|
407
|
+
/** ------------------------------------------------------------------
|
|
408
|
+
* @function Rio.configs
|
|
409
|
+
* @description Return GPIO configuration for all supported GPIO
|
|
410
|
+
* @return {Object}
|
|
411
|
+
*/
|
|
412
|
+
Rio.configs = () => {
|
|
413
|
+
let configs = {}
|
|
414
|
+
Rio.RPI_GPIOS.forEach(gpio => {
|
|
415
|
+
configs[gpio] = Rio.config(gpio)
|
|
416
|
+
})
|
|
417
|
+
log("gpio configs:", configs)
|
|
418
|
+
return configs
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** ------------------------------------------------------------------
|
|
422
|
+
* @function Rio.info
|
|
423
|
+
* @description Exec gpioinfo
|
|
424
|
+
* @return {Object}
|
|
425
|
+
*/
|
|
426
|
+
Rio.info = () => {
|
|
427
|
+
return exeFile("gpioinfo", [Rio.RPI_CHIP])
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** ------------------------------------------------------------------
|
|
431
|
+
* @function Rio.detect
|
|
432
|
+
* @description Exec gpiodetect
|
|
433
|
+
* @return {Object}
|
|
434
|
+
*/
|
|
435
|
+
Rio.detect = () => {
|
|
436
|
+
return exeFile("gpiodetect")
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** ------------------------------------------------------------------
|
|
440
|
+
* @function Rio.version
|
|
441
|
+
* @description Return libgpiod version
|
|
442
|
+
* @return {Object}
|
|
443
|
+
*/
|
|
444
|
+
Rio.version = () => {
|
|
445
|
+
return exeFile("gpiodetect", ["-v"]).split("\n").shift().substring(22).trim()
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** ------------------------------------------------------------------
|
|
449
|
+
* @function Rio.stopMonitoring
|
|
450
|
+
* @description Stop monitoring of all inputs
|
|
451
|
+
*/
|
|
452
|
+
Rio.stopMonitoring = () => {
|
|
453
|
+
Rio.monitoringProcess !== null ? Rio.monitoringProcess.kill("SIGTERM") : false
|
|
454
|
+
Rio.monitored = {}
|
|
455
|
+
Rio.monitoringProcess = null
|
|
456
|
+
log("gpio monitoring stopped")
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** ------------------------------------------------------------------
|
|
460
|
+
* @function Rio.cpuInfo
|
|
461
|
+
* @description Read '/proc/cpuinfo' and store it for further call
|
|
462
|
+
* @return {String}
|
|
463
|
+
*/
|
|
464
|
+
Rio.cpuInfo = () => {
|
|
465
|
+
if (typeof Rio.cpuInfo.value === "undefined") {
|
|
466
|
+
try {
|
|
467
|
+
Rio.cpuInfo.value = readFileSync("/proc/cpuinfo", "utf8")
|
|
468
|
+
} catch (err) {
|
|
469
|
+
trap("catched error opening '/proc/cpuinfo:", err)
|
|
470
|
+
Rio.cpuInfo.value = ""
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return Rio.cpuInfo.value
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** ------------------------------------------------------------------
|
|
477
|
+
* @function Rio.os
|
|
478
|
+
* @description Return OS ID and name
|
|
479
|
+
* @return {Object} {id: '12', name: 'Bookworm'}
|
|
480
|
+
*/
|
|
481
|
+
Rio.os = () => {
|
|
482
|
+
const strOs = exeShell("cat /etc/os-release")
|
|
483
|
+
if (strOs.length > 0) {
|
|
484
|
+
const lines = strOs.split("\n")
|
|
485
|
+
const info = {}
|
|
486
|
+
lines.forEach(line => {
|
|
487
|
+
const [key, value] = line.split("=")
|
|
488
|
+
if (key && value) {
|
|
489
|
+
info[key] = value.replace(/"/g, "");
|
|
490
|
+
}
|
|
491
|
+
})
|
|
492
|
+
let res = {
|
|
493
|
+
id: info["VERSION_ID"],
|
|
494
|
+
name: info["VERSION_CODENAME"]
|
|
495
|
+
}
|
|
496
|
+
res.name = res.name.substring(0, 1).toUpperCase() + res.name.substring(1);
|
|
497
|
+
return res
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/** ------------------------------------------------------------------
|
|
501
|
+
* @function Rio.isSystemSupported
|
|
502
|
+
* @description Check if all hardware and software requirements are
|
|
503
|
+
* satisfied to run the RPI-IO module
|
|
504
|
+
* @return {Boolean}
|
|
505
|
+
*/
|
|
506
|
+
Rio.isSystemSupported = () => {
|
|
507
|
+
|
|
508
|
+
const info = Rio.cpuInfo()
|
|
509
|
+
|
|
510
|
+
// Test hardware
|
|
511
|
+
if (/Raspberry Pi/i.test(Rio.cpuInfo()) === false) {
|
|
512
|
+
trap("not a Raspberry Pi hardware")
|
|
513
|
+
return false
|
|
514
|
+
}
|
|
515
|
+
log("👍 Hardware is Raspberry Pi")
|
|
516
|
+
|
|
517
|
+
// Test model
|
|
518
|
+
const line = Rio.cpuInfo().split("\n").find(str => str.indexOf("Model") !== -1)
|
|
519
|
+
const model = line.split(":")[1].trim()
|
|
520
|
+
if (Rio.TESTED_MODELS.indexOf(model) === -1) {
|
|
521
|
+
trap("model not tested:", model)
|
|
522
|
+
return false
|
|
523
|
+
}
|
|
524
|
+
log("👍 ", model, "is supported")
|
|
525
|
+
|
|
526
|
+
// Test OS
|
|
527
|
+
const thisOs = Rio.os()
|
|
528
|
+
if (Rio.TESTED_OS.indexOf(thisOs.name) === -1) {
|
|
529
|
+
trap("OS not tested:", thisOs.name, thisOs.id)
|
|
530
|
+
return false
|
|
531
|
+
}
|
|
532
|
+
log("👍 ", thisOs.name, "OS is supported")
|
|
533
|
+
|
|
534
|
+
// Test libgpiod version x.y
|
|
535
|
+
const gpiodVersion = Rio.version().substring(0, 4)
|
|
536
|
+
const foundLib = Rio.TESTED_LIBGPIOD.find(testedVersion => {
|
|
537
|
+
return gpiodVersion === testedVersion.substring(0, 4)
|
|
538
|
+
})
|
|
539
|
+
if (!foundLib) {
|
|
540
|
+
trap("libgpiod version not tested:", gpiodVersion)
|
|
541
|
+
return false
|
|
542
|
+
}
|
|
543
|
+
log("👍 libgpiod version", gpiodVersion + ".x", "is supported")
|
|
544
|
+
log("👍 Systems requirements for rpi-io are met")
|
|
545
|
+
return true
|
|
546
|
+
}
|
|
547
|
+
/** ------------------------------------------------------------------
|
|
548
|
+
* @function Rio.log
|
|
549
|
+
*/
|
|
550
|
+
Rio.log = cfgLog
|
|
551
|
+
// -------------------------------------------------------------------
|
|
552
|
+
// EoF
|
|
553
|
+
// -------------------------------------------------------------------
|
package/esm/utl.mjs
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// -------------------------------------------------------------------
|
|
2
|
+
// RPi GPIO CONTROL - UTILITIES
|
|
3
|
+
// -------------------------------------------------------------------
|
|
4
|
+
import {log,trap} from "./dev.mjs"
|
|
5
|
+
import {execSync, execFileSync, spawnSync} from "child_process"
|
|
6
|
+
|
|
7
|
+
// -------------------------------------------------------------------
|
|
8
|
+
// JS UTILITIES
|
|
9
|
+
/** ------------------------------------------------------------------
|
|
10
|
+
* @function sleep
|
|
11
|
+
* @description Wait before continuing
|
|
12
|
+
* @param {Number} ms
|
|
13
|
+
*/
|
|
14
|
+
export const sleep = ms => {
|
|
15
|
+
log("sleeping", ms, "ms")
|
|
16
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** ------------------------------------------------------------------
|
|
20
|
+
* @function ctrlC
|
|
21
|
+
* @description Intercept ctrl+c then exec callback
|
|
22
|
+
* @param {Function} callback
|
|
23
|
+
*/
|
|
24
|
+
export const ctrlC = callback => {
|
|
25
|
+
process.on("SIGINT", () => {
|
|
26
|
+
log("ctrl+c pressed")
|
|
27
|
+
typeof callback === "function" ? callback() : false
|
|
28
|
+
process.exit(0)
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// -------------------------------------------------------------------
|
|
33
|
+
// NODEJS UTILITIES
|
|
34
|
+
/** ------------------------------------------------------------------
|
|
35
|
+
* @function exeShell
|
|
36
|
+
* @description Async run command in Linux shell
|
|
37
|
+
* @param {String} cmd
|
|
38
|
+
* @return {String} stdout or ""
|
|
39
|
+
*/
|
|
40
|
+
export const exeShell = cmd => {
|
|
41
|
+
try {
|
|
42
|
+
return execSync(cmd, {
|
|
43
|
+
stdio: "pipe",
|
|
44
|
+
encoding: "utf8",
|
|
45
|
+
})
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (err.code) {
|
|
48
|
+
// Spawning child process failed
|
|
49
|
+
trap("execFileSync error code", err.code)
|
|
50
|
+
} else {
|
|
51
|
+
// Child was spawned but exited with non-zero exit code
|
|
52
|
+
// Error contains any stdout and stderr from the child
|
|
53
|
+
const {stdout, stderr} = err
|
|
54
|
+
trap({stdout, stderr})
|
|
55
|
+
}
|
|
56
|
+
return ""
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** ------------------------------------------------------------------
|
|
61
|
+
* @function exeFile
|
|
62
|
+
* @description Async run executable file
|
|
63
|
+
* @param {String} file
|
|
64
|
+
* @param {Array} args
|
|
65
|
+
* @return {String} stdout or ""
|
|
66
|
+
*/
|
|
67
|
+
export const exeFile = (file, args) => {
|
|
68
|
+
try {
|
|
69
|
+
return execFileSync(file, args, {
|
|
70
|
+
stdio: "pipe",
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
})
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (err.code) {
|
|
75
|
+
// Spawning child process failed
|
|
76
|
+
trap("execFileSync error code", err.code)
|
|
77
|
+
} else {
|
|
78
|
+
// Child was spawned but exited with non-zero exit code
|
|
79
|
+
// Error contains any stdout and stderr from the child
|
|
80
|
+
const {stdout, stderr} = err
|
|
81
|
+
trap({stdout, stderr})
|
|
82
|
+
}
|
|
83
|
+
return ""
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** ------------------------------------------------------------------
|
|
88
|
+
* @function wait
|
|
89
|
+
* @description Wait before continuing (sync blocking mode)
|
|
90
|
+
* @param {Number} ms
|
|
91
|
+
*/
|
|
92
|
+
export const wait = ms => {
|
|
93
|
+
log("waiting", ms, "ms")
|
|
94
|
+
spawnSync("sleep", [ms / 1000])
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
// -------------------------------------------------------------------
|
|
99
|
+
// EoF
|
|
100
|
+
// -------------------------------------------------------------------
|
package/history.md
ADDED
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rpi-io",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Nodejs module to control Raspbbery Pi GPIO",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"raspberry",
|
|
7
7
|
"gpio",
|
|
8
|
-
"nodejs"
|
|
8
|
+
"nodejs",
|
|
9
|
+
"esm",
|
|
10
|
+
"pwm"
|
|
9
11
|
],
|
|
10
12
|
"homepage": "https://github.com/gdorbes/rpi-io#readme",
|
|
11
13
|
"bugs": {
|
|
@@ -15,11 +17,16 @@
|
|
|
15
17
|
"type": "git",
|
|
16
18
|
"url": "git+https://github.com/gdorbes/rpi-io.git"
|
|
17
19
|
},
|
|
18
|
-
"license": "
|
|
20
|
+
"license": "MIT",
|
|
19
21
|
"author": "Guillaume DORBES",
|
|
20
22
|
"type": "module",
|
|
21
|
-
"
|
|
23
|
+
"engines" : {
|
|
24
|
+
"npm" : ">=10.0.0",
|
|
25
|
+
"node" : ">=23.0.0"
|
|
26
|
+
},
|
|
27
|
+
"main": "./esm/rio.mjs",
|
|
28
|
+
"exports": "./esm/rio.mjs",
|
|
22
29
|
"scripts": {
|
|
23
|
-
"test":
|
|
30
|
+
"test":"node ./scr/test.js"
|
|
24
31
|
}
|
|
25
32
|
}
|
package/scr/test.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// -------------------------------------------------------------------
|
|
2
|
+
// RPi GPIO CONTROL TEST SCRIPT
|
|
3
|
+
// -------------------------------------------------------------------
|
|
4
|
+
import {log, trap, now} from "../esm/dev.mjs"
|
|
5
|
+
import {ctrlC} from "../esm/utl.mjs"
|
|
6
|
+
import {Rio} from "../esm/rio.mjs"
|
|
7
|
+
|
|
8
|
+
/** ------------------------------------------------------------------
|
|
9
|
+
* @function info
|
|
10
|
+
* @description Colored log for info messages Node.js only ~ CSS color #F50
|
|
11
|
+
* @param {String} cmd
|
|
12
|
+
* @param {String} txt
|
|
13
|
+
*/
|
|
14
|
+
function info(cmd, txt) {
|
|
15
|
+
console.log(now(), "🔎", "\x1b[38;2;0;160;16m", cmd, "\x1b[0m", txt)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** ------------------------------------------------------------------
|
|
19
|
+
* @function isNumber
|
|
20
|
+
* @description Test if param is number
|
|
21
|
+
* @param {String} str
|
|
22
|
+
* @return {Number}
|
|
23
|
+
*/
|
|
24
|
+
function isNumber(str) {
|
|
25
|
+
const num = parseInt(str)
|
|
26
|
+
if (typeof num !== "number" || num !== num) {
|
|
27
|
+
trap("<gpio_number> is not a number or not defined")
|
|
28
|
+
return -1
|
|
29
|
+
} else {
|
|
30
|
+
return num
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// -------------------------------------------------------------------
|
|
35
|
+
// TOP LEVEL TEST
|
|
36
|
+
// -------------------------------------------------------------------
|
|
37
|
+
(function () {
|
|
38
|
+
Rio.log(2)
|
|
39
|
+
ctrlC(Rio.stopMonitoring)
|
|
40
|
+
|
|
41
|
+
if (process.argv.length < 3) {
|
|
42
|
+
trap("Arguments are missing! Type 'help' to display list of commands.")
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
let gpio
|
|
46
|
+
switch (process.argv[2]) {
|
|
47
|
+
case "help":
|
|
48
|
+
info("help", "To display this list.")
|
|
49
|
+
info("detect", "To display gpio chipsets")
|
|
50
|
+
info("lines", "To display gpiochip0 lines")
|
|
51
|
+
info("version", "To display libgpiod version")
|
|
52
|
+
info("system", "To test if your system (hw,sw, mw) meets the tested requirements.")
|
|
53
|
+
info("config", "To display gpio system configuration.")
|
|
54
|
+
info("out <gpio_number>", "To define a gpio as 'out' and set it to 1 for 5s.")
|
|
55
|
+
info("in <gpio_number>", "To define a gpio as 'in' to display its value ")
|
|
56
|
+
info("monitor <gpio_number>", "To define a gpio as 'in' to monitor events until ctrl+c.")
|
|
57
|
+
info("pwm <gpio_number>", "Test SG90~like servo from 0° to 180° i.e. period=20ms, duty-cycle-range=0.5-2.5ms")
|
|
58
|
+
info("benchmark <gpio_out> <gpio_in>", "Displays elapsed time of 1000 write + read operations")
|
|
59
|
+
break
|
|
60
|
+
case "version":
|
|
61
|
+
log("libgpiod version:", Rio.version())
|
|
62
|
+
break
|
|
63
|
+
case "detect":
|
|
64
|
+
log("chipsets:", Rio.detect())
|
|
65
|
+
break
|
|
66
|
+
case "lines":
|
|
67
|
+
log("lines:", Rio.info())
|
|
68
|
+
break
|
|
69
|
+
case "system":
|
|
70
|
+
Rio.isSystemSupported()
|
|
71
|
+
break
|
|
72
|
+
case "config":
|
|
73
|
+
Rio.configs()
|
|
74
|
+
break
|
|
75
|
+
case "out":
|
|
76
|
+
gpio = isNumber(process.argv[3])
|
|
77
|
+
if (gpio > 0) {
|
|
78
|
+
const led = new Rio(gpio, "out")
|
|
79
|
+
led.set(1, 5000)
|
|
80
|
+
}
|
|
81
|
+
break
|
|
82
|
+
case "in":
|
|
83
|
+
gpio = isNumber(process.argv[3])
|
|
84
|
+
if (gpio > 0) {
|
|
85
|
+
const btn = new Rio(gpio, "in")
|
|
86
|
+
btn.get()
|
|
87
|
+
/* btn.monitor("both", (event, time) => {
|
|
88
|
+
log("event", event)
|
|
89
|
+
}) */
|
|
90
|
+
}
|
|
91
|
+
break
|
|
92
|
+
case "monitor":
|
|
93
|
+
gpio = isNumber(process.argv[3])
|
|
94
|
+
if (gpio > 0) {
|
|
95
|
+
const btn = new Rio(gpio, "in")
|
|
96
|
+
btn.monitor("both", (event) => {
|
|
97
|
+
log("event", event)
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
break
|
|
101
|
+
case "benchmark":
|
|
102
|
+
const gpioOut = isNumber(process.argv[3])
|
|
103
|
+
const gpioIn = isNumber(process.argv[4])
|
|
104
|
+
let len = 1000
|
|
105
|
+
if (gpioOut > 0) {
|
|
106
|
+
const led = new Rio(gpioOut, "out")
|
|
107
|
+
const start1 = new Date()
|
|
108
|
+
while (len--) {
|
|
109
|
+
led.write(1)
|
|
110
|
+
}
|
|
111
|
+
const stop1 = new Date()
|
|
112
|
+
const time1 = Math.abs(stop1 - start1)
|
|
113
|
+
log("Elapsed time for 1000 write ops:", time1, "ms")
|
|
114
|
+
}
|
|
115
|
+
len = 1000
|
|
116
|
+
if (gpioIn > 0) {
|
|
117
|
+
const btn = new Rio(gpioIn, "in")
|
|
118
|
+
const start2 = new Date()
|
|
119
|
+
while (len--) {
|
|
120
|
+
btn.read()
|
|
121
|
+
}
|
|
122
|
+
const stop2 = new Date()
|
|
123
|
+
const time2 = Math.abs(stop2 - start2)
|
|
124
|
+
log("Elapsed time for 1000 read ops:", time2, "ms")
|
|
125
|
+
}
|
|
126
|
+
break
|
|
127
|
+
case "pwm":
|
|
128
|
+
const servo = new Rio(isNumber(process.argv[3]), "pwm")
|
|
129
|
+
|
|
130
|
+
// 50hz, 0.5 ms
|
|
131
|
+
servo.pwmInit(20000000, 500000, {
|
|
132
|
+
dutyMin: 500000,
|
|
133
|
+
dutyMax: 2500000
|
|
134
|
+
})
|
|
135
|
+
log("servo position 0°")
|
|
136
|
+
|
|
137
|
+
// after 2s -> duty=1.5 ms
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
servo.pwmDuty(1500000)
|
|
140
|
+
log("servo position = 90°")
|
|
141
|
+
},2000)
|
|
142
|
+
|
|
143
|
+
// after 4s -> duty=1.5 ms
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
servo.pwmDuty(2500000)
|
|
146
|
+
log("servo position = 180°")
|
|
147
|
+
}, 4000)
|
|
148
|
+
|
|
149
|
+
// after 5s -> stop
|
|
150
|
+
setTimeout(() => {
|
|
151
|
+
servo.pwmStop()
|
|
152
|
+
log("servo stopped")
|
|
153
|
+
}, 5000)
|
|
154
|
+
break
|
|
155
|
+
default:
|
|
156
|
+
trap("Unknown command: Type 'test help' to display list of commands.")
|
|
157
|
+
}
|
|
158
|
+
})()
|
|
159
|
+
|
|
160
|
+
// -------------------------------------------------------------------
|
|
161
|
+
// EoF
|
|
162
|
+
// -------------------------------------------------------------------
|
package/rpi-io-1.0.0.tgz
DELETED
|
Binary file
|