# Reverse-ingeneering de la graveuse laser Jinsoku LE1620

#### Informations

- Christian Simon
- FabLabSU
- Date de fin : 29 avril 2022

#### Contexte - Motivation

Il s'agit de détourner une CNC peu chère de son usage initial. 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 [Pillink](https://wiki.fablab.sorbonne-universite.fr/wiki/doku.php?id=wiki:projets:pillink "https://wiki.fablab.sorbonne-universite.fr/wiki/doku.php?id=wiki:projets: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.

#### Présentation de la machine

C'est <span class="search_hit">graveuse</span> laser à bas-coût que l'on peut trouver sur de nombreux site, dont [Amazon](https://www.amazon.fr/Jinsoku-Engraver-Engraving-Cutting-Machine/dp/B0969ZJJYB "https://www.amazon.fr/Jinsoku-Engraver-Engraving-Cutting-Machine/dp/B0969ZJJYB"), pour 245€ environ, produite par Genmitsu et [SainSmart](https://www.sainsmart.com/collections/laser-cutting/products/genmitsu-jinsoku-le-1620-portable-single-arm-laser-engraver "https://www.sainsmart.com/collections/laser-cutting/products/genmitsu-jinsoku-le-1620-portable-single-arm-laser-engraver").

[![](https://wiki.fablab.sorbonne-universite.fr/wiki/lib/exe/fetch.php?w=400&tok=f987f3&media=wiki:%E2%80%8Bprojets:jinsoku-le1620.jpg)](https://wiki.fablab.sorbonne-universite.fr/wiki/lib/exe/detail.php?id=wiki%3Adivers%3Areverse_jinsokule1620&media=wiki:%E2%80%8Bprojets:jinsoku-le1620.jpg "wiki:​projets:jinsoku-le1620.jpg")

Elle est basée sur un contrôleur dont le firmware est basé sur l'open-source [grbl](https://github.com/grbl/grbl "https://github.com/grbl/grbl").

On peut donc la contrôler avec la plupart des logiciels usuels : [LaserGRBL](https://lasergrbl.com "https://lasergrbl.com") (Windows seulement), [Lightburn](https://lightburnsoftware.com "https://lightburnsoftware.com") (non-libre). On pense aussi à [UGS](https://winder.github.io/ugs_website/ "https://winder.github.io/ugs_website/") (Universal Gcode Sender, libre et multiplateforme).

Références : [Le blog Ben Maker](https://benmaker.fr/2021/11/18/genmitsu-jinsoku-le-1620-mini-graveur-laser/ "https://benmaker.fr/2021/11/18/genmitsu-jinsoku-le-1620-mini-graveur-laser/")

On trouve également quelques “unboxing” sur Youtube, mais ils sont sans intérêt pour la plupart.

#### 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 [Ben Ripley](https://www.benripley.com/diy/arduino/three-ways-to-read-a-pwm-signal-with-arduino/ "https://www.benripley.com/diy/arduino/three-ways-to-read-a-pwm-signal-with-arduino/"), qui présente 3 méthodes :

<div class="level3" id="bkmrk-simple%2C-basique-avec"><div class="level3">- <div class="li">Simple, basique avec la fonction pulseIn().</div>- <div class="li">Avec des interruptions externes à coups de attachInterrupt().</div>- <div class="li">Avec des bibliothèques qui implémentent des fonctions autour de ces interruptions.</div></div></div>Il utilise \[PinChangeInt\](https://playground.arduino.cc/Main/PinChangeInt/ "https://playground.arduino.cc/Main/PinChangeInt/") mais ont trouve très vite ses évolutions dont \[PinChangeInterrupt\](https://github.com/NicoHood/PinChangeInterrupt "https://github.com/NicoHood/PinChangeInterrupt"), incluse dans la base de bibliothèques de l'IDE Arduino.

On trouve facilement des exemples de gens l'utilisant, par exemple [le blog QuadMeUp de @pspychalski](https://quadmeup.com/read-rc-pwm-signal-with-arduino/ "https://quadmeup.com/read-rc-pwm-signal-with-arduino/"). 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.

```C++
 #include <PinChangeInterrupt.h>

/*
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("");
  
  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);
    
    /* test */
    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 [le blog Ben Maker](https://benmaker.fr/2021/11/18/genmitsu-jinsoku-le-1620-mini-graveur-laser/ "https://benmaker.fr/2021/11/18/genmitsu-jinsoku-le-1620-mini-graveur-laser/").

On met en place 4 tracés, en définissant 4 lignes à 4 puissances différentes :[![](https://wiki.fablab.sorbonne-universite.fr/wiki/lib/exe/fetch.php?w=400&tok=bd0853&media=wiki:divers:capture_d_ecran_2022-04-28_a_18.47.23.png)](https://wiki.fablab.sorbonne-universite.fr/wiki/lib/exe/detail.php?id=wiki%3Adivers%3Areverse_jinsokule1620&media=wiki:divers:capture_d_ecran_2022-04-28_a_18.47.23.png "wiki:divers:capture_d_ecran_2022-04-28_a_18.47.23.png")

Le Gcode est sauvegardé

```
; 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 [grbl, page "Laser Mode"](https://github.com/gnea/grbl/blob/master/doc/markdown/laser_mode.md "https://github.com/gnea/grbl/blob/master/doc/markdown/laser_mode.md").

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 :

<div class="level3" id="bkmrk-s%C3%A9quence-envoy%C3%A9e-m3-"><div class="level3"><div class="table sectionedit7"><table class="inline"><tbody><tr class="row0"><td class="col0">séquence envoyée</td><td class="col1 centeralign">M3 G1S100F100</td><td class="col2 centeralign">M3 G1S400F100</td><td class="col3 centeralign">M3 G1S800F100</td></tr><tr class="row1"><td class="col0">PWM constaté</td><td class="col1 centeralign">10%</td><td class="col2 centeralign">40%</td><td class="col3 centeralign">80%</td></tr><tr class="row2"><td class="col0">observation</td><td class="col1">[![pwm_100.png](https://wiki.fablab.sorbonne-universite.fr/BookStack/uploads/images/gallery/2022-11/scaled-1680-/pwm-100.png)](https://wiki.fablab.sorbonne-universite.fr/BookStack/uploads/images/gallery/2022-11/pwm-100.png)</td><td class="col2">[![pwm_400.png](https://wiki.fablab.sorbonne-universite.fr/BookStack/uploads/images/gallery/2022-11/scaled-1680-/pwm-400.png)](https://wiki.fablab.sorbonne-universite.fr/BookStack/uploads/images/gallery/2022-11/pwm-400.png)

</td><td class="col3">[![pwm_800.png](https://wiki.fablab.sorbonne-universite.fr/BookStack/uploads/images/gallery/2022-11/scaled-1680-/pwm-800.png)](https://wiki.fablab.sorbonne-universite.fr/BookStack/uploads/images/gallery/2022-11/pwm-800.png)</td></tr></tbody></table>

</div></div></div>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 (intégration du fichier vidéo uploadé sur Bookstack)

<video controls="controls" height="560" width="315"><source src="https://wiki.fablab.sorbonne-universite.fr/BookStack/attachments/25" type="video/mp4"></source></video>

Et encore (fichier vidéo accessible depuis Peertube) :

<video controls="controls" height="360" width="202"><source src="https://wiki.fablab.sorbonne-universite.fr/BookStack/<iframe%20title=%22jinsoku_LE1620%22%20width=%22202%22%20height=%22360%22%20src=%22https:/d-videothequepeertube.sorbonne-universite.fr/videos/embed/4ca36f2f-693f-4578-a863-0b32cf5d5795%22%20frameborder=%220%22%20allowfullscreen=%22%22%20sandbox=%22allow-same-origin%20allow-scripts%20allow-popups%22></iframe>"></source></video>