Christian Simon Avril 2022 ====== Reverse-ingeneering de la graveuse laser Jinsoku LE1620 ====== ==== Présentation de la machine ==== C'est graveuse laser à bas-coût que l'on peut trouver sur de nombreux site, dont [[https://www.amazon.fr/Jinsoku-Engraver-Engraving-Cutting-Machine/dp/B0969ZJJYB|Amazon]], pour 245€ environ, produite par Genmitsu et [[https://www.sainsmart.com/collections/laser-cutting/products/genmitsu-jinsoku-le-1620-portable-single-arm-laser-engraver|SainSmart]]. {{ :wiki:​projets:jinsoku-le1620.jpg?400 |}} Elle est basée sur un contrôleur dont le firmware est basé sur l'open-source [[https://github.com/grbl/grbl|grbl]]. On peut donc la contrôler avec la plupart des logiciels usuels : [[https://lasergrbl.com|LaserGRBL]] (Windows seulement), [[https://lightburnsoftware.com|Lightburn]] (non-libre). On pense aussi à [[https://winder.github.io/ugs_website/|UGS]] (Universal Gcode Sender, libre et multiplateforme). Références : [[https://benmaker.fr/2021/11/18/genmitsu-jinsoku-le-1620-mini-graveur-laser/ | Le blog Ben Maker]] On trouve également quelques "unboxing" sur Youtube, mais ils sont sans intérêt pour la plupart. ==== Motivation ==== C'est un point de départ intéressant pour construire des machines 2D+ rapidement : l'électronique et le bâti sont déjà montés, ça permet de faire vite plein de choses. C'est initialement dans le cadre du projet [[https://wiki.fablab.sorbonne-universite.fr/wiki/doku.php?id=wiki:projets:pillink|Pillink]] que j'y ai pensé. Mais on pourrait également transformer cette découpeuse laser en découpeuse à plasma (ou autre !) avec un effort minimal. L'étape indispensable est de pouvoir modifier la tête pour contrôler //autre chose// que le faisceau laser de gravure, sans changer le reste. ==== Démarche ==== La puissance du laser est fixée dans le Gcode, qui est transmis au contrôleur, qui envoie un PWM à une carte fille montée sous le bras, qui elle-même contrôle et alimente le laser. On va donc chercher à exploiter ce PWM pour déclencher d'autres actions : démarrer l'aspiration (ouvrir une électrovanne), actionner un servo-moteur, ouvrir un relais. Les valeurs du PWM étant autant de codes d'actions possible... à la précision et au bruit près ! ==== Décodage d'un PWM ==== La première étape est d'arriver à décoder un PWM avec un Arduino. La meilleure lecture est le blog de [[https://www.benripley.com/diy/arduino/three-ways-to-read-a-pwm-signal-with-arduino/ |Ben Ripley]], qui présente 3 méthodes : * Simple, basique avec la fonction pulseIn(). * Avec des interruptions externes à coups de attachInterrupt(). * Avec des bibliothèques qui implémentent des fonctions autour de ces interruptions. Il utilise [[https://playground.arduino.cc/Main/PinChangeInt/|PinChangeInt]] mais ont trouve très vite ses évolutions dont [[https://github.com/NicoHood/PinChangeInterrupt|PinChangeInterrupt]], incluse dans la base de bibliothèques de l'IDE Arduino. On trouve facilement des exemples de gens l'utilisant, par exemple [[https://quadmeup.com/read-rc-pwm-signal-with-arduino/| le blog QuadMeUp de @pspychalski]]. L'exemple permet de décoder des signaux RC transmis par une télécommande avec un PWM à 50 Hz... La première inconnue est donc la capacité à travailler à plus haute fréquence. Pour prendre en main cela, on programme un premier Arduino pour générer des PWM de 0 (''dutycycle'' 0%) à 255 (''dutycycle'' 100%), selon une entrée. Le PWM généré par l'Arduino UNO est à 490 Hz, déjà de fréquence plus élevée. Ce PWM est envoyé vers l'Arduino qui décode, et allume des LED selon la valeur décodée. Lorsque cet ensemble fonctionne, on a le code suivant. {{ :wiki:divers:rc_pwm_read.ino.zip |}} #include /* * Define pins used to provide RC PWM signal to Arduino * Pins 8, 9 and 10 are used since they work on both ATMega328 and * ATMega32u4 board. So this code will work on Uno/Mini/Nano/Micro/Leonardo * See PinChangeInterrupt documentation for usable pins on other boards */ const byte channel_pin[] = {8, 9, 10}; volatile unsigned long rising_start[] = {0, 0, 0}; volatile long channel_length[] = {0, 0, 0}; #define led_r 7 #define led_j 6 #define led_v 5 #define led_b 4 int etat; void setup() { Serial.begin(57600); pinMode(channel_pin[0], INPUT); pinMode(channel_pin[1], INPUT); pinMode(channel_pin[2], INPUT); pinMode(led_r, OUTPUT); pinMode(led_j, OUTPUT); pinMode(led_v, OUTPUT); pinMode(led_b, OUTPUT); attachPinChangeInterrupt(digitalPinToPinChangeInterrupt(channel_pin[0]), onRising0, CHANGE); attachPinChangeInterrupt(digitalPinToPinChangeInterrupt(channel_pin[1]), onRising1, CHANGE); attachPinChangeInterrupt(digitalPinToPinChangeInterrupt(channel_pin[2]), onRising2, CHANGE); digitalWrite(led_r, HIGH); digitalWrite(led_j, HIGH); digitalWrite(led_v, HIGH); digitalWrite(led_b, HIGH); delay(1000); digitalWrite(led_r, LOW); digitalWrite(led_j, LOW); digitalWrite(led_v, LOW); digitalWrite(led_b, LOW); delay(1000); digitalWrite(led_r, HIGH); digitalWrite(led_j, HIGH); digitalWrite(led_v, HIGH); digitalWrite(led_b, HIGH); delay(1000); digitalWrite(led_r, LOW); digitalWrite(led_j, LOW); digitalWrite(led_v, LOW); digitalWrite(led_b, LOW); } void processPin(byte pin) { uint8_t trigger = getPinChangeInterruptTrigger(digitalPinToPCINT(channel_pin[pin])); if(trigger == RISING) { rising_start[pin] = micros(); } else if(trigger == FALLING) { channel_length[pin] = micros() - rising_start[pin]; } } void onRising0(void) { processPin(0); } void onRising1(void) { processPin(1); } void onRising2(void) { processPin(2); } void loop() { Serial.print(channel_length[0]); etat=map(channel_length[0], 0, 1000, 1, 5); Serial.print(" | "); Serial.print(etat); Serial.print(" | "); Serial.print(channel_length[1]); Serial.print(" | "); Serial.print(channel_length[2]); Serial.println(""); switch (etat) { case 1: digitalWrite(led_r, HIGH); digitalWrite(led_j, LOW); digitalWrite(led_v, LOW); digitalWrite(led_b, LOW); break; case 2: digitalWrite(led_r, LOW); digitalWrite(led_j, HIGH); digitalWrite(led_v, LOW); digitalWrite(led_b, LOW); break; case 3: digitalWrite(led_r, LOW); digitalWrite(led_j, LOW); digitalWrite(led_v, HIGH); digitalWrite(led_b, LOW); break; case 4: digitalWrite(led_r, LOW); digitalWrite(led_j, LOW); digitalWrite(led_v, LOW); digitalWrite(led_b, HIGH); break; } } ==== Du Gcode au PWM ==== Avant de faire interpréter le PWM à l'Arduino désormais programmé, on va vérifier les caractéristiques du PWM qui sort du contrôleur, en fonction des Gcode envoyés. Hélas, l'interface UGS est incapable (//a priori//) d'allumer/éteindre/moduler le laser de la machine. Pour trouver les Gcode à envoyer, on a donc recours à Lightburn. La configuration du logiciel pour la Jinsoku-LE1620 est détaillée sur [[https://benmaker.fr/2021/11/18/genmitsu-jinsoku-le-1620-mini-graveur-laser/ |le blog Ben Maker]]. On met en place 4 tracés, en définissant 4 lignes à 4 puissances différentes :{{ :wiki:divers:capture_d_ecran_2022-04-28_a_18.47.23.png?400 |}} Le Gcode est sauvegardé {{ :wiki:divers:quatre_rect_puissance.gc.zip |}} : ; LightBurn 1.1.03 ; GRBL device profile, absolute coords ; Bounds: X17.22 Y16.41 to X37.78 Y65.59 G00 G17 G40 G21 G54 G90 M4 ; Cut @ 100 mm/sec, 20% power M9 G0 X17.219Y16.408 M3 ; Layer C00 G1 Y65.592S200F6000 G1 X20.781 G1 Y16.408 G1 X17.219 ; Cut @ 100 mm/sec, 40% power M9 G0 X23.219Y16.408 ; Layer C01 G1 Y65.592S400 G1 X26.781 G1 Y16.408 G1 X23.219 ; Cut @ 100 mm/sec, 80% power M9 G0 X29.219Y16.408 ; Layer C03 G1 Y65.592S800 G1 X32.781 G1 Y16.408 G1 X29.219 ; Cut @ 100 mm/sec, 100% power M9 G0 X34.219Y16.408 ; Layer C02 G1 Y65.592S1000 G1 X37.781 G1 Y16.408 G1 X34.219 M9 G1 S0 M5 G90 ; return to user-defined finish pos G0 X0 Y0 M2 Pour comprendre ce code, on se reporte à la documentation de [[https://github.com/gnea/grbl/blob/master/doc/markdown/laser_mode.md|grbl, page "Laser Mode"]]. En examinant le Gcode, on repère des lignes G1 qui sont suivies de SXXX et FXXX. On a en particulier choisi le mode d'opération M3 "puissance constante" (et non M4 modulé en fonction de la vitesse de déplacement). Le code du réglage de la puissance est SXXXX (de 0 à 1000), et F est le "feed-rate". Avec Lightburn, on envoie divers séquences, et on observe alors à l'oscilloscope : |séquence envoyée| M3 G1S100F100 | M3 G1S400F100 | M3 G1S800F100 | |PWM constaté | 10% | 40% | 80% | |observation|{{ :wiki:divers:pwm_100.png?300 }}|{{ :wiki:divers:pwm_400.png?300 }}|{{:wiki:divers:pwm_800.png?300}} | Accessoirement, on peut mesurer la fréquence du PWM, qui est 1kHz, conformément d'ailleurs à ce qui est annoncé dans la documentation de grbl. ==== Décodage par l'Arduino et remontage global ==== Un point important est d'assurer la continuité des masses sur l'ensemble du montage. Même ainsi les moteurs pas-à-pas génèrent un bruit important sur le PWM que l'on cherche à exploiter. Je dessine alors un carré avec 4 côtés de couleurs différentes dans Lightburn, j'exporte le Gcode, que j'ouvre avec UGS : ; LightBurn 1.1.03 ; GRBL device profile, absolute coords ; Bounds: X20 Y30 to X50 Y60 G00 G17 G40 G21 G54 G90 M4 ; Cut @ 100 mm/sec, 20% power M9 G0 X20Y30 M3 ; Layer C00 G1 X50S200F6000 ; Cut @ 100 mm/sec, 40% power M9 G0 X50Y30 ; Layer C01 G1 Y60S400 ; Cut @ 100 mm/sec, 60% power M9 G0 X50Y60 ; Layer C03 G1 X20S600 ; Cut @ 100 mm/sec, 100% power M9 G0 X20Y60 ; Layer C02 G1 Y30S1000 M9 G1 S0 M5 G90 ; return to user-defined finish pos G0 X0 Y0 M2 Voici le résultat : {{ :wiki:divers:jinsoku_le1620.mp4 |}}