Un relai WakeOnLAN avec Arduino

Un relai WakeOnLAN avec Arduino

Tutoriel publié en mars 2012 par Galdon dans la catégorie Divers

Avant de lire ce tutoriel, je vous conseille fortement de lire ce tuto qui explique comment fonctionne le Wake On LAN : Tout sur le Wake On LAN.

Arduino Wake on LAN repeater

Comme je l'explique dans mon billet, il est assez difficile de faire fonctionner le Wake On LAN depuis internet, les possibilités dépendent énormément du modem/routeur que vous utilisez, pour arriver à router correctement le paquet magique.

Mais sortons un peu du sujet avant d'entrer dans la pratique...

Je vais vous parler d'un bout de plastique, un circuit imprimé inventé en 2005 dans un bar d’une petite ville du nord de l’Italie.

Ce truc dont je vous parle s'appelle Arduino.

Il s'agit d'une carte électronique dotée d'un microcontrôleur qu'on peut programmer (dans un langage très proche du C++ appelé Processing), sans avoir de connaissance en électronique, aucune. Rangez donc vos fers à souder, l'engin marche "out of the box" comme on dit.

Concrètement, ça ressemble à ça :

Arduino Uno

Ce qui est génial avec l'Arduino, c'est qu'on peut étendre ses fonctionnalités en y ajoutant des modules qu'on appelle shield, comme le shield Ethernet par exemple, qui permet de connecter son Arduino à un réseau LAN via un câble RJ45, comme n'importe quel ordinateur...

Si vous voulez en savoir plus sur le projet Arduino, je vous conseille de jeter un oeil ici :
http://www.framablog.org/index.php/post/2011/12/10/arduino-histoire

Vous l'aurez deviné, je vais dans ce tutoriel vous apprendre à programmer un Arduino pour servir de relai aux paquets magiques, de façon à pouvoir faire du Wake On WAN peu importe les fonctionnalités et limitations de votre modem/routeur.

Pour créer votre relai, il va vous falloir un Arduino doté d'une fiche RJ45 pour pouvoir brancher le câble réseau. Vous avez donc le choix entre un Arduino + un shield Ethernet, ou alors un Arduino Ethernet (qui est en fait un Arduino avec un shield Ethernet intégré), plus éventuellement un adaptateur électrique pour pouvoir le faire fonctionner sans alimentation USB.

En ce qui me concerne, j'ai réalisé ce tuto avec :

  • Un Arduino Uno R3 (Revision 3)
  • Un Arduino Ethernet Shield (R3)
  • Un adaptateur 9V 660mA DC

Roadmap

Le but du jeu pour créer notre relai va donc être d'écrire un programme pour notre Arduino qui va écouter un port UDP donné (par exemple le port 10009), et qui va transmettre tout ce qu'il reçoit sur ce port vers l'adresse de broadcast (ex: 192.168.1.255), sur le port 9 (port traditionnellement utilisé pour les paquets magiques).

Il vous suffira alors de configurer sur votre modem/routeur une redirection du port UDP 9 vers l'IP de l'arduino, sur le port 10009, pour que les paquets magiques qui arrivent depuis internet sur votre routeur soient transmis à l'Arduino.

Parallèlement à cette première tâche, je vais aussi vous montrer comment utiliser l'Arduino pour toujours connaître votre IP publique...

Créer un relai pour paquet magique avec Arduino

Commnencons par télécharger & installer le logiciel de développement Arduino sur le site officiel : http://arduino.cc/en/Main/Software (si ça n'est pas déjà fait).

Lancez-le, une fenêtre s'ouvre, c'est ici qu'on va écrire le code source de notre programme :

Arduino 1.0

La première chose à faire, c'est d'inclure les bibliothèques dont nous aurons besoin :

  • SPI.h
    On doit toujours l'inclure quand on utilise un shield. Je ne sais pas si c'est nécessaire de l'inclure quand on utilise un Arduino Ethernet
  • Ethernet.h
    Fourni les fonctions qui permettent de se connecter au réseau local (via une adresse IP et une adresse MAC entre autres)
  • EthernetUdp.h
    Fourni des outils pour envoyer et recevoir des paquets UDP
#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h>

Ensuite on déclare quelques variables qui vont nous servir à nous connecter au réseau.

Attention, pour l'adresse MAC, en théorie vous pouvez mettre n'importe quoi, mais je vous conseille fortement d'utiliser celle qui est indiquée sur un sticker collé sur l'Arduino ou le shield Ethernet.

byte arduinoMAC[] = { 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 }; // Adresse MAC Arduino sticker
IPAddress arduinoIP(192, 168, 1, 200);                      // Adresse IP Arduino
unsigned int arduinoPort = 10009;                           // Port écouté par Arduino
IPAddress broadcastIP(192, 168, 1, 255);                    // Adresse IP de Broadcast LAN

// Création d'on objet de classe EthernetUDP
EthernetUDP udp;

Vous pouvez voir que j'ai créé un objet EthernetUDP, que j'ai appelé udp.
Eh oui, on n'utilise pas directement le classe EthernetUDP, on en créé une instance avant.
Certaines librairies comme EthernetUDP s'utilisent via la programmation orientée objet.

Pour lire et envoyer des données en UDP, on va avoir besoin d'une variable pour stocker temporairement les octets, donc on va déclarer un tableau de type byte (octet) de taille 102.

En effet, 102 est la taille d'un paquet magique (6 fois FF suivi de 16 fois l'adresse MAC du pc à réveiller, 6 + 16*6 = 102).

Au passage j'ai déclaré une constante MAGIC_PACKET_SIZE pour mémoriser la taille des paquets magiques, c'est plus pratique à utiliser et ça rend aussi le code plus facile à comprendre :

#define MAGIC_PACKET_SIZE 102
byte packetBuffer[MAGIC_PACKET_SIZE];

Maintenant on dispose dans notre programme de tout ce que nous avons besoin, passons au coeur de tout programme Arduino : setup() & loop() :

void setup() {
  // Connexion au réseau local
  Ethernet.begin(arduinoMAC,arduinoIP);

  // Ecoute du port redirigé par le routeur
  udp.begin(arduinoPort);
}


void loop() {
  forward_wol_magic_packet();
  delay(100);
}

Ces deux fonctions sont le strict minimum dans un programme Arduino. setup() n'est exécutée qu'une seule fois quand le circuit est mis sous tension, et loop() est une boucle infinie qui tourne en permanence, tant que le circuit est alimenté.

Le code que j'ai mis dans setup() ne devrait pas vous poser problème de compréhension
Dans la fonction loop(), j'appelle la fonction forward_wol_magic_packet().

Cette fonction n'existe pas encore, nous allons la créer juste après, son rôle est de regarder si un paquet UDP a été reçu, et si tel et le cas, de le transmettre sur l'adresse de broadcast, port 9, c'est tout !

J'ai aussi ajouté un delay de 100 millisecondes, je ne sais pas si c'est réellement nécessaire, je fais ça pour éviter au microcontrôleur de tourner "à fond" en permanence, avec ce delay il va marquer une pause de 100ms à chaque itération (via une interruption).

Bien, maintenant écrivons notre fonction forward_wol_magic_packet() :

void forward_wol_magic_packet(){
  // si on a des données à lire, on va le traiter
  int packetSize = udp.parsePacket();
  if(packetSize)
  {
    // lecture des données et stockage dans packetBufffer
    udp.read(packetBuffer, MAGIC_PACKET_SIZE);

    // envoi du paquet sur l'adresse de broadcast
    udp.beginPacket(broadcastIP, 9);
    udp.write(packetBuffer, MAGIC_PACKET_SIZE);
    udp.endPacket();
  }
}

packetSize contient le nombre d'octets des données reçues par Arduino sur le port écouté.
Si aucune donnée n'a été envoyée à Arduino, alors packetSize vaudra 0 et on n'entre pas dans le if.

Par contre si votre Arduino a reçu un paquet magique forwardé par votre modem/routeur, qui lui même l'a reçu depuis internet, alors on va lire ces données dans notre tableau packetBuffer. Vous voyez qu'au passage j'ai indiqué à la fonction write() combien d'octets elle devait lire, en l'occurrence MAGIC_PACKET_SIZE soit, si vous suivez, 102 octets !

Une fois qu'on a le paquet en mémoire, on a plus qu'à l'envoyer à tout le monde via l'adresse de broadcast, sur le port 9.

Tout comme read(), je passe en second paramètre à write() le nombre d'octets à écrire.

Ça n'est pas obligatoire, mais je vous le recommande très fortement, sinon vous pourriez avoir des problèmes si votre paquet (i.e. l'adresse MAC du destinataire) contient l'octet 0x00 (qui correspond au caractère NUL qui marque la fin d'une chaîne de caractère, et qui serait de ce fait interprété comme la fin de votre message par write() si vous ne lui aviez pas indiqué la taille des données à envoyer).

Et voilà, c'est tout !
Y'a plus qu'à compiler et uploader sur votre Arduino et ça roule.

Ceux qui le souhaitent peuvent arrêter là, dans la suite de ce tuto je vais vous montrer comment améliorer notre programme en y ajoutant un notificateur de changement d'adresse IP.

Notificateur de changement d'IP

Pouvoir diffuser les paquets magiques à tout votre réseau local c'est bien, encore faut-il connaître votre adresse IP publique (celle qui vous identifie sur internet, et qui vous a est attribuée automatiquement par votre FAI).

Pour ça, la solution la plus simple consiste à utiliser un système de DNS dynamique comme DynDNS, TZO ou encore no ip. En général, les modem/routeur sont dotés de clients intégrés pour au moins un fournisseur de DNS dynamique, il suffit de lui indiquer les identifiants de connexion et le routeur va se charger tout seul de prévenir le service quand votre IP changera.

Si pour une raison ou pour une autre vous ne voulez ou ne pouvez pas utiliser ce système, une autre solution consiste à héberger sur un serveur web un petit script PHP qui va être chargé d'enregistrer dans un fichier texte l'adresse IP des clients qui l'appellent.

Il suffit de se servir de l'Arduino pour appeler à intervalle régulier (par exemple toutes les 60 secondes) le script PHP, ainsi vous garderez toujours votre IP à jour dans le fichier texte.

Exemple:
URL du script : http://monsite.free.fr/logmyip.php
URL du fichier texte: http://monsite.free.fr/monip.txt

Alors ici je ne vais pas vous montrer la partie PHP, seulement la partie Arduino. Je vous laisse le soin d'écrire votre propre script PHP.

Si vous le souhaitez, je mets à disposition sur Stockmotion le script PHP que j'utilise. Il permet d'avoir un historique des changements d'IP :

IP Change LoggerScript PHP qui permet d'enregistrer les changements d'adresse IP.
Télécharger


On va donc commencer par ajouter quelques variables supplémentaires au début du programme (avant la fonction setup) :

// Client TCP pour notification script PHP
EthernetClient tcpClient;

// Paramètres d'appel du script PHP
char notifierHostname[] = "monsite.free.fr";
char nofifierPath[]     = "/logmyip.php";

Pour exécuter le script PHP, nous allons devoir envoyer une requête HTTP au serveur web. Le protocole HTTP (qui est est un protocole de la couche application) utilise une connexion TCP pour établir la liaison entre le client et le serveur. Le schéma classique est le suivant :

  1. Le client ouvre une connexion TCP avec le serveur
  2. Le client envoi sa requête HTTP au serveur via la connexion TCP qu'il vient d'ouvrir
    exemple: GET /mapage.html HTTP/1.0
  3. Le serveur envoie la réponse à la requête du client via la connexion TCP
  4. Le client ferme la connexion TCP
    Note: avec le protocole HTTP 1.1, la connexion n'est pas toujours fermée, elle peut être réutilisée pour envoyer d'autres requêtes HTTP, on appelle ça le pipelining.

Donc nous allons avoir besoin de gérer une connexion TCP avec l'Arduino, ça tombe bien, la librairie Ethernet.h fourni une classe EthernetClient qui permet de faire ça simplement.

Comme nous l'avont fait tout à l'heure avec EthernetUdp, on créé un objet de classe EthernetClient que j'ai appelé tcpClient.

Les 2 autres variables servent juste à mémoriser le nom de domaine (host) sur lequel est hébergé le script, ainsi que le chemin absolu (URL) du script sur le serveur.

Maintenant nous allons créer une fonction qui va appeler le script en se connectant au serveur (via TCP), et en lui envoyant une requête HTTP :

void ip_change_notify(char* host, char* path){
  if (tcpClient.connect(host, 80)) {
    tcpClient.println("GET " + (String)path + " HTTP/1.0");
    tcpClient.println("Host: " + (String)host);
    tcpClient.println("User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:8.0.1) Gecko/20100101 Firefox/8.0.1");
    tcpClient.println("Accept: text/html,application/xhtml+xml,application/xml;q=0.9");
    tcpClient.println("Accept-Language: fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3");
    tcpClient.println("Accept-Encoding: gzip, deflate");
    tcpClient.println("Accept-Charset: ISO-8859-1,utf-8;q=0.7");
    tcpClient.println("Connection: close");
    tcpClient.println(); // Fin du HTTP header (ligne vide)
  }
  tcpClient.stop();
}

Et voici un exemple d'appel de cette fonction :

ip_change_notify(notifierHostname, nofifierPath);

Comme vous le voyez, cette fonction prend en paramètre le nom de domaine (ça peut aussi être une adresse IP), ainsi que le chemin (URL) du script à appeler.

On commence par ouvrir une connexion TCP en utilisant la fonction connect(host, port) . Pour ceux qui l'ignorent, 80 correspond au port par défaut pour le protocole HTTP.

Une fois que la connexion est ouverte, y'a plus qu'à envoyer notre requête.

Il ne faut pas oublier de refermer la connexion TCP dans tous les cas (même si celle-ci a échouée) avec la fonction stop(), sinon ça va planter au prochain appel de connect().

Alors vous allez me dire que j'aurais pu me contententer de faire beaucoup plus court, comme ça par exemple :

tcpClient.println("GET " + (String)path + " HTTP/1.0");
tcpClient.println("Host: " + (String)host);
tcpClient.println(); // Fin du HTTP header (ligne vide)

C'est vrai, sauf que les navigateurs web (comme Firefox, Chrome ou Safari) n'envoient jamais de requête aussi simple. En envoyant des requêtes aussi incomplètes, il est possible que le serveur refuse de la traiter, il vous renverrait alors quelque chose comme HTTP/1.0 400 Bad Request.

Donc pour être certain que tout fonctionne, j'ai tout simplement recopié bêtement les headers envoyés par Firefox. C'est très facile de les récupérer avec une extension comme Live HTTP Headers.

Multi-tâche

Bien, donc je récapitule, nous avons :

  • Une fonction forward_wol_magic_packet qui permet de transmettre des paquets magiques sur tout le réseau, et qui doit être appelée très souvent (toutes les 100 ms, à chaque tour de boucle)
  • Une fonction ip_change_notify qui permet de prévenir un script PHP d'un éventuel changement d'IP publique.
    Cette seconde fonction doit quant à elle être appelée beaucoup moins souvent (il ne faut pas flooder le serveur PHP), disons une fois par minute

Malheureusement, l'Arduino ne supporte pas le multitâche (les threads) nativement. Il existe des librairies qui permettent d'émuler ce fonctionnement, mais pour ce tuto on va faire plus simple, il est possible de s'en sortir relativement facilement avec un simple compteur.

À chaque itération de la boucle, on va incrémenter ce compteur. Étant donné qu'on a un delay de 100ms à chaque itération, un tour de boucle prendra environ 100ms (un peu plus puisque l'exécution des autres instructions contenues dans la boucle consomme du temps).

Dans 1 minute il y a 60 secondes.
Dans 1 seconde il y a 10 fois 100 millisecondes.
Donc dans 60 secondes il y a 60*10 soit 600 fois 100 millisecondes

On va donc devoir appeler notre fonction ip_change_notify une fois toutes les 600 itérations !

Allons-y, on déclare 2 variables :

int notifierPeriod = 600; // Le delay de la loop vaut 100ms, je veux exécuter le notifier toutes les 60s, 60/0.1 = 600
int loopCount      = 0;

Et voilà le nouveau visage de la boucle loop() :

void loop() {
  // Job 1
  forward_wol_magic_packet();

  // Job 2
  if( loopCount >= notifierPeriod ){
    ip_change_notify(notifierHostname, nofifierPath);
    loopCount = 0;
  }

  loopCount++;
  delay(100);
}

Il ne faut pas oublier de remettre le compteur à 0, sinon on ne passera qu'une fois dans la condition (sans compter qu'on risque d'atteindre les limites de la mémoire au bout d'un moment si le compteur ne cesse de grossir).

Voilà le code final du programme :

/*
 * Arduino Wake on LAN repeater
 * + IP change notifier
 * © 2012 www.finalclap.com
**/

#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h>

byte arduinoMAC[] = { 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 }; // Adresse MAC Arduino sticker
IPAddress arduinoIP(192, 168, 1, 200);                      // Adresse IP Arduino
unsigned int arduinoPort = 10009;                           // Port écouté par Arduino
IPAddress broadcastIP(192, 168, 1, 255);                    // Adresse IP de Broadcast LAN

// Buffer
#define MAGIC_PACKET_SIZE 102
byte packetBuffer[MAGIC_PACKET_SIZE];

EthernetUDP udp;          // Création d'on objet de classe EthernetUDP
EthernetClient tcpClient; // Client TCP pour notification script PHP IPChangeLog

// Paramètres d'appel du script PHP
char notifierHostname[] = "monsite.free.fr";
char nofifierPath[]     = "/logmyip.php";
int notifierPeriod      = 600; // Le delay de la loop vaut 100ms, je veux éxécuter le notifier toutes les 60s, 60/0.1 = 600
int loopCount           = 0;

void setup() {
  // Connexion au réseau local
  Ethernet.begin(arduinoMAC,arduinoIP);

  // Ecoute du port redirigé par le routeur
  udp.begin(arduinoPort);
}

void loop() {
  // Job 1
  forward_wol_magic_packet();

  // Job 2
  if( loopCount >= notifierPeriod ){
    ip_change_notify(notifierHostname, nofifierPath);
    loopCount = 0;
  }

  loopCount++;
  delay(100);
}

/*
 * Job #1 : Wake On Lan magic packet forwarding
**/
void forward_wol_magic_packet(){
  // si on a des données à lire, on va le traiter
  int packetSize = udp.parsePacket();
  if(packetSize)
  {
    // lecture des données et stockage dans packetBufffer
    udp.read(packetBuffer, MAGIC_PACKET_SIZE);

    // envoi du paquet sur l'adresse de broadcast
    udp.beginPacket(broadcastIP, 9);
    udp.write(packetBuffer, MAGIC_PACKET_SIZE);
    udp.endPacket();
  }
}

/*
 * Job #2 : IP change notifier
**/
void ip_change_notify(char* host, char* path){
  if (tcpClient.connect(host, 80)) {
    tcpClient.println("GET " + (String)path + " HTTP/1.0");
    tcpClient.println("Host: " + (String)host);
    tcpClient.println("User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:8.0.1) Gecko/20100101 Firefox/8.0.1");
    tcpClient.println("Accept: text/html,application/xhtml+xml,application/xml;q=0.9");
    tcpClient.println("Accept-Language: fr,fr-fr;q=0.8,en-us;q=0.5,en;q=0.3");
    tcpClient.println("Accept-Encoding: gzip, deflate");
    tcpClient.println("Accept-Charset: ISO-8859-1,utf-8;q=0.7");
    tcpClient.println("Connection: close");
    tcpClient.println(); // Fin du HTTP header (ligne vide)
  }
  tcpClient.stop();
}

Have fun ;)

Découvrez ce tutoriel photoshop : texte "boules à neige" à lire tout de suite !

2 commentaires :
commentaire n°2668 par Davidp
Davidp samedi 26 octobre 2013, 17:56
Merci, superbe astuce qui permet de pallier le problème de WOL de la livebox2 !
commentaire n°7309 par Patrice12345
Patrice12345 lundi 16 août 2021, 16:51
Ayant un ordinateur distant que j'aimerais beaucoup allumer/éteindre à distance votre programme m'intéresse beaucoup. Malheureusement je n'y connait pas grand chose en programmation à part le copier/coller! C'est pourquoi auriez-vous la gentillesse 1)de me donner un modèle de script PHP et 2) de me donner les modifications éventuelles à apporter pour utiliser Windows 10, Chrome et une Livebox Play (la noire) Un grand, grand merci
facultatif
Facebook Twitter RSS Email
Forum Excel
Venez découvrir le nouveau forum excel question/réponse à la stackoverflow.com !
Forum Excel
hit parade n'en a rien a foutre du W3C Positionnement et Statistiques Gratuites Vincent Paré