rpi-io 2.2.3 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -3
- package/README.md +21 -4
- package/addon/gpio.c +435 -4
- package/documentation/api.md +67 -0
- package/documentation/examples.md +3 -0
- package/esm/main.mjs +56 -3
- package/esm/step.mjs +212 -0
- package/package.json +1 -1
- package/test/pulse.js +43 -0
- package/test/read.js +2 -0
- package/test/step-motor.js +59 -0
- package/test/write.js +1 -0
- package/esm/uln2003.mjs +0 -115
- package/test/exampleUln2003.js +0 -103
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
|
-
## [
|
|
2
|
+
## [3.0.0] - 2026-07-18
|
|
3
3
|
### Added
|
|
4
|
-
-
|
|
5
|
-
|
|
4
|
+
- methods **pulseStart**(*count, edge, options*) and **pulseStop**() for output lines
|
|
5
|
+
|
|
6
|
+
## [2.2.4] - 2026-07-06
|
|
7
|
+
### Added
|
|
8
|
+
- Static function **RIO.lineIsSupported**(*line*)
|
|
9
|
+
- Static function **RIO.lineIsAvailable**(*line*)
|
|
6
10
|
### Changed
|
|
7
11
|
- Fixes in License
|
|
8
12
|
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# rpi-io
|
|
2
|
-
    
|
|
3
3
|
|
|
4
|
-
Current version is
|
|
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.
|
|
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
|
|
|
@@ -133,7 +133,7 @@ PLEASE NOTE: In all this document, GPIO line numbers are the BCM ones as defined
|
|
|
133
133
|
|
|
134
134
|
### *OUT* operations
|
|
135
135
|
|
|
136
|
-
####
|
|
136
|
+
#### Examples of LED control
|
|
137
137
|
##### Diagram
|
|
138
138
|
```
|
|
139
139
|
GPIO Pin (Output)
|
|
@@ -150,7 +150,7 @@ GPIO Pin (Output)
|
|
|
150
150
|
─
|
|
151
151
|
```
|
|
152
152
|
|
|
153
|
-
#####
|
|
153
|
+
##### Simple write
|
|
154
154
|
|
|
155
155
|
```javascript
|
|
156
156
|
// Import rpi-io module
|
|
@@ -169,6 +169,23 @@ setTimeout(()=>{
|
|
|
169
169
|
}, 5000)
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
+
Pulse sequence
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
import {RIO} from "rpi-io"
|
|
176
|
+
const myOutput = new RIO(17, "output", {value:0})
|
|
177
|
+
|
|
178
|
+
// Slow pulse x10, ascending edge, w = 0.5 s, s = 0.5 s
|
|
179
|
+
myOutput.pulseStart(10, "asc", {
|
|
180
|
+
pulseWidth: 500000,
|
|
181
|
+
spaceWidth: 500000
|
|
182
|
+
}).then(pulseStatus => {
|
|
183
|
+
console.log("pulse status stopped when completed:", pulseStatus)
|
|
184
|
+
// { elapsedMs: 9500.113291, pulsesCompleted: 10, stopped: false }
|
|
185
|
+
myOutput.close()
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
172
189
|
|
|
173
190
|
|
|
174
191
|
### *IN* operations
|
package/addon/gpio.c
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
// -------------------------------------------------------------------
|
|
2
|
-
// RPI-IO: - gpio.c
|
|
3
|
-
// source: claude.ai/chat/
|
|
4
|
-
//
|
|
5
|
-
|
|
2
|
+
// RPI-IO: - gpio.c v42 - 2026-07-16
|
|
3
|
+
// Private source grd: https://claude.ai/chat/cf72d4ca-335a-410f-8d0d-c18d0c5a9f12
|
|
4
|
+
// ------------------------------------------------------------------
|
|
6
5
|
#include <node_api.h>
|
|
7
6
|
#include <gpiod.h>
|
|
8
7
|
#include <string.h>
|
|
@@ -45,12 +44,25 @@ typedef struct {
|
|
|
45
44
|
pthread_t monitor_thread;
|
|
46
45
|
napi_threadsafe_function tsfn;
|
|
47
46
|
napi_ref callback_ref;
|
|
47
|
+
|
|
48
|
+
// Pour pulse() : empêche close()/finalize pendant un train en cours
|
|
49
|
+
// (opération exécutée dans le thread pool libuv via napi_async_work)
|
|
50
|
+
volatile int is_pulsing;
|
|
51
|
+
volatile int stop_requested; // positionné par close()/pulseStop() pour arrêter pulse() en cours
|
|
48
52
|
} gpio_context_t;
|
|
49
53
|
|
|
54
|
+
// Déclaration anticipée (définie plus loin, avec le reste du code de pulse()) :
|
|
55
|
+
// utilisée par finalize_gpio ci-dessous.
|
|
56
|
+
static int pulse_force_stop_and_wait(gpio_context_t *ctx, long timeout_ms);
|
|
57
|
+
|
|
50
58
|
// Libérer les ressources GPIO
|
|
51
59
|
static void finalize_gpio(napi_env env, void* finalize_data, void* finalize_hint) {
|
|
52
60
|
gpio_context_t *ctx = (gpio_context_t*)finalize_data;
|
|
53
61
|
if (ctx) {
|
|
62
|
+
// Si un pulse() tourne encore (handle GC sans close() explicite),
|
|
63
|
+
// on force son arrêt avant de libérer les ressources sous ses pieds.
|
|
64
|
+
pulse_force_stop_and_wait(ctx, 500);
|
|
65
|
+
|
|
54
66
|
// Arrêter le monitoring si actif
|
|
55
67
|
if (ctx->is_monitoring) {
|
|
56
68
|
ctx->is_monitoring = 0;
|
|
@@ -745,6 +757,411 @@ static napi_value Read(napi_env env, napi_callback_info info) {
|
|
|
745
757
|
return result;
|
|
746
758
|
}
|
|
747
759
|
|
|
760
|
+
// -------------------------------------------------------------------
|
|
761
|
+
// Fonction: pulse(handle, count, widthUs, spacingUs, polarity)
|
|
762
|
+
//
|
|
763
|
+
// Génère un train de N impulsions sur une ligne déjà ouverte en sortie.
|
|
764
|
+
// - count: nombre d'impulsions
|
|
765
|
+
// - widthUs: largeur d'impulsion en microsecondes
|
|
766
|
+
// - spacingUs: espacement entre impulsions en microsecondes
|
|
767
|
+
// - polarity: 1 = impulsion active à l'état HAUT (front montant)
|
|
768
|
+
// 0 = impulsion active à l'état BAS (front descendant)
|
|
769
|
+
//
|
|
770
|
+
// Exécutée en tâche async_work dans le thread pool libuv pour ne pas
|
|
771
|
+
// bloquer la boucle d'événements JS. Résout une Promise avec la durée
|
|
772
|
+
// réelle écoulée (ms).
|
|
773
|
+
// -------------------------------------------------------------------
|
|
774
|
+
|
|
775
|
+
typedef struct {
|
|
776
|
+
napi_async_work work;
|
|
777
|
+
napi_deferred deferred;
|
|
778
|
+
gpio_context_t *ctx;
|
|
779
|
+
|
|
780
|
+
unsigned int count;
|
|
781
|
+
int64_t width_ns;
|
|
782
|
+
int64_t spacing_ns;
|
|
783
|
+
int polarity; // 1 = actif haut, 0 = actif bas
|
|
784
|
+
|
|
785
|
+
double elapsed_ms;
|
|
786
|
+
unsigned int pulses_completed;
|
|
787
|
+
int stopped_early; // 1 si interrompu par close()
|
|
788
|
+
int error_code; // 0 = ok, sinon voir pulse_error_message()
|
|
789
|
+
} pulse_work_t;
|
|
790
|
+
|
|
791
|
+
// Plage acceptée pour widthUs/spacingUs, en microsecondes.
|
|
792
|
+
// Le plafond (1h) est purement une garde-fou pratique : int64_t ne
|
|
793
|
+
// déborde de toute façon pas avant des millénaires en nanosecondes,
|
|
794
|
+
// mais une valeur aussi grande relève très probablement d'une erreur
|
|
795
|
+
// de l'appelant (unité confondue ms/µs, notamment) plutôt qu'un vrai
|
|
796
|
+
// besoin, et monopoliserait un thread du pool libuv inutilement.
|
|
797
|
+
#define PULSE_MAX_US 3600000000LL // 1 heure en µs
|
|
798
|
+
|
|
799
|
+
static const char* pulse_error_message(int code) {
|
|
800
|
+
switch (code) {
|
|
801
|
+
case 1: return "GPIO handle has been closed";
|
|
802
|
+
case 2: return "GPIO line is not configured as output";
|
|
803
|
+
case 3: return "Failed to set GPIO value during pulse train";
|
|
804
|
+
default: return "Unknown error during pulse()";
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Attente active de haute précision basée sur CLOCK_MONOTONIC.
|
|
809
|
+
// Ne pas utiliser nanosleep()/usleep() ici : leur résolution/latence
|
|
810
|
+
// réelle sur Linux (souvent >50-100µs de jitter) est insuffisante
|
|
811
|
+
// pour des largeurs d'impulsion de quelques microsecondes.
|
|
812
|
+
//
|
|
813
|
+
// Découpée en tranches d'au plus PULSE_STOP_CHECK_NS pour que
|
|
814
|
+
// stop_requested soit détecté rapidement même sur un long espacement,
|
|
815
|
+
// sans quoi close() pourrait devoir attendre toute la durée restante.
|
|
816
|
+
// Retourne 1 si l'attente a été interrompue par une demande d'arrêt.
|
|
817
|
+
#define PULSE_STOP_CHECK_NS 2000000LL // 2 ms
|
|
818
|
+
|
|
819
|
+
static int pulse_busy_wait_ns(int64_t ns, volatile int *stop_requested) {
|
|
820
|
+
if (ns <= 0) return 0;
|
|
821
|
+
|
|
822
|
+
struct timespec start, now;
|
|
823
|
+
clock_gettime(CLOCK_MONOTONIC, &start);
|
|
824
|
+
|
|
825
|
+
long long remaining_ns = ns;
|
|
826
|
+
|
|
827
|
+
while (remaining_ns > 0) {
|
|
828
|
+
long long slice_ns = remaining_ns < PULSE_STOP_CHECK_NS ? remaining_ns : PULSE_STOP_CHECK_NS;
|
|
829
|
+
|
|
830
|
+
// Recalcule la cible de fin de tranche à partir de maintenant
|
|
831
|
+
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
832
|
+
long long slice_target_nsec = (long long)now.tv_nsec + slice_ns;
|
|
833
|
+
time_t slice_target_sec = now.tv_sec + (time_t)(slice_target_nsec / 1000000000LL);
|
|
834
|
+
slice_target_nsec %= 1000000000LL;
|
|
835
|
+
|
|
836
|
+
do {
|
|
837
|
+
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
838
|
+
} while (now.tv_sec < slice_target_sec ||
|
|
839
|
+
(now.tv_sec == slice_target_sec && now.tv_nsec < slice_target_nsec));
|
|
840
|
+
|
|
841
|
+
if (*stop_requested) {
|
|
842
|
+
return 1;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Temps réellement écoulé depuis le début de l'attente totale
|
|
846
|
+
long long elapsed_ns = (long long)(now.tv_sec - start.tv_sec) * 1000000000LL +
|
|
847
|
+
((long long)now.tv_nsec - (long long)start.tv_nsec);
|
|
848
|
+
remaining_ns = ns - elapsed_ns;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return 0;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Positionne la ligne à la valeur donnée. Retourne 0 si ok, <0 sinon.
|
|
855
|
+
// Réutilise directement le handle déjà ouvert par openOutput() -
|
|
856
|
+
// pas de réouverture de chip/ligne, pour une latence minimale.
|
|
857
|
+
static int pulse_set_value(gpio_context_t *ctx, int value) {
|
|
858
|
+
#ifdef LIBGPIOD_V2
|
|
859
|
+
enum gpiod_line_value gpio_value = value ? GPIOD_LINE_VALUE_ACTIVE : GPIOD_LINE_VALUE_INACTIVE;
|
|
860
|
+
return gpiod_line_request_set_value(ctx->request, ctx->offset, gpio_value);
|
|
861
|
+
#else
|
|
862
|
+
return gpiod_line_set_value(ctx->line, value ? 1 : 0);
|
|
863
|
+
#endif
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Demande l'arrêt d'un pulse() en cours et attend (bloquant, thread JS)
|
|
867
|
+
// sa terminaison effective dans le thread du pool libuv, bornée par
|
|
868
|
+
// timeout_ms par sécurité. Ne fait rien si aucun pulse() n'est en cours.
|
|
869
|
+
// Retourne 1 si un train était effectivement en cours et a été arrêté,
|
|
870
|
+
// 0 si rien n'était en cours.
|
|
871
|
+
static int pulse_force_stop_and_wait(gpio_context_t *ctx, long timeout_ms) {
|
|
872
|
+
if (!ctx->is_pulsing) {
|
|
873
|
+
return 0;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Vu depuis PulseExecute (thread pool libuv) toutes les 2 ms max
|
|
877
|
+
// pendant les attentes, et à chaque itération sinon.
|
|
878
|
+
ctx->stop_requested = 1;
|
|
879
|
+
|
|
880
|
+
struct timespec wait_start, now;
|
|
881
|
+
clock_gettime(CLOCK_MONOTONIC, &wait_start);
|
|
882
|
+
|
|
883
|
+
while (ctx->is_pulsing) {
|
|
884
|
+
usleep(200); // 0.2 ms - relâche le CPU, pas de busy-wait pur ici
|
|
885
|
+
clock_gettime(CLOCK_MONOTONIC, &now);
|
|
886
|
+
long elapsed_ms = (now.tv_sec - wait_start.tv_sec) * 1000L +
|
|
887
|
+
(now.tv_nsec - wait_start.tv_nsec) / 1000000L;
|
|
888
|
+
if (elapsed_ms > timeout_ms) {
|
|
889
|
+
// Ne devrait jamais arriver (vérifications toutes les 2ms max
|
|
890
|
+
// côté PulseExecute). Filet de sécurité pour ne jamais bloquer
|
|
891
|
+
// indéfiniment l'appelant.
|
|
892
|
+
fprintf(stderr,
|
|
893
|
+
"rpi-io: warning - pulse() stop timed out after %ldms\n",
|
|
894
|
+
timeout_ms);
|
|
895
|
+
break;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
ctx->stop_requested = 0;
|
|
900
|
+
return 1;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Exécuté dans le thread pool libuv - jamais sur le thread JS principal
|
|
904
|
+
static void PulseExecute(napi_env env, void* data) {
|
|
905
|
+
pulse_work_t *w = (pulse_work_t*)data;
|
|
906
|
+
gpio_context_t *ctx = w->ctx;
|
|
907
|
+
|
|
908
|
+
if (ctx->is_closed) {
|
|
909
|
+
w->error_code = 1;
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (!ctx->is_output) {
|
|
913
|
+
w->error_code = 2;
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
int active_value = w->polarity ? 1 : 0;
|
|
918
|
+
int idle_value = w->polarity ? 0 : 1;
|
|
919
|
+
|
|
920
|
+
struct timespec t0, t1;
|
|
921
|
+
clock_gettime(CLOCK_MONOTONIC, &t0);
|
|
922
|
+
|
|
923
|
+
unsigned int i;
|
|
924
|
+
for (i = 0; i < w->count; i++) {
|
|
925
|
+
if (ctx->stop_requested) {
|
|
926
|
+
w->stopped_early = 1;
|
|
927
|
+
break;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (pulse_set_value(ctx, active_value) < 0) {
|
|
931
|
+
w->error_code = 3;
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
if (pulse_busy_wait_ns(w->width_ns, &ctx->stop_requested)) {
|
|
936
|
+
w->stopped_early = 1;
|
|
937
|
+
// On coupe immédiatement l'impulsion avant de sortir
|
|
938
|
+
pulse_set_value(ctx, idle_value);
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (pulse_set_value(ctx, idle_value) < 0) {
|
|
943
|
+
w->error_code = 3;
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (i < w->count - 1) {
|
|
948
|
+
if (pulse_busy_wait_ns(w->spacing_ns, &ctx->stop_requested)) {
|
|
949
|
+
w->stopped_early = 1;
|
|
950
|
+
break;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Sécurité : quelle que soit la raison de sortie, s'assurer que la
|
|
956
|
+
// ligne est bien au repos avant de rendre la main.
|
|
957
|
+
if (w->error_code == 0) {
|
|
958
|
+
pulse_set_value(ctx, idle_value);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
w->pulses_completed = i < w->count ? i : w->count;
|
|
962
|
+
|
|
963
|
+
clock_gettime(CLOCK_MONOTONIC, &t1);
|
|
964
|
+
w->elapsed_ms = (double)(t1.tv_sec - t0.tv_sec) * 1000.0 +
|
|
965
|
+
(double)(t1.tv_nsec - t0.tv_nsec) / 1e6;
|
|
966
|
+
|
|
967
|
+
// IMPORTANT: is_pulsing doit être remis à 0 ICI, dans le thread du
|
|
968
|
+
// pool libuv, et non dans PulseComplete (qui s'exécute sur le thread
|
|
969
|
+
// JS principal). close()/pulseStop() bloquent volontairement le
|
|
970
|
+
// thread JS en attendant is_pulsing == 0 : si ce flag n'était remis
|
|
971
|
+
// à 0 que par PulseComplete, on aurait un interblocage (le thread JS
|
|
972
|
+
// bloqué empêcherait sa propre boucle d'événements de déclencher
|
|
973
|
+
// PulseComplete).
|
|
974
|
+
ctx->is_pulsing = 0;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Exécuté de retour sur le thread JS - résout ou rejette la Promise
|
|
978
|
+
static void PulseComplete(napi_env env, napi_status status, void* data) {
|
|
979
|
+
pulse_work_t *w = (pulse_work_t*)data;
|
|
980
|
+
|
|
981
|
+
// Note: ctx->is_pulsing a déjà été remis à 0 par PulseExecute
|
|
982
|
+
// (voir commentaire à la fin de PulseExecute pour la raison).
|
|
983
|
+
|
|
984
|
+
if (status != napi_ok || w->error_code != 0) {
|
|
985
|
+
napi_value err;
|
|
986
|
+
const char *msg = (status != napi_ok)
|
|
987
|
+
? "Async work failed"
|
|
988
|
+
: pulse_error_message(w->error_code);
|
|
989
|
+
napi_create_string_utf8(env, msg, NAPI_AUTO_LENGTH, &err);
|
|
990
|
+
napi_reject_deferred(env, w->deferred, err);
|
|
991
|
+
} else {
|
|
992
|
+
napi_value result, elapsed, completed, stopped;
|
|
993
|
+
napi_create_object(env, &result);
|
|
994
|
+
napi_create_double(env, w->elapsed_ms, &elapsed);
|
|
995
|
+
napi_create_uint32(env, w->pulses_completed, &completed);
|
|
996
|
+
napi_get_boolean(env, w->stopped_early, &stopped);
|
|
997
|
+
|
|
998
|
+
napi_set_named_property(env, result, "elapsedMs", elapsed);
|
|
999
|
+
napi_set_named_property(env, result, "pulsesCompleted", completed);
|
|
1000
|
+
napi_set_named_property(env, result, "stopped", stopped);
|
|
1001
|
+
|
|
1002
|
+
napi_resolve_deferred(env, w->deferred, result);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
napi_delete_async_work(env, w->work);
|
|
1006
|
+
free(w);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
static napi_value Pulse(napi_env env, napi_callback_info info) {
|
|
1010
|
+
napi_status status;
|
|
1011
|
+
size_t argc = 5;
|
|
1012
|
+
napi_value args[5];
|
|
1013
|
+
gpio_context_t *ctx = NULL;
|
|
1014
|
+
int32_t count, polarity;
|
|
1015
|
+
int64_t width_us, spacing_us;
|
|
1016
|
+
|
|
1017
|
+
status = napi_get_cb_info(env, info, &argc, args, NULL, NULL);
|
|
1018
|
+
if (status != napi_ok || argc < 5) {
|
|
1019
|
+
napi_throw_error(env, NULL,
|
|
1020
|
+
"Expected (handle, count, widthUs, spacingUs, polarity) arguments");
|
|
1021
|
+
return NULL;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
status = napi_get_value_external(env, args[0], (void**)&ctx);
|
|
1025
|
+
if (status != napi_ok || !ctx) {
|
|
1026
|
+
napi_throw_error(env, NULL, "Invalid GPIO handle");
|
|
1027
|
+
return NULL;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
if (ctx->is_closed) {
|
|
1031
|
+
napi_throw_error(env, NULL, "GPIO handle has been closed");
|
|
1032
|
+
return NULL;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if (!ctx->is_output) {
|
|
1036
|
+
napi_throw_error(env, NULL, "GPIO line is not configured as output");
|
|
1037
|
+
return NULL;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
if (ctx->is_pulsing) {
|
|
1041
|
+
napi_throw_error(env, NULL, "A pulse() train is already in progress on this handle");
|
|
1042
|
+
return NULL;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (napi_get_value_int32(env, args[1], &count) != napi_ok || count <= 0) {
|
|
1046
|
+
napi_throw_error(env, NULL, "Invalid count: must be a positive integer");
|
|
1047
|
+
return NULL;
|
|
1048
|
+
}
|
|
1049
|
+
if (napi_get_value_int64(env, args[2], &width_us) != napi_ok ||
|
|
1050
|
+
width_us < 0 || width_us > PULSE_MAX_US) {
|
|
1051
|
+
napi_throw_error(env, NULL,
|
|
1052
|
+
"Invalid widthUs: must be an integer in [0, 3600000000] (0 to 1h in microseconds)");
|
|
1053
|
+
return NULL;
|
|
1054
|
+
}
|
|
1055
|
+
if (napi_get_value_int64(env, args[3], &spacing_us) != napi_ok ||
|
|
1056
|
+
spacing_us < 0 || spacing_us > PULSE_MAX_US) {
|
|
1057
|
+
napi_throw_error(env, NULL,
|
|
1058
|
+
"Invalid spacingUs: must be an integer in [0, 3600000000] (0 to 1h in microseconds)");
|
|
1059
|
+
return NULL;
|
|
1060
|
+
}
|
|
1061
|
+
if (napi_get_value_int32(env, args[4], &polarity) != napi_ok) {
|
|
1062
|
+
napi_throw_error(env, NULL, "Invalid polarity: must be 0 or 1");
|
|
1063
|
+
return NULL;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
pulse_work_t *w = (pulse_work_t*)malloc(sizeof(pulse_work_t));
|
|
1067
|
+
if (!w) {
|
|
1068
|
+
napi_throw_error(env, NULL, "Memory allocation failed");
|
|
1069
|
+
return NULL;
|
|
1070
|
+
}
|
|
1071
|
+
memset(w, 0, sizeof(pulse_work_t));
|
|
1072
|
+
|
|
1073
|
+
w->ctx = ctx;
|
|
1074
|
+
w->count = (unsigned int)count;
|
|
1075
|
+
w->width_ns = width_us * 1000LL;
|
|
1076
|
+
w->spacing_ns = spacing_us * 1000LL;
|
|
1077
|
+
w->polarity = polarity ? 1 : 0;
|
|
1078
|
+
|
|
1079
|
+
napi_value promise;
|
|
1080
|
+
status = napi_create_promise(env, &w->deferred, &promise);
|
|
1081
|
+
if (status != napi_ok) {
|
|
1082
|
+
free(w);
|
|
1083
|
+
napi_throw_error(env, NULL, "Failed to create promise");
|
|
1084
|
+
return NULL;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
napi_value resource_name;
|
|
1088
|
+
napi_create_string_utf8(env, "GPIOPulse", NAPI_AUTO_LENGTH, &resource_name);
|
|
1089
|
+
|
|
1090
|
+
status = napi_create_async_work(env, NULL, resource_name,
|
|
1091
|
+
PulseExecute, PulseComplete, w, &w->work);
|
|
1092
|
+
if (status != napi_ok) {
|
|
1093
|
+
free(w);
|
|
1094
|
+
napi_throw_error(env, NULL, "Failed to create async work");
|
|
1095
|
+
return NULL;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
ctx->is_pulsing = 1;
|
|
1099
|
+
ctx->stop_requested = 0;
|
|
1100
|
+
|
|
1101
|
+
status = napi_queue_async_work(env, w->work);
|
|
1102
|
+
if (status != napi_ok) {
|
|
1103
|
+
ctx->is_pulsing = 0;
|
|
1104
|
+
napi_delete_async_work(env, w->work);
|
|
1105
|
+
free(w);
|
|
1106
|
+
napi_throw_error(env, NULL, "Failed to queue async work");
|
|
1107
|
+
return NULL;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
return promise;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// -------------------------------------------------------------------
|
|
1114
|
+
// Fonction: pulseStop(handle)
|
|
1115
|
+
//
|
|
1116
|
+
// Interrompt immédiatement un train d'impulsions en cours sur ce
|
|
1117
|
+
// handle, s'il y en a un. Contrairement à close(), le handle reste
|
|
1118
|
+
// ouvert et utilisable ensuite (write, read, ou un nouveau pulse()).
|
|
1119
|
+
//
|
|
1120
|
+
// Cas d'usage typique : butée de fin de course sur un moteur pas à
|
|
1121
|
+
// pas - un GPIO d'entrée en interruption (startMonitoring) appelle
|
|
1122
|
+
// pulseStop() sur le GPIO de pas pour arrêter net le mouvement.
|
|
1123
|
+
//
|
|
1124
|
+
// Fonction SYNCHRONE et BLOQUANTE (bornée à ~500ms de sécurité, mais
|
|
1125
|
+
// l'arrêt réel prend au maximum quelques millisecondes) : au retour,
|
|
1126
|
+
// la ligne est garantie au repos et plus aucune impulsion ne sera
|
|
1127
|
+
// émise. C'est volontaire : pour une butée de fin de course, le code
|
|
1128
|
+
// appelant a besoin de cette garantie avant de décider de la suite
|
|
1129
|
+
// (ex: inverser le sens de rotation) sans risque de course avec une
|
|
1130
|
+
// dernière impulsion encore en vol.
|
|
1131
|
+
//
|
|
1132
|
+
// Retourne un booléen : true si un train était en cours et a été
|
|
1133
|
+
// arrêté, false si rien n'était en cours (no-op).
|
|
1134
|
+
// -------------------------------------------------------------------
|
|
1135
|
+
static napi_value PulseStop(napi_env env, napi_callback_info info) {
|
|
1136
|
+
napi_status status;
|
|
1137
|
+
size_t argc = 1;
|
|
1138
|
+
napi_value args[1];
|
|
1139
|
+
gpio_context_t *ctx = NULL;
|
|
1140
|
+
|
|
1141
|
+
status = napi_get_cb_info(env, info, &argc, args, NULL, NULL);
|
|
1142
|
+
if (status != napi_ok || argc < 1) {
|
|
1143
|
+
napi_throw_error(env, NULL, "Expected handle argument");
|
|
1144
|
+
return NULL;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
status = napi_get_value_external(env, args[0], (void**)&ctx);
|
|
1148
|
+
if (status != napi_ok || !ctx) {
|
|
1149
|
+
napi_throw_error(env, NULL, "Invalid GPIO handle");
|
|
1150
|
+
return NULL;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
if (ctx->is_closed) {
|
|
1154
|
+
napi_throw_error(env, NULL, "GPIO handle has been closed");
|
|
1155
|
+
return NULL;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
int was_stopped = pulse_force_stop_and_wait(ctx, 500);
|
|
1159
|
+
|
|
1160
|
+
napi_value result;
|
|
1161
|
+
napi_get_boolean(env, was_stopped, &result);
|
|
1162
|
+
return result;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
748
1165
|
// Fonction: close(handle)
|
|
749
1166
|
static napi_value Close(napi_env env, napi_callback_info info) {
|
|
750
1167
|
napi_status status;
|
|
@@ -771,6 +1188,10 @@ static napi_value Close(napi_env env, napi_callback_info info) {
|
|
|
771
1188
|
return result;
|
|
772
1189
|
}
|
|
773
1190
|
|
|
1191
|
+
// Force l'arrêt d'un pulse() en cours et attend sa terminaison réelle
|
|
1192
|
+
// avant de libérer les ressources GPIO sous ses pieds.
|
|
1193
|
+
pulse_force_stop_and_wait(ctx, 500);
|
|
1194
|
+
|
|
774
1195
|
// Arrêter le monitoring si actif
|
|
775
1196
|
if (ctx->is_monitoring) {
|
|
776
1197
|
ctx->is_monitoring = 0;
|
|
@@ -854,6 +1275,16 @@ static napi_value Init(napi_env env, napi_value exports) {
|
|
|
854
1275
|
napi_set_named_property(env, exports, "read", fn);
|
|
855
1276
|
}
|
|
856
1277
|
|
|
1278
|
+
status = napi_create_function(env, NULL, 0, Pulse, NULL, &fn);
|
|
1279
|
+
if (status == napi_ok) {
|
|
1280
|
+
napi_set_named_property(env, exports, "pulse", fn);
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
status = napi_create_function(env, NULL, 0, PulseStop, NULL, &fn);
|
|
1284
|
+
if (status == napi_ok) {
|
|
1285
|
+
napi_set_named_property(env, exports, "pulseStop", fn);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
857
1288
|
status = napi_create_function(env, NULL, 0, StartMonitoring, NULL, &fn);
|
|
858
1289
|
if (status == napi_ok) {
|
|
859
1290
|
napi_set_named_property(env, exports, "startMonitoring", fn);
|
package/documentation/api.md
CHANGED
|
@@ -84,6 +84,73 @@ myOuput.write(1)
|
|
|
84
84
|
|
|
85
85
|
|
|
86
86
|
|
|
87
|
+
### pulseStart(count, edge, opt)
|
|
88
|
+
|
|
89
|
+
To start a pulse sequence to "output" instance.
|
|
90
|
+
|
|
91
|
+
#### Example
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
import {RIO} from "rpi-io"
|
|
95
|
+
const myOutput = new RIO(17, "output", {value:0})
|
|
96
|
+
|
|
97
|
+
// Slow pulse x10, ascending edge, w = 0.5 s, s = 0.5 s
|
|
98
|
+
myOutput.pulseStart(10, "asc", {
|
|
99
|
+
pulseWidth: 500000,
|
|
100
|
+
spaceWidth: 500000
|
|
101
|
+
}).then(pulseStatus => {
|
|
102
|
+
console.log("pulse status stopped when completed:", pulseStatus)
|
|
103
|
+
// { elapsedMs: 9500.113291, pulsesCompleted: 10, stopped: false }
|
|
104
|
+
myOutput.close()
|
|
105
|
+
})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### Parameter(s)
|
|
109
|
+
|
|
110
|
+
- **count** *{Number}*
|
|
111
|
+
- **edge** *{"asc'"|'"desc"}* Ascending or descendig edge
|
|
112
|
+
- **options** *{Object}* Pulse options. See default values below
|
|
113
|
+
|
|
114
|
+
```javascript
|
|
115
|
+
{
|
|
116
|
+
pulseWidth: 10, // pulse width in µs
|
|
117
|
+
spaceWidth: 10 // space beween pulses in µs
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### Return
|
|
122
|
+
|
|
123
|
+
*{Promise<{`elapsedMs`: number, `pulsesCompleted`: number, `stopped`: boolean}>}*
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
### pulseStop()
|
|
128
|
+
|
|
129
|
+
To stop a pulse sequence on an "output" instance. This function does not return an error is no pulse sequence is active.
|
|
130
|
+
|
|
131
|
+
#### Example
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
import {RIO, sleep} from "rpi-io"
|
|
135
|
+
const myOutput = new RIO(17, "output", {value:1})
|
|
136
|
+
|
|
137
|
+
// Fast pulse x1000, descending edge, w = 5µs, s = 5µs
|
|
138
|
+
myOutput.pulseStart(1000, "desc", {
|
|
139
|
+
pulseWidth: 5,
|
|
140
|
+
spaceWidth: 5
|
|
141
|
+
}).then(pulseStatus => {
|
|
142
|
+
console.log("pulse status stopped when completed or stopped:", pulseStatus)
|
|
143
|
+
// { elapsedMs: 5.283217, pulsesCompleted: 456, stopped: true }
|
|
144
|
+
myOutput.write(0) // Reset line
|
|
145
|
+
myOutput.close()
|
|
146
|
+
})
|
|
147
|
+
await sleep(5)
|
|
148
|
+
myOutput.pulseStop() // Stop pulse train after 5ms
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
####
|
|
153
|
+
|
|
87
154
|
### read()
|
|
88
155
|
|
|
89
156
|
To read value from "input" instance.
|
|
@@ -5,6 +5,9 @@ To get familiar with the **rpi-io**, you might have a glance on examples and pla
|
|
|
5
5
|
# Simple write
|
|
6
6
|
node /your-project/node_modules/rpi-io/test/write.js
|
|
7
7
|
|
|
8
|
+
# Pulse sequences
|
|
9
|
+
node /your-project/node_modules/rpi-io/test/pulse.js
|
|
10
|
+
|
|
8
11
|
# Read and monitor
|
|
9
12
|
node /your-project/node_modules/rpi-io/test/read.js
|
|
10
13
|
|
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
|
/** ------------------------------------------------------------------
|
package/esm/step.mjs
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
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
|
+
// -------------------------------------------------------------------
|
package/package.json
CHANGED
package/test/pulse.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// -------------------------------------------------------------------
|
|
2
|
+
// TEST - Simple slow and fast pulse trains
|
|
3
|
+
// -------------------------------------------------------------------
|
|
4
|
+
import {RIO, traceCfg, log, sleep, ctrlC, lineNumber} from "../esm/main.mjs"
|
|
5
|
+
|
|
6
|
+
(async () => {
|
|
7
|
+
traceCfg(2)
|
|
8
|
+
|
|
9
|
+
// Read line number from command line
|
|
10
|
+
const line = lineNumber(2)
|
|
11
|
+
if (line < 0) return
|
|
12
|
+
|
|
13
|
+
// Init output line and ctrl+c to close it
|
|
14
|
+
const led = new RIO(line, "output", {value: 0})
|
|
15
|
+
log("led:", led)
|
|
16
|
+
ctrlC(() => {
|
|
17
|
+
led.close()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// Slow pulse x10, ascending edge, w = 0.5 s, s = 0.5 s
|
|
21
|
+
const pulseStatus = await led.pulseStart(10, "asc", {
|
|
22
|
+
pulseWidth: 500000,
|
|
23
|
+
spaceWidth: 500000
|
|
24
|
+
})
|
|
25
|
+
log("pulse status stopped when completed:", pulseStatus)
|
|
26
|
+
|
|
27
|
+
// Fast pulse x1000, descending edge, w = 5µs, s = 5µs
|
|
28
|
+
led.write(1) // Init line according to edge
|
|
29
|
+
led.pulseStart(1000, "desc", {
|
|
30
|
+
pulseWidth: 5,
|
|
31
|
+
spaceWidth: 5
|
|
32
|
+
}).then(pulseStatus => {
|
|
33
|
+
log("pulse status stopped when stopped:", pulseStatus)
|
|
34
|
+
led.write(0) // Reset line
|
|
35
|
+
led.close()
|
|
36
|
+
})
|
|
37
|
+
await sleep(5)
|
|
38
|
+
led.pulseStop() // Stop pulse train after 5ms
|
|
39
|
+
|
|
40
|
+
})()
|
|
41
|
+
// -------------------------------------------------------------------
|
|
42
|
+
// EoF
|
|
43
|
+
// -------------------------------------------------------------------
|
package/test/read.js
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
// -------------------------------------------------------------------
|
package/test/write.js
CHANGED
package/esm/uln2003.mjs
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
// -------------------------------------------------------------------
|
|
2
|
-
// RPI-IO: ULN2003 STEP MOTORS CONTROL
|
|
3
|
-
// -------------------------------------------------------------------
|
|
4
|
-
|
|
5
|
-
import {RIO} from "./main.mjs";
|
|
6
|
-
|
|
7
|
-
export class ULN2003 {
|
|
8
|
-
|
|
9
|
-
static RPi_GPIO_STD = [5, 6, 16, 17, 20, 21, 22, 23, 24, 25, 26, 27]
|
|
10
|
-
static RPi_GPIO_PWM = [12, 13, 18, 19]
|
|
11
|
-
|
|
12
|
-
/** ------------------------------------------------------------------
|
|
13
|
-
* @function isValidGpio
|
|
14
|
-
* @description Check if pin a valid multipurpose GPIO
|
|
15
|
-
* @param {Number} pin
|
|
16
|
-
* @return {Boolean}
|
|
17
|
-
*/
|
|
18
|
-
static static isValidGpio(pin) {
|
|
19
|
-
return [...ULN2003.RPi_GPIO_STD, ...ULN2003.RPi_GPIO_PWM].GPIOS.includes(pin);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
static MODE = ["half", "full"]
|
|
23
|
-
static STEPS = {
|
|
24
|
-
half: [
|
|
25
|
-
[1, 0, 0, 0],
|
|
26
|
-
[1, 1, 0, 0],
|
|
27
|
-
[0, 1, 0, 0],
|
|
28
|
-
[0, 1, 1, 0],
|
|
29
|
-
[0, 0, 1, 0],
|
|
30
|
-
[0, 0, 1, 1],
|
|
31
|
-
[0, 0, 0, 1],
|
|
32
|
-
[1, 0, 0, 1]
|
|
33
|
-
],
|
|
34
|
-
full: [
|
|
35
|
-
[1, 0, 0, 1],
|
|
36
|
-
[1, 1, 0, 0],
|
|
37
|
-
[0, 1, 1, 0],
|
|
38
|
-
[0, 0, 1, 1]
|
|
39
|
-
]
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
/** ------------------------------------------------------------------
|
|
44
|
-
* @method constructor
|
|
45
|
-
* @param {Array} pins - Array of 4 GPIO BCM port number e.g. [21,22,24,25]
|
|
46
|
-
* @param {Object} opt - Delay between steps and sequencing mode. See below
|
|
47
|
-
*/
|
|
48
|
-
constructor(pins, opt) {
|
|
49
|
-
|
|
50
|
-
// Init options with default values and check validity
|
|
51
|
-
const defopt = {
|
|
52
|
-
// Delay between two steps in 1 ≤ ms ≤ 1000
|
|
53
|
-
delayMs: 10,
|
|
54
|
-
// Sequencing mode: 'full' or 'half' (default)
|
|
55
|
-
seqMode: ULN2003.MODE[0]
|
|
56
|
-
}
|
|
57
|
-
opt = {...defopt, ...opt}
|
|
58
|
-
opt.seqMode = opt.seqMode.toLowerCase()
|
|
59
|
-
|
|
60
|
-
// Check if pins
|
|
61
|
-
pins.forEach(pin => {
|
|
62
|
-
if (!ULN2003.isValidGpio(pin)) {
|
|
63
|
-
throw new Error("Invalid pin number: " + pin)
|
|
64
|
-
}
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
rpiIoInstances.forEach(instance => {
|
|
69
|
-
if (!instance instanceof RIO) throw new Error("Invalid instance")
|
|
70
|
-
if (instance.mode !== "output") throw new Error("Invalid instance mode")
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
// Reset option values if not valid
|
|
74
|
-
if (ULN2003.MODE.indexOf(opt.seqMode) === -1) opt.seqMode = defopt.seqMode
|
|
75
|
-
if (typeof opt.delayMs !== "number" || opt.delayMs < 1 || opt.delayMs > 1000) opt.delayMs = defopt.delayMs
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// Throw error if list of rpi-io instances is not valid: wrong number, wrong instance, wrong mode
|
|
79
|
-
if (!Array.isArray(rpiIoInstances) || rpiIoInstances.length !== 4) throw new Error("Invalid instance list.")
|
|
80
|
-
rpiIoInstances.forEach(instance => {
|
|
81
|
-
if (!instance instanceof RIO) throw new Error("Invalid instance")
|
|
82
|
-
if (instance.mode !== "output") throw new Error("Invalid instance mode")
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
// No option error here
|
|
86
|
-
|
|
87
|
-
this.coils = rpiIoInstances
|
|
88
|
-
this.sequence = ULN2003.STEPS[opt.seqMode]
|
|
89
|
-
this.len = this.sequence.length
|
|
90
|
-
this.delay = opt.delayMs
|
|
91
|
-
this.A = rpiIoInstances[0]
|
|
92
|
-
this.B = rpiIoInstances[1]
|
|
93
|
-
this.C = rpiIoInstances[2]
|
|
94
|
-
this.D = rpiIoInstances[0]
|
|
95
|
-
this.inc = 0
|
|
96
|
-
this.index = 0
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const toto = new ULN2003()
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
/** ------------------------------------------------------------------
|
|
104
|
-
* @method uln2003
|
|
105
|
-
* @description To control step motors based on ULN20003-like drivers.
|
|
106
|
-
* This function used 4 rpi-io output instances to control
|
|
107
|
-
* step motors back and forth.
|
|
108
|
-
* @param {Object} pins
|
|
109
|
-
* @param {Number} steps - Nombre de pas (entier > 0)
|
|
110
|
-
* @param {'fwd'|'rev'} dir - Direction : 'fwd' = avant, 'rev' = arrière
|
|
111
|
-
*/
|
|
112
|
-
|
|
113
|
-
// -------------------------------------------------------------------
|
|
114
|
-
// EoF
|
|
115
|
-
// -------------------------------------------------------------------
|
package/test/exampleUln2003.js
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
// -------------------------------------------------------------------
|
|
2
|
-
// TEST - ULN 2003
|
|
3
|
-
// -------------------------------------------------------------------
|
|
4
|
-
import {RIO, traceCfg, log, sleep, ctrlC, lineNumber} from "../esm/main.mjs"
|
|
5
|
-
|
|
6
|
-
(async () => {
|
|
7
|
-
traceCfg(2)
|
|
8
|
-
|
|
9
|
-
// Init outputs
|
|
10
|
-
const pins = {
|
|
11
|
-
A: new RIO(21, 'output', {value: 0}),
|
|
12
|
-
B: new RIO(22, 'output', {value: 0}),
|
|
13
|
-
C: new RIO(24, 'output', {value: 0}),
|
|
14
|
-
D: new RIO(25, 'output', {value: 0}),
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const HALF_STEP = [
|
|
18
|
-
[1, 0, 0, 0],
|
|
19
|
-
[1, 1, 0, 0],
|
|
20
|
-
[0, 1, 0, 0],
|
|
21
|
-
[0, 1, 1, 0],
|
|
22
|
-
[0, 0, 1, 0],
|
|
23
|
-
[0, 0, 1, 1],
|
|
24
|
-
[0, 0, 0, 1],
|
|
25
|
-
[1, 0, 0, 1],
|
|
26
|
-
]
|
|
27
|
-
|
|
28
|
-
const FULL_STEP = [
|
|
29
|
-
[1, 0, 0, 1],
|
|
30
|
-
[1, 1, 0, 0],
|
|
31
|
-
[0, 1, 1, 0],
|
|
32
|
-
[0, 0, 1, 1],
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
/** ------------------------------------------------------------------
|
|
36
|
-
* @function write
|
|
37
|
-
* @param {Number} port
|
|
38
|
-
* @param {Number} value
|
|
39
|
-
*/
|
|
40
|
-
const write = (port, value) => {
|
|
41
|
-
pins[port].write(value)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const reset = () => {
|
|
45
|
-
for (const [port, instance] of Object.entries(pins)) {
|
|
46
|
-
instance.write(0)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const test = async () => {
|
|
51
|
-
for (const [port, instance] of Object.entries(pins)) {
|
|
52
|
-
instance.write(1)
|
|
53
|
-
await sleep(500)
|
|
54
|
-
instance.write(0)
|
|
55
|
-
await sleep(500)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Close ports on ctrl+c
|
|
60
|
-
ctrlC(() => {
|
|
61
|
-
reset()
|
|
62
|
-
RIO.closeAll();
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
/** --------------------------------------------------------------
|
|
66
|
-
* @function stepMotor
|
|
67
|
-
* @param {Object} opts
|
|
68
|
-
* @param {Number} opts.steps - Nombre de pas (entier > 0)
|
|
69
|
-
* @param {'fwd'|'rev'} opts.dir - Direction : 'fwd' = avant, 'rev' = arrière
|
|
70
|
-
* @param {number} opts.delayMs - Délai entre deux pas en millisecondes (≥ 1)
|
|
71
|
-
* @param {'half'|'full'} [opts.mode='half'] - Mode de séquençage
|
|
72
|
-
* @returns {Promise<void>}
|
|
73
|
-
*/
|
|
74
|
-
async function stepMotor({steps, dir = 'fwd', delayMs = 10, mode = 'half'}) {
|
|
75
|
-
const sequence = mode === 'half' ? HALF_STEP : FULL_STEP;
|
|
76
|
-
const len = sequence.length;
|
|
77
|
-
const inc = dir === 'fwd' ? 1 : -1;
|
|
78
|
-
let index = 0;
|
|
79
|
-
|
|
80
|
-
for (let i = 0; i < steps; i++) {
|
|
81
|
-
const [a, b, c, d] = sequence[index];
|
|
82
|
-
write('A', a);
|
|
83
|
-
write('B', b);
|
|
84
|
-
write('C', c);
|
|
85
|
-
write('D', d);
|
|
86
|
-
index = ((index + inc) % len + len) % len;
|
|
87
|
-
await sleep(delayMs); // sleep() natif rpi-io
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Désactivation des bobines
|
|
91
|
-
write('A', 0);
|
|
92
|
-
write('B', 0);
|
|
93
|
-
write('C', 0);
|
|
94
|
-
write('D', 0);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
await stepMotor({ steps: 1600, dir: 'rev', delayMs: 8 })
|
|
99
|
-
|
|
100
|
-
})()
|
|
101
|
-
// -------------------------------------------------------------------
|
|
102
|
-
// EoF
|
|
103
|
-
// -------------------------------------------------------------------
|