rpi-io 2.2.4 → 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 +14 -3
- package/README.md +21 -4
- package/addon/gpio.c +435 -4
- package/documentation/api.md +67 -1
- package/documentation/examples.md +3 -0
- package/documentation/stepper-motor.md +192 -0
- package/esm/log.mjs +30 -40
- package/esm/main.mjs +56 -3
- package/extensions/stepper-bipolar.mjs +131 -0
- package/extensions/stepper-unipolar.mjs +172 -0
- package/package.json +6 -2
- package/test/log-warn.js +41 -0
- package/test/pulse.js +43 -0
- package/test/read.js +2 -0
- package/test/stepper-motor-bipolar.js +36 -0
- package/test/stepper-motor-unipolar.js +24 -0
- package/test/write.js +1 -0
- package/esm/uln2003.mjs +0 -115
- package/test/exampleUln2003.js +0 -103
|
@@ -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
|
+

|
|
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
|
+

|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
// -------------------------------------------------------------------
|
package/esm/main.mjs
CHANGED
|
@@ -104,7 +104,7 @@ export class RIO {
|
|
|
104
104
|
throw new Error("This line is not setup as PWM (see README)")
|
|
105
105
|
|
|
106
106
|
if (opt.period < 0.1 || opt.period > 1000000)
|
|
107
|
-
throw new
|
|
107
|
+
throw new RangeError("PWM period is out of range (100ns - 1s)")
|
|
108
108
|
|
|
109
109
|
// Normalize PWM values in ns
|
|
110
110
|
this.period = opt.period * 1000
|
|
@@ -190,7 +190,7 @@ export class RIO {
|
|
|
190
190
|
throw new Error("Cannot write to this GPIO mode:", this.mode)
|
|
191
191
|
|
|
192
192
|
if ([0, 1].indexOf(value) === -1)
|
|
193
|
-
throw new
|
|
193
|
+
throw new RangeError("Value must be either 0 or 1")
|
|
194
194
|
|
|
195
195
|
ADDON.write(this.handle, value)
|
|
196
196
|
}
|
|
@@ -306,12 +306,65 @@ export class RIO {
|
|
|
306
306
|
throw new Error("Duty of PWM line " + this.line + " cannot be updated")
|
|
307
307
|
|
|
308
308
|
if (percent < 0 || percent > 100 || typeof percent !== "number") {
|
|
309
|
-
throw new
|
|
309
|
+
throw new RangeError("Duty value (%) of PWM line" + this.line + " is not valid")
|
|
310
310
|
}
|
|
311
311
|
|
|
312
312
|
writeFileSync(this.pwmPathChannel + "duty_cycle", (this.dutyMin + ((percent / 100) * (this.dutyMax - this.dutyMin))).toString())
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
/** --------------------------------------------------------------
|
|
316
|
+
* @method pulseStart
|
|
317
|
+
* @description Start a train of N pulses on an output GPIO line
|
|
318
|
+
* @param {Number} count - Number of pulses
|
|
319
|
+
* @param {"asc"|"desc"} edge - ascending/descending edge
|
|
320
|
+
* @param {Object} opt - See pulse properties belows
|
|
321
|
+
* @return {Promise<{elapsedMs: number, pulsesCompleted: number, stopped: boolean}>}
|
|
322
|
+
*/
|
|
323
|
+
pulseStart(count, edge, opt) {
|
|
324
|
+
|
|
325
|
+
const edges = ["desc", "asc"]
|
|
326
|
+
const defopt = {
|
|
327
|
+
pulseWidth: 10, // Pulse width in µs
|
|
328
|
+
spaceWidth: 10, // Space width between pulses in µs
|
|
329
|
+
}
|
|
330
|
+
opt = {...defopt, ...opt}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
if (this.closed)
|
|
334
|
+
throw new Error("GPIO handle has been closed")
|
|
335
|
+
|
|
336
|
+
if (this.mode !== "output")
|
|
337
|
+
throw new Error("Cannot write to this GPIO mode:", this.mode)
|
|
338
|
+
|
|
339
|
+
if (!Number.isInteger(count) || count <= 0) {
|
|
340
|
+
throw new RangeError("Count must be a positive integer")
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (edges.indexOf(edge) === -1) {
|
|
344
|
+
throw new RangeError("Invalid edge value")
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!Number.isFinite(opt.pulseWidth) || opt.pulseWidth < 0) {
|
|
348
|
+
throw new RangeError("pulseWidth must be a non-negative number")
|
|
349
|
+
}
|
|
350
|
+
if (!Number.isFinite(opt.spaceWidth) || opt.spaceWidth < 0) {
|
|
351
|
+
throw new RangeError("spaceWidth must be a non-negative number")
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return ADDON.pulse(this.handle, count, Math.round(opt.spaceWidth), Math.round(opt.spaceWidth), edge === edges[0] ? 0 : 1)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** --------------------------------------------------------------
|
|
358
|
+
* @method pulseStop
|
|
359
|
+
* @description Immediately interrupts an ongoing pulse sequence if one is in progress.
|
|
360
|
+
* Synchronous and blocking (a few ms max in practice), the handle remains open and usable afterwards.
|
|
361
|
+
* Upon return, the line is guaranteed to be idle—no pulses remain in flight.
|
|
362
|
+
* @return {Boolean} true if a sequence was in progress and was stopped.
|
|
363
|
+
*/
|
|
364
|
+
pulseStop() {
|
|
365
|
+
return ADDON.pulseStop(this.handle)
|
|
366
|
+
}
|
|
367
|
+
|
|
315
368
|
// -------------------------------------------------------------------
|
|
316
369
|
// STATIC FUNCTIONS
|
|
317
370
|
/** ------------------------------------------------------------------
|
|
@@ -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
|
+
"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": {
|