rpi-io 3.0.0 → 3.1.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/CHANGELOG.md CHANGED
@@ -1,4 +1,11 @@
1
1
  # Changelog
2
+
3
+ ## [3.1.0] - 2026-07-22
4
+ ### Changed
5
+ - log utility refactored
6
+ ### Added
7
+ - extension classes for unipolar and bipolar stepper motors
8
+
2
9
  ## [3.0.0] - 2026-07-18
3
10
  ### Added
4
11
  - methods **pulseStart**(*count, edge, options*) and **pulseStop**() for output lines
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # rpi-io
2
2
  ![Static Badge](https://img.shields.io/badge/rpi--io-_3.y.z_-FF5500?style=flat) ![Static Badge](https://img.shields.io/badge/Nodejs-%3E_23-66cc33?logo=nodedotjs&logoColor=white) ![Static Badge](https://img.shields.io/badge/NPM-%3E_10-CC3534?logo=npm&logoColor=white) ![Static Badge](https://img.shields.io/badge/Raspberry_Pi-Zero2_4B_5B-C51A4A?logo=raspberrypi&logoColor=white) ![Static Badge](https://img.shields.io/badge/OS-Bookworm_Trixie-0D7AB9?style=flat)
3
3
 
4
- Current version is 3.0.0. The latest update introduces pulse sequence management methods for output lines. See [CHANGELOG.md](CHANGELOG.md) and documentation folder.
4
+ Current version is 3.1.0. This version takes advantage of the *pulseStart()* and *pulseStop()* methods introduced in version 3.0.0 to propose extension classes for stepper motor control. See [extension](https://github.com/gdorbes/rpi-io/tree/master/extensions) and [documentation](https://github.com/gdorbes/rpi-io/tree/master/documentation) folders.
5
5
 
6
6
  **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.
7
7
 
@@ -169,7 +169,7 @@ setTimeout(()=>{
169
169
  }, 5000)
170
170
  ```
171
171
 
172
- Pulse sequence
172
+ ##### Pulse sequence
173
173
 
174
174
  ```js
175
175
  import {RIO} from "rpi-io"
@@ -114,7 +114,7 @@ myOutput.pulseStart(10, "asc", {
114
114
  ```javascript
115
115
  {
116
116
  pulseWidth: 10, // pulse width in µs
117
- spaceWidth: 10 // space beween pulses in µs
117
+ spaceWidth: 10 // space between pulses in µs
118
118
  }
119
119
  ```
120
120
 
@@ -321,7 +321,6 @@ log("This is useful info")
321
321
  ```
322
322
 
323
323
 
324
-
325
324
  ### sleep(time)
326
325
 
327
326
  Similar to `setTimeout` but in async mode.
@@ -0,0 +1,192 @@
1
+ # Stepper Motor Extensions
2
+
3
+ Using the basic **rpi-io** methods for output lines (*write*, *pulseStart*, *pulseStop*), it is easy to drive stepper motors, ranging from simple hobbyist motors (e.g., 28BYJ-48) to those designed for robotics and industrial applications, such as the NEMA series (e.g., NEMA 17, 23...) using some hardware driver (e.g. DMA542).
4
+
5
+ To facilitate this type of use, since version 3.1 **rpi-io** has included two stepper motor controller classes:
6
+
7
+ - The *StepperUnipolar* class is intended for stepper motors without a hardware driver.
8
+ - The *StepperBipolar* class is intended for stepper motors with a hardware driver such as the DM542.
9
+
10
+
11
+
12
+ ## class StepperUnipolar
13
+
14
+ ### Electronic wiring example
15
+
16
+ ![](https://rpisquare.com//img/stepper-unipolar.png)
17
+
18
+ PLEASE NOTE:
19
+
20
+ - **GND is shared** — the Pi's GND and the ULN2003's GND must be tied together even if you power the motor from a separate supply, or the signals won't have a common reference.
21
+
22
+ - **Power source for VCC**: for light loads the Pi's 5V pin can drive the ULN2003/motor directly, but the 28BYJ-48 can draw enough current that it's safer to power VCC from a separate 5V supply, keeping only GND common with the Pi.
23
+
24
+ - The four GPIO pins (17, 18, 27, 22) are just a common convention — any four free GPIO pins work, as long as your code (e.g. via `RPi.GPIO` or `gpiozero`) references the pins you actually wired.
25
+
26
+ - The ULN2003 board's output side connects to the motor with its keyed 5-pin connector — it only fits one way, so no wiring decisions needed there.
27
+
28
+
29
+
30
+ ### Example
31
+
32
+ See test [script](https://github.com/gdorbes/rpi-io/blob/master/test/stepper-motor-unipolar.js).
33
+
34
+ ```js
35
+ import {StepperUnipolar, RIO, traceCfg, log, warn, sleep} from "../extensions/stepper-unipolar.mjs"
36
+
37
+ traceCfg(2)
38
+
39
+ // Open step controller in half step mode => Half turn forward and backward => Close
40
+ const motor28BYJH = new StepperUnipolar(new RIO(21, "output"), new RIO(22, "output"), new RIO(24, "output"), new RIO(25, "output"), "half", 0)
41
+ log("motor28BYJH creation status:", motor28BYJH.status)
42
+ await motor28BYJH.rotate(2048, "forward", false)
43
+ await motor28BYJH.rotate(2048, "backward", false)
44
+ motor28BYJH.close()
45
+
46
+ // Open step controller in full step mode => Half turn forward and backward => Close
47
+ const motor28BYJF = new StepperUnipolar(new RIO(21, "output"), new RIO(22, "output"), new RIO(24, "output"), new RIO(25, "output"), "full", 4)
48
+ log("motor28BYJF creation status:", motor28BYJF.status)
49
+ await motor28BYJF.rotate(1024, "forward", false)
50
+ await motor28BYJF.rotate(1024, "backward", false)
51
+ motor28BYJF.close()
52
+ ```
53
+
54
+
55
+
56
+ ### API
57
+
58
+ #### constructor StepperUnipolar(coilA, coilB, coilC, coilD, mode, delay)
59
+
60
+ **Parameter(s)**
61
+
62
+ - **coilA** *{Object}* **rpi-io** instance for Coil A
63
+
64
+ - **coilB** *{Object}* **rpi-io** instance for Coil B
65
+
66
+ - **coilC** *{Object}* **rpi-io** instance for Coil C
67
+
68
+ - **coilD** *{Object}* **rpi-io** instance for Coil D
69
+
70
+ - ##### **mode** *{'half','full'}* sequence mode: half step or full step
71
+
72
+ - **delay** *{Number}* delay in ms between steps
73
+
74
+
75
+
76
+ **Return** *{Object}* StepperUnipolar instance
77
+
78
+
79
+
80
+ #### rotate(steps, direction, showStep)
81
+
82
+ **Parameter(s)**
83
+
84
+ - **steps** *{Number}*
85
+ - **direction** *{'forward'|'backward"'*
86
+ - **showStep** *{Boolean}* ** to display step in console (default = false)
87
+
88
+ **Return** *{Promise}*
89
+
90
+
91
+
92
+ #### close()
93
+
94
+ To disable coils and close rpi-io instances.
95
+
96
+
97
+
98
+ ## class StepperBipolar
99
+
100
+ ### Electronic wiring example
101
+
102
+ ![](https://rpisquare.com//img/stepper-bipolar.png)
103
+
104
+ PLEASE NOTE:
105
+
106
+ - **The junction dot** is where the Pi's 5V pin splits into two wires, tying PUL+ and DIR+ together onto one common rail.
107
+
108
+ - **STEP and DIR switch low to trigger**, since GPIO pulls PUL- or DIR- down relative to the shared +5V.
109
+
110
+ - **Voltage margin is tight**: the Pi's GPIO output high is 3.3V, but the common rail here is 5V, giving only ~1.7V across the optocoupler when "off" — close to some optocouplers' turn-off threshold. Many builds get away with this, but if you see erratic stepping consider using a Darlington array (e.g. ULN2003) between RPi and DM542.
111
+
112
+ - **ENA+/ENA-** would follow the same common-anode pattern if you wire it — tied into the same 5V junction, with a GPIO pin driving ENA- low to enable.
113
+
114
+
115
+
116
+ ### Example
117
+
118
+ See test [script](https://github.com/gdorbes/rpi-io/blob/master/test/stepper-motor-bipolar.js).
119
+
120
+ ```js
121
+ import {StepperBipolar, RIO, traceCfg, log, warn, sleep, ctrlC} from "../extensions/stepper-bipolar.mjs"
122
+
123
+ traceCfg(2)
124
+
125
+ ctrlC(() => {
126
+ RIO.closeAll()
127
+ })
128
+
129
+ // Open output lines GPIO21 and GPIO22
130
+ const pul = new RIO(21, "output", {value: 0})
131
+ const dir = new RIO(22, "output", {value: 0})
132
+
133
+ // Create Bipolar Stepper Motor instance
134
+ const bipolar = new StepperBipolar(pul, dir, "desc", 10)
135
+
136
+ // Start rotating full turn (depending on hardware driver settings)
137
+ bipolar.rotate(25600, "forward").then(stepped => {
138
+ log("stepped:", stepped)
139
+ // Reset and close lines
140
+ pul.write(0)
141
+ dir.write(0)
142
+ pul.close()
143
+ dir.close()
144
+ })
145
+
146
+ // Stop rotation after 200ms
147
+ await sleep(200)
148
+ log("stop rotation")
149
+ bipolar.stop()
150
+ ```
151
+
152
+
153
+
154
+ ### API
155
+
156
+ #### constructor StepperBipolar(pul, dir, edge, width)
157
+
158
+ **Parameter(s)**
159
+
160
+ - **pul** *{Object}* **rpi-io** instance for pulse pin
161
+
162
+ - **dir** *{Object}* **rpi-io** instance for direction pin
163
+
164
+ - ##### **edge** *{'desc','asc'}* pulse active edge: descending or ascending
165
+
166
+ - **width** *{Number}* pulse width and space between two edges in µs
167
+
168
+ **Return** *{Object}* StepperBipolar instance
169
+
170
+
171
+
172
+ #### rotate(steps, direction)
173
+
174
+ **Parameter(s)**
175
+
176
+ - **steps** *{Number}*
177
+ - **direction** *{'forward'|'backward"'*
178
+ - **showStep** *{Boolean}* ** to display step in console (default = false)
179
+
180
+ **Return** *{Promise<{elapsedMs: number, pulsesCompleted: number, stopped: boolean}>}*
181
+
182
+
183
+
184
+ #### stop()
185
+
186
+ To interrupt an ongoing pulse sequence if one is in progress.
187
+
188
+
189
+
190
+ #### close()
191
+
192
+ To disable *pul* and *dir* rpi-io instances.
package/esm/log.mjs CHANGED
@@ -1,58 +1,48 @@
1
1
  // -------------------------------------------------------------------
2
2
  // RPI-IO: Custom console.log
3
3
  // -------------------------------------------------------------------
4
- let debug = {
5
- trace: false,
6
- warn: true
4
+ const noop = function () {
7
5
  }
8
-
6
+ export let log = noop
7
+ export let warn = noop
9
8
  /** ------------------------------------------------------------------
10
9
  * @function traceCfg
11
10
  * @description Set log configuration
12
11
  * @param {Number} level 0: no log, 1: warn only, 2: warn and log
13
12
  */
14
13
  export const traceCfg = level => {
15
- debug = {
16
- trace: false,
17
- warn: false
18
- }
19
- level > 1 ? debug.trace = true : false
20
- level > 0 ? debug.warn = true : false
21
- }
22
14
 
23
- /** ------------------------------------------------------------------
24
- * @function nowStr
25
- * @description Return timestamp as formatted string
26
- * @return {String}
27
- */
28
- const nowStr = () => {
29
- const date = new Date()
30
- return date.toLocaleTimeString() + "." + date.getMilliseconds().toLocaleString('en', {
31
- minimumIntegerDigits: 3,
32
- minimumFractionDigits: 0,
33
- useGrouping: false
34
- })
35
- }
36
-
37
- /** ------------------------------------------------------------------
38
- * @function log
39
- * @description Global customized timestamped console.log
40
- * Requires global variable 'debug'
41
- */
42
- export const log = function () {
43
- if (debug.trace)
15
+ const nowStr = () => {
16
+ const date = new Date()
17
+ return date.toLocaleTimeString() + "." + date.getMilliseconds().toLocaleString('en', {
18
+ minimumIntegerDigits: 3,
19
+ minimumFractionDigits: 0,
20
+ useGrouping: false
21
+ })
22
+ }
23
+ const timeStamped = function () {
44
24
  console.log.apply(console, Array.prototype.concat.apply([nowStr(), "🔎 "], arguments))
45
- }
25
+ }
26
+ const timeColored = function () {
27
+ console.log.apply(console, Array.prototype.concat.apply([nowStr(), "⚠️ ", "\x1b[38;2;255;80;0m", ...arguments, "\x1b[0m"]))
28
+ }
46
29
 
47
- /** ------------------------------------------------------------------
48
- * @function warn
49
- * @description Colored log for error messages ⚠️ nodejs only ~ CSS color #F50
50
- */
51
- export const warn = function () {
52
- if (debug.warn)
53
- console.log.apply(console, Array.prototype.concat.apply([nowStr(), "⚠️", "\x1b[38;2;255;80;0m", ...arguments, "\x1b[0m"]))
30
+ switch (level) {
31
+ case 2:
32
+ warn = timeColored
33
+ log = timeStamped
34
+ break
35
+ case 1:
36
+ warn = timeColored
37
+ log = noop
38
+ break
39
+ default:
40
+ warn = noop
41
+ log = noop
42
+ }
54
43
  }
55
44
 
45
+
56
46
  // -------------------------------------------------------------------
57
47
  // EoF
58
48
  // -------------------------------------------------------------------
@@ -0,0 +1,131 @@
1
+ // -------------------------------------------------------------------
2
+ // RPI-IO: BIPOLAR STEPPER MOTOR EXTENSION
3
+ // -------------------------------------------------------------------
4
+ import {RIO, traceCfg, log, warn, sleep, ctrlC} from "../esm/main.mjs"
5
+
6
+ export {RIO, traceCfg, log, warn, sleep, ctrlC}
7
+
8
+ /** ------------------------------------------------------------------
9
+ * @class StepperBipolar
10
+ * @description Class definition for Bipolar Stepper motors.
11
+ * Bipolar stepper motors (e.g. NEMA 17 or 23) require hardware driver
12
+ * such as DM542 or equilavent depending on motor power
13
+ */
14
+ export class StepperBipolar {
15
+
16
+ static STATUS_MSG = {
17
+ 0: "none",
18
+ 11: "pul or dir rpi-io instance definition is missing",
19
+ 12: "At least one rpi-io instance is not valid",
20
+ 13: "Wrong edge {'asc','desc'}",
21
+ 14: "Wrong pulse width (5 ≤ p ≤ 10,000,000)",
22
+ 200: "ok"
23
+ }
24
+
25
+ static EDGES = ["desc", "asc"]
26
+
27
+ // Private method: Update instance status
28
+ #setStatus(code, data) {
29
+ this.status.code = code
30
+ this.status.msg = StepperBipolar.STATUS_MSG[code]
31
+ this.status.data = data
32
+ }
33
+
34
+ /** ------------------------------------------------------------------
35
+ * @method constructor
36
+ * @param {Object} pul // rpi-io output instance for pulse pin
37
+ * @param {Object} dir // rpi-io output instance for direction pin
38
+ * @param {"desc"|"asc"} edge // pulse active edge: descending or ascending
39
+ * @param {Number} width // pulse width and space between two edges in µs
40
+ */
41
+ constructor(pul, dir, edge, width) {
42
+
43
+ // Init instance construction status
44
+ this.status = {
45
+ code: 0,
46
+ msg: StepperBipolar.STATUS_MSG[0],
47
+ data: ""
48
+ }
49
+
50
+ // ERR11: pul or pir rpi-io instance definition is missing
51
+ if (typeof pul === "undefined" || typeof dir === "undefined") {
52
+ this.#setStatus(11, "")
53
+ RIO.closeAll()
54
+ return
55
+ }
56
+
57
+ // ERR12: At least one coil port rpi-io instance is not valid
58
+ try {
59
+ pul.write(0)
60
+ dir.write(0)
61
+ } catch (err) {
62
+ this.#setStatus(12, "")
63
+ RIO.closeAll()
64
+ return
65
+ }
66
+
67
+ // ERR13: Wrong edge {'asc','desc'}
68
+ if (StepperBipolar.EDGES.indexOf(edge) === -1) {
69
+ this.#setStatus(13, edge)
70
+ RIO.closeAll()
71
+ return
72
+ }
73
+
74
+ // ERR14: Wrong pulse width (5 ≤ p ≤ 10,000,000)
75
+ if (typeof width !== "number" || width < 5 || width > 10000000) {
76
+ this.#setStatus(14, width)
77
+ RIO.closeAll()
78
+ return
79
+ }
80
+
81
+ // Test successfully completed => Init properties
82
+ this.ioPulse = pul
83
+ this.ioDir = dir
84
+ this.edge = edge
85
+ this.width = width
86
+ this.#setStatus(200, "")
87
+ }
88
+
89
+ /** ------------------------------------------------------------------
90
+ * @method rotate
91
+ * @description Rotate the motor a certain number of steps forward or backward.
92
+ * Full rotation depends on hardware driver settings.
93
+ * @param {Number} steps
94
+ * @param {"forward"|"backward"} direction
95
+ * @return {Promise<{elapsedMs: number, pulsesCompleted: number, stopped: boolean}>}
96
+ */
97
+ async rotate(steps, direction) {
98
+
99
+ // Set direction
100
+ this.ioDir.write("forward" ? 1 : 0)
101
+
102
+ // Start rotation
103
+ log("starting rotation:", steps, "steps", direction)
104
+ return await this.ioPulse.pulseStart(steps, this.edge, {
105
+ pulseWidth: this.width,
106
+ spaceWidth: this.width,
107
+ })
108
+ }
109
+
110
+ /** ------------------------------------------------------------------
111
+ * @method stop
112
+ * @description Immediately interrupts an ongoing pulse sequence if one is in progress.
113
+ */
114
+ stop() {
115
+ this.ioPulse.pulseStop()
116
+ }
117
+
118
+ /** ------------------------------------------------------------------
119
+ * @method close
120
+ * @description Disable pul and dir rpi-io instances used for this step motor controller
121
+ */
122
+ close() {
123
+ this.ioPulse.close
124
+ this.ioDir.close
125
+ log("Bipolar controller closed")
126
+ }
127
+ }
128
+
129
+ // -------------------------------------------------------------------
130
+ // EoF
131
+ // -------------------------------------------------------------------
@@ -0,0 +1,172 @@
1
+ // -------------------------------------------------------------------
2
+ // RPI-IO: UNIPOLAR STEPPER MOTOR EXTENSION
3
+ // -------------------------------------------------------------------
4
+ import {RIO, traceCfg, log, warn, sleep} from "../esm/main.mjs"
5
+
6
+ export {RIO, traceCfg, log, warn, sleep}
7
+
8
+ /** ------------------------------------------------------------------
9
+ * @class StepperUnipolar
10
+ * @description Class definition for Unipolar Stepper motors.
11
+ * Unipolar stepper motors (e.g. 28BYJ-48) are directly controlled by RPi using
12
+ * a Darlington Transistor Array (e.g. ULN2003)
13
+ */
14
+ export class StepperUnipolar {
15
+
16
+ static STATUS_MSG = {
17
+ 0: "none",
18
+ 11: "At least one coil port rpi-io instance definition is missing",
19
+ 12: "At least one coil port rpi-io instance is not valid",
20
+ 13: "Wrong delay parameter value (0 ≤ d ≤ 1000)",
21
+ 14: "Wrong Unipolar sequence {'full','half'}",
22
+ 200: "ok"
23
+ }
24
+ static MODE = ["half", "full"]
25
+ static STEPS = {
26
+ half: [
27
+ [1, 0, 0, 0],
28
+ [1, 1, 0, 0],
29
+ [0, 1, 0, 0],
30
+ [0, 1, 1, 0],
31
+ [0, 0, 1, 0],
32
+ [0, 0, 1, 1],
33
+ [0, 0, 0, 1],
34
+ [1, 0, 0, 1]
35
+ ],
36
+ full: [
37
+ [1, 0, 0, 1],
38
+ [1, 1, 0, 0],
39
+ [0, 1, 1, 0],
40
+ [0, 0, 1, 1]
41
+ ]
42
+ }
43
+
44
+ // Private method: Update instance status
45
+ #setStatus(code, data) {
46
+ this.status.code = code
47
+ this.status.msg = StepperUnipolar.STATUS_MSG[code]
48
+ this.status.data = data
49
+ }
50
+
51
+ /** ------------------------------------------------------------------
52
+ * @method constructor
53
+ * @param {Object} coilA // rpi-io output instance for CoilA
54
+ * @param {Object} coilB // rpi-io output instance for CoilB
55
+ * @param {Object} coilC // rpi-io output instance for CoilC
56
+ * @param {Object} coilD // rpi-io output instance for CoilD
57
+ * @param {"half"|"ful"} mode // Full or half step sequence
58
+ * @param {Number} delay // Delay between steps in ms
59
+ */
60
+ constructor(coilA, coilB, coilC, coilD, mode = "half", delay = 10) {
61
+
62
+ // Init instance construction status
63
+ this.status = {
64
+ code: 0,
65
+ msg: StepperUnipolar.STATUS_MSG[0],
66
+ data: ""
67
+ }
68
+
69
+ // ERR11: At least one coil port rpi-io instance definition is missing
70
+ if (typeof coilA === "undefined" || typeof coilB === "undefined" || typeof coilC === "undefined" || typeof coilD === "undefined") {
71
+ this.#setStatus(11, "")
72
+ RIO.closeAll()
73
+ return
74
+ }
75
+
76
+ // ERR12: At least one coil port rpi-io instance is not valid
77
+ try {
78
+ coilA.write(0)
79
+ coilB.write(0)
80
+ coilC.write(0)
81
+ coilD.write(0)
82
+ } catch (err) {
83
+ this.#setStatus(12, "")
84
+ RIO.closeAll()
85
+ return
86
+ }
87
+
88
+ // ERR13: Wrong delay parameter value (0 ≤ d ≤ 1000)
89
+ if (typeof delay !== "number" || delay < 0 || delay > 1000) {
90
+ this.#setStatus(13, delay)
91
+ RIO.closeAll()
92
+ return
93
+ }
94
+
95
+ // ERR14: Wrong mode {'full','half'}
96
+ if (StepperUnipolar.MODE.indexOf(mode) === -1) {
97
+ this.#setStatus(14, mode)
98
+ RIO.closeAll()
99
+ return
100
+ }
101
+
102
+ // Test successfully completed => Init properties
103
+ this.coils = {
104
+ a: coilA,
105
+ b: coilB,
106
+ c: coilC,
107
+ d: coilD,
108
+ }
109
+ this.sequence = StepperUnipolar.STEPS[mode]
110
+ this.len = this.sequence.length
111
+ this.delay = delay
112
+ this.index = 0
113
+ this.increment = 0
114
+ this.#setStatus(200, "")
115
+ }
116
+
117
+ /** ------------------------------------------------------------------
118
+ * @method rotate
119
+ * @description Rotate the motor a certain number of steps forward or backward.
120
+ * Full rotation depends on selected mode.
121
+ * @param {Number} steps
122
+ * @param {"forward"|"backward"} direction
123
+ * @param {Boolean} showStep
124
+ */
125
+ async rotate(steps, direction, showStep = false) {
126
+
127
+ this.increment = direction === "forward" ? 1 : -1
128
+
129
+ log("starting rotation:", steps, "steps", direction)
130
+
131
+ // Rotate
132
+ for (let i = 0; i < steps; i++) {
133
+ const [a, b, c, d] = this.sequence[this.index]
134
+ if (showStep) {
135
+ log("rotation step:", i, [a, b, c, d])
136
+ }
137
+ this.coils.a.write(a)
138
+ this.coils.b.write(b)
139
+ this.coils.c.write(c)
140
+ this.coils.d.write(d)
141
+
142
+ this.index = ((this.index + this.increment % this.len + this.len) % this.len)
143
+ await sleep(this.delay, false)
144
+ }
145
+
146
+ // Disable coils
147
+ this.coils.a.write(0)
148
+ this.coils.b.write(0)
149
+ this.coils.c.write(0)
150
+ this.coils.d.write(0)
151
+ }
152
+
153
+ /** ------------------------------------------------------------------
154
+ * @method close
155
+ * @description Disable coils and close rpi-io instances used for this step motor controller
156
+ */
157
+ close() {
158
+ this.coils.a.write(0)
159
+ this.coils.b.write(0)
160
+ this.coils.c.write(0)
161
+ this.coils.d.write(0)
162
+ this.coils.a.close()
163
+ this.coils.b.close()
164
+ this.coils.c.close()
165
+ this.coils.d.close()
166
+ log("Unipolar controller closed")
167
+ }
168
+ }
169
+
170
+ // -------------------------------------------------------------------
171
+ // EoF
172
+ // -------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rpi-io",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Nodejs module to control Raspberry Pi GPIO",
5
5
  "keywords": [
6
6
  "raspberry",
@@ -18,7 +18,11 @@
18
18
  "bookworm",
19
19
  "trixie",
20
20
  "iot",
21
- "embedded"
21
+ "embedded",
22
+ "motor",
23
+ "stepper",
24
+ "motor control",
25
+ "stepper motor"
22
26
  ],
23
27
  "homepage": "https://github.com/gdorbes/rpi-io#readme",
24
28
  "bugs": {
@@ -0,0 +1,41 @@
1
+ // -------------------------------------------------------------------
2
+ // TEST CUSTOM CONSOLE
3
+ // -------------------------------------------------------------------
4
+ import {traceCfg, log, warn} from "../esm/log.mjs"
5
+
6
+ // Default behavior: No console
7
+ log("nothing")
8
+ warn("nothing")
9
+
10
+ // Full console
11
+ traceCfg(2)
12
+ log("console display")
13
+ warn("warning display")
14
+
15
+ // Warning only
16
+ traceCfg(1)
17
+ log("not displayed")
18
+ warn("warning display again")
19
+
20
+ // Back to no console
21
+ traceCfg(0)
22
+ log("nothing again")
23
+ warn("nothing again")
24
+
25
+ // Full console
26
+ traceCfg(2)
27
+ log("console display again")
28
+ warn("warning display again")
29
+
30
+ // Warning only
31
+ traceCfg(1)
32
+ log("not displayed")
33
+ warn("warning display again again")
34
+
35
+ // Back to no console
36
+ traceCfg(0)
37
+ log("nothing again")
38
+ warn("nothing again")
39
+ // -------------------------------------------------------------------
40
+ // EoF
41
+ // -------------------------------------------------------------------
@@ -0,0 +1,36 @@
1
+ // -------------------------------------------------------------------
2
+ // RPI-IO: BIPOLAR STEPPER MOTOR TEST - DM542 HARDWARE DRIVER
3
+ // -------------------------------------------------------------------
4
+ import {StepperBipolar, RIO, traceCfg, log, warn, sleep, ctrlC} from "../extensions/stepper-bipolar.mjs"
5
+
6
+ traceCfg(2)
7
+
8
+ ctrlC(() => {
9
+ RIO.closeAll()
10
+ })
11
+
12
+ // Open output lines GPIO21 and GPIO22
13
+ const pul = new RIO(21, "output", {value: 0})
14
+ const dir = new RIO(22, "output", {value: 0})
15
+
16
+ // Create Bipolar Stepper Motor instance
17
+ const bipolar = new StepperBipolar(pul, dir, "desc", 10)
18
+
19
+ // Start rotating full turn (depending on hardware driver settings)
20
+ bipolar.rotate(25600, "forward").then(stepped => {
21
+ log("stepped:", stepped)
22
+ // Reset and close lines
23
+ pul.write(0)
24
+ dir.write(0)
25
+ pul.close()
26
+ dir.close()
27
+ })
28
+
29
+ // Stop rotation after 200ms
30
+ await sleep(200)
31
+ log("stop rotation")
32
+ bipolar.stop()
33
+
34
+ // -------------------------------------------------------------------
35
+ // EoF
36
+ // -------------------------------------------------------------------
@@ -0,0 +1,24 @@
1
+ // -------------------------------------------------------------------
2
+ // RPI-IO: UNIPOLAR STEPPER MOTOR TEST
3
+ // -------------------------------------------------------------------
4
+ import {StepperUnipolar, RIO, traceCfg, log, warn, sleep} from "../extensions/stepper-unipolar.mjs"
5
+
6
+ traceCfg(2)
7
+
8
+ // Open step controller in half step mode => Half turn forward and backward => Close
9
+ const motor28BYJH = new StepperUnipolar(new RIO(21, "output"), new RIO(22, "output"), new RIO(24, "output"), new RIO(25, "output"), "half", 0)
10
+ log("motor28BYJH creation status:", motor28BYJH.status)
11
+ await motor28BYJH.rotate(2048, "forward", false)
12
+ await motor28BYJH.rotate(2048, "backward", false)
13
+ motor28BYJH.close()
14
+
15
+ // Open step controller in full step mode => Half turn forward and backward => Close
16
+ const motor28BYJF = new StepperUnipolar(new RIO(21, "output"), new RIO(22, "output"), new RIO(24, "output"), new RIO(25, "output"), "full", 4)
17
+ log("motor28BYJF creation status:", motor28BYJF.status)
18
+ await motor28BYJF.rotate(1024, "forward", false)
19
+ await motor28BYJF.rotate(1024, "backward", false)
20
+ motor28BYJF.close()
21
+
22
+ // -------------------------------------------------------------------
23
+ // EoF
24
+ // -------------------------------------------------------------------
package/esm/step.mjs DELETED
@@ -1,212 +0,0 @@
1
- // -------------------------------------------------------------------
2
- // RPI-IO: STEP MOTOR EXTENSION ⚠️ Work in Progress
3
- // -------------------------------------------------------------------
4
- import {RIO} from "./main.mjs";
5
- // -------------------------------------------------------------------
6
- // UTILITY FUNCTIONS
7
- /** ------------------------------------------------------------------
8
- * @function tlog
9
- * @description Timestamped console.log
10
- */
11
- export function tlog() {
12
- const date = new Date()
13
- const nowMs = date.toLocaleTimeString() + "." + date.getMilliseconds().toLocaleString('en', {
14
- minimumIntegerDigits: 3,
15
- minimumFractionDigits: 0,
16
- useGrouping: false
17
- })
18
- console.log.apply(console, Array.prototype.concat.apply([nowMs, "🔎 "], arguments))
19
- }
20
-
21
- /** ------------------------------------------------------------------
22
- * @function wait
23
- * @description Wait ms as a promise and optional tlog
24
- * @param {Number} ms
25
- * @param {Boolean} logWait
26
- */
27
- export function wait(ms, logWait = true) {
28
- logWait ? tlog("sleeping", ms, "ms") : false
29
- return new Promise(resolve => setTimeout(resolve, ms));
30
- }
31
-
32
- export class STEP {
33
-
34
- static STATUS_MSG = {
35
- 0: "none",
36
- 1: "Unknown driver name",
37
- 11: "At least one coil port rpi-io instance definition is missing",
38
- 12: "At least one coil port rpi-io instance is not valid",
39
- 13: "Wrong delay parameter value (0 ≤ d ≤ 1000)",
40
- 14: "Wrong ULN2003 sequence {'full','half'}",
41
- 200: "ok"
42
- }
43
- static ULN2003_MODE = ["half", "full"]
44
- static ULN2003_STEPS = {
45
- half: [
46
- [1, 0, 0, 0],
47
- [1, 1, 0, 0],
48
- [0, 1, 0, 0],
49
- [0, 1, 1, 0],
50
- [0, 0, 1, 0],
51
- [0, 0, 1, 1],
52
- [0, 0, 0, 1],
53
- [1, 0, 0, 1]
54
- ],
55
- full: [
56
- [1, 0, 0, 1],
57
- [1, 1, 0, 0],
58
- [0, 1, 1, 0],
59
- [0, 0, 1, 1]
60
- ]
61
- }
62
-
63
- // Private method: Update instance status
64
- #setStatus(code, data) {
65
- this.status.code = code
66
- this.status.msg = STEP.STATUS_MSG[code]
67
- this.status.data = data
68
- }
69
-
70
- /** ------------------------------------------------------------------
71
- * @method constructor
72
- * @param {String} driver // Supported driver name
73
- * @param {Object} rios // rpi-io Instance container depending on driver
74
- * @param {Object} opt // Options depending on driver
75
- */
76
- constructor(driver, rios, opt) {
77
-
78
- const defopt = {
79
- // Parameters for ULN2003
80
- delay: 10,
81
- sequence: "half"
82
- }
83
- opt = {...defopt, ...opt}
84
-
85
- // Init instance construction status
86
- this.status = {
87
- code: 0,
88
- msg: STEP.STATUS_MSG[0],
89
- data: ""
90
- }
91
-
92
- // Test parameters depending on driver
93
- switch (driver) {
94
- case "uln2003":
95
-
96
- // ERR11: At least one coil port rpi-io instance definition is missing
97
- if (typeof rios.coilA === "undefined" || typeof rios.coilB === "undefined" || typeof rios.coilC === "undefined" || typeof rios.coilD === "undefined") {
98
- this.#setStatus(11, "")
99
- RIO.closeAll()
100
- return
101
- }
102
-
103
- // ERR12: At least one coil port rpi-io instance is not valid
104
- try {
105
- rios.coilA.write(0)
106
- rios.coilB.write(0)
107
- rios.coilC.write(0)
108
- rios.coilD.write(0)
109
- } catch (err) {
110
- this.#setStatus(12, "")
111
- RIO.closeAll()
112
- return
113
- }
114
-
115
- // ERR13: Wrong delay parameter value (0 ≤ d ≤ 1000)
116
- if (typeof opt.delay !== "number" || opt.delay < 0 || opt.delay > 1000) {
117
- this.#setStatus(13, opt.delay)
118
- RIO.closeAll()
119
- return
120
- }
121
-
122
- // ERR14: Wrong ULN2003 sequence {'full','half'}
123
- if (STEP.ULN2003_MODE.indexOf(opt.sequence) === -1) {
124
- this.#setStatus(14, opt.sequence)
125
- RIO.closeAll()
126
- return
127
- }
128
-
129
- // Test successfully completed => Init properties
130
- this.driver = "uln2003"
131
- this.props = {
132
- a: rios.coilA,
133
- b: rios.coilB,
134
- c: rios.coilC,
135
- d: rios.coilD,
136
- sequence: STEP.ULN2003_STEPS[opt.sequence],
137
- len: STEP.ULN2003_STEPS[opt.sequence].length,
138
- delay: opt.delay,
139
- inc: 0,
140
- index: 0
141
- }
142
- this.#setStatus(200, "")
143
- break
144
- default:
145
- this.#setStatus(1, driver)
146
- }
147
- }
148
-
149
- /** ------------------------------------------------------------------
150
- * @method rotate
151
- * @description Rotate the motor a certain number of steps forward or backward.
152
- * A full rotation consists of 200 steps.
153
- * @param {Number} steps
154
- * @param {"forward"|"backward"} direction
155
- * @param {Boolean} showStep
156
- */
157
- async rotate(steps, direction, showStep = false) {
158
-
159
- switch (this.driver) {
160
- case "uln2003":
161
- this.props.inc = direction === "forward" ? 1 : -1
162
-
163
- tlog("starting rotation:", steps, "steps", direction)
164
-
165
- // Rotate
166
- for (let i = 0; i < steps; i++) {
167
- const [a, b, c, d] = this.props.sequence[this.props.index]
168
- if (showStep) {
169
- tlog("rotation step:", i, [a, b, c, d])
170
- }
171
- this.props.a.write(a)
172
- this.props.b.write(b)
173
- this.props.c.write(c)
174
- this.props.d.write(d)
175
-
176
- this.props.index = ((this.props.index + this.props.inc) % this.props.len + this.props.len) % this.props.len
177
- await wait(this.props.delay, false)
178
- }
179
-
180
- // Disable coils
181
- this.props.a.write(0)
182
- this.props.b.write(0)
183
- this.props.c.write(0)
184
- this.props.d.write(0)
185
- }
186
- }
187
-
188
- /** ------------------------------------------------------------------
189
- * @method close
190
- * @description Disable coils and close rpi-io instances used for step motor controller
191
- */
192
- close() {
193
-
194
- switch (this.driver) {
195
- case "uln2003":
196
- this.props.a.write(0)
197
- this.props.b.write(0)
198
- this.props.c.write(0)
199
- this.props.d.write(0)
200
- this.props.a.close()
201
- this.props.b.close()
202
- this.props.c.close()
203
- this.props.d.close()
204
- tlog("ULN2003 controller closed")
205
- }
206
- }
207
- }
208
-
209
-
210
- // -------------------------------------------------------------------
211
- // EoF
212
- // -------------------------------------------------------------------
@@ -1,59 +0,0 @@
1
- // -------------------------------------------------------------------
2
- // RPI-IO: STEP MOTOR TEST SCRIPT ⚠️ Work in Progress
3
- // -------------------------------------------------------------------
4
- import {RIO} from "../esm/main.mjs";
5
- import {STEP, tlog} from "../esm/step.mjs"
6
-
7
- // Close any open rpi-io instance to prevent error
8
- RIO.closeAll()
9
-
10
- // Open step controller in half step mode => Half turn forward and backward => Close
11
- const motor28BYJH = new STEP("uln2003", {
12
- coilA: new RIO(21, "output"),
13
- coilB: new RIO(22, "output"),
14
- coilC: new RIO(24, "output"),
15
- coilD: new RIO(25, "output")
16
- }, {
17
- delay: 0,
18
- sequence: "half"
19
- })
20
- tlog("motor28BYJH creation status:", motor28BYJH.status)
21
- await motor28BYJH.rotate(2048, "forward", false)
22
- await motor28BYJH.rotate(2048, "backward", false)
23
- motor28BYJH.close()
24
-
25
- // Open step controller in full step mode => Half turn forward and backward => Close
26
- const motor28BYJF = new STEP("uln2003", {
27
- coilA: new RIO(21, "output"),
28
- coilB: new RIO(22, "output"),
29
- coilC: new RIO(24, "output"),
30
- coilD: new RIO(25, "output")
31
- }, {
32
- delay: 4,
33
- sequence: "full"
34
- })
35
- tlog("motor28BYJF creation status:", motor28BYJF.status)
36
- await motor28BYJF.rotate(1024, "forward", false)
37
- await motor28BYJF.rotate(1024, "backward", false)
38
- motor28BYJF.close()
39
-
40
- // A4988 drivers require one line per parameter as in the example below
41
- const a4988 = {
42
- pulse: new RIO(5, "output"),
43
- direction: new RIO(6, "output"),
44
- enable: new RIO(26, "output")
45
- }
46
-
47
- // DM452 drivers require one line per parameter as in the example below
48
- const dm452 = {
49
- pulse: new RIO(16, "output"),
50
- direction: new RIO(17, "output"),
51
- polarity: new RIO(27, "output")
52
- }
53
-
54
- // When done close all rpi-io instances
55
- RIO.closeAll()
56
-
57
- // -------------------------------------------------------------------
58
- // EoF
59
- // -------------------------------------------------------------------