Souhaitant remplacer mon radio-réveil par une nouvelle horloge, je me suis lancé dans le développement matériel, avec Arduino, d'un système de message et notification relié à internet.
Idée générale
L'idée générale est de déporter toute la scénarisation et le calcul de l'affichage sur un serveur web.
Le périphérique ZeKube est donc un 'simple' micro-écran déporté avec fonction d'interaction utilisateur. De base le périphérique demande régulièrement ce qu'il faut afficher. Le type de vidéo diffusé est adapté à chaque modèle de ZeKube en fonction des caractéristique envoyé par celui-là dans la requête HTTP.
Du coup, il est facile d'étendre le système tant pour la logique de notification que pour les type d'affichage, tout ce fait coté serveur web.
Le système étant multi-utilisateur et multi-ZeKube, il y a une logique d'enregistrement des utilisateurs et des ZeKube. Cette logique est entièrement gérée coté serveur web et passe par la saisie d'un code unique affiché sur ZeKube, prouvant que l'utilisateur possède bien le périphérique à ajouter.
La logique du Kube est la suivante :
- envoyer une requête HTTP au serveur avec les caractéristiques du Kube dans des en-tête spécifiques, ajout de la référence du Kube (numéro de série, modèle) et de l'état des éventuelles capteurs (accéléromètre...) à la requête CGI
- récupération de la réponse et affichage à la volée (streaming) après décodage du début du 'film' indiquant le format (entrelacement audio-vidéo) et le débit d'affichage (nombre d'images par seconde)
- s'il y a interaction physique (mouvement, tape...) sur le Kube => interruption de l'affichage et retour à l'étape 1
- s'il le 'film' est terminé ou s'il y a une coupure => retour à l'étape 1
La logique du serveur web, recevant une requête d'un Kube est :
- avec le numéro de série envoyé dans la requête HTTP du Kube :
- si le Kube n'est pas présent dans la base de donnée => enregistrement dans la base de donnée sans rattachement à un utilisateur
- si le Kube est présent dans la base mais non attaché à un utilisateur => affichage par défilement de l'URL du site sur le Kube => l'utilisateur peut alors saisir le numéro de série (inscrit ou fourni avec le Kube) sur l'interface web
- lors de la saisie d'un numéro de série de Kube sur l'interface web => génération d'un code unique pour ce Kube + affichage par défilement du dernier code généré => l'utilisateur doit alors saisir ce code sur l'interface web
- saisie du code unique d'un kube => rattachement du Kube à l'utilisateur qui vient de prouver qu'il est bien devant ce Kube
- à partir de maintenant à chaque requête HTTP du Kube, les notifications sont dépilés et génère un film à afficher sur le Kube
Notes :
- Le Kube, ne sait qu'afficher un film, la logique d'enregistrement et de notification est entièrement générée coté serveur web.
- Le serveur web connaît les caractéristiques du Kube, grâce à la requête HTTP, et adapte le calcul du film (résolution, plan des pixels...) en fonction de ces informations.
- Les notification sont gérées (ajout, suppression) par une pile dans la base de données.
- Les utilisateurs ont accès à un micro tableau de bords à travers la base de données : liste de leur Kube, attachement/détachement des Kube, ajout de notifications...
- Enfin, une API HTTP est disponible pour insérer des notifications depuis des programmes externes (script en ligne de commande utilisant curl/wget, bot Jabber...).
La forme de la requête HTTP, envoyée par le Kube est :
[http://zekube.toto.com/device/movie?serial=xxxxx&orientation=yyyy&move="zzzzzz](http://zekube.toto.com/device/movie?serial=xxxxx&orientation=yyyy&move="zzzzzz) avec l'ajout des 2 en-têtes HTTP suivant : "User-Agent: ZeKube/1" et "ZEKUBE_SESSION=sdfqfqsdfqsfqsf"
Montage WiFi avec TinyDuino
TinyDuino est une série de micro-carte compatible Arduino. Il existe déjà tout une série de shield pour cette série.
Nous allons utiliser ici la suite :
- carte processeur TinyDuino
- carte interface USB pour la programmation et l'alimentation
- shield WiFi ATWINC1500
- shield Accelerometer ASD2611
- shield Proto Terminal pour de connecter au circuit d'affichage
Pour le montage électronique de l'affichage sur matrice LED :
- MAX7219
- Matrix LED bicolor 8*8
- 3 résistances
- 2 condensateurs
Ce qui donne l'ensemble :
/*----------------------------------------------------------------------- * ZeKube * * (c) Antoine Emerit *-----------------------------------------------------------------------*/ //#define DEBUG #define LOG #define WIFI101 //#define CC3000 //#define ETHERNET #define MATRIX_LED //#define MATRIX_NEO //----------------------------------------------------------------------- // Bibliothèques //----------------------------------------------------------------------- #ifdef WIFI101 #include <SPI.h> #include <WiFi101.h> WiFiClient client; #endif #ifdef CC3000 #include <Adafruit_CC3000.h> #include <ccspi.h> #include <SPI.h> #include <string.h> Adafruit_CC3000 cc3000 = Adafruit_CC3000(8, 2, A3, 8); #endif #ifdef MATRIX_LED #include "LedControl.h" #endif #ifdef MATRIX_NEO #include "FastLED.h" #include <SPI.h> #include <Ethernet.h> #endif //----------------------------------------------------------------------- // Data //----------------------------------------------------------------------- // Movie stream format char *stream_marker = (char*)"ZK1"; unsigned char stream_format; unsigned int stream_speed; unsigned int stream_audio_trunk_size; unsigned int stream_video_trunk_size; // Device attributs #ifdef MAXTRIX_LED char device_serial[] = "1111"; #else char device_serial[] = "2222"; #endif char device_session[] = "sdfqsdlfkjqhsdflqkjdlhh654654654"; char device_orientation[] = "bottom"; char device_move[] = "freeze"; #ifdef MATRIX_LED // Matrix display LedControl lc = LedControl(3,4,5,1); #endif #ifdef MATRIX_NEO CRGB leds[64]; byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; IPAddress ip(192, 168, 100, 177); EthernetClient client; #endif //----------------------------------------------------------------------- // Initialisation (network, memory...) //----------------------------------------------------------------------- void setup() { // Loging/debug init #ifdef LOG Serial.begin(9600); Serial.println(F("============")); #endif // Network init #ifdef WIFI101 WiFi.setPins(8, 2, A3, -1); if (WiFi.status() == WL_NO_SHIELD) { #ifdef LOG Serial.println(F("WiFi shield not present")); #endif while (true) delay(500); } if (WiFi.begin(F("kozodo"), F("sdfgsdgsdgsdfgs")) != WL_CONNECTED) { #ifdef LOG Serial.println(F("No WiFi ?!")); #endif while (true) delay(500); } #ifdef LOG IPAddress ip = WiFi.localIP(); Serial.print(F("IP: ")); Serial.println(ip); #endif #endif #ifdef CC3000 if (cc3000.begin()) { while (!cc3000.connectToAP("antoine", "internetcestsuper", WLAN_SEC_WPA2)) { #ifdef LOG Serial.print(F(".")); #endif delay(1000); } } else { #ifdef LOG Serial.println("No WiFi card ?"); #endif while (true) delay(1000); } /* if (!cc3000.begin(false, true, "zekube")) { while (!cc3000.startSmartConfig("zekube")) { #ifdef LOG Serial.print(F(".")); #endif delay(1000); } } */ /* #ifdef LOG uint8_t major, minor; uint16_t version; if(!cc3000.getFirmwareVersion(&major, &minor)) { Serial.println(F("\nUnable to retrieve the firmware version!\r\n")); version = 0; } else { Serial.print(F("\nFirmware V. : ")); Serial.print(major); Serial.print(F(".")); Serial.println(minor); version = major; version <<= 8; version |= minor; } #endif */ while (!cc3000.checkDHCP()) { delay(500); } #ifdef LOG uint32_t ipAddress, netmask, gateway, dhcpserv, dnsserv; if (!cc3000.getIPAddress(&ipAddress, &netmask, &gateway, &dhcpserv, &dnsserv)) { Serial.println(F("Unable to retrieve the IP Address!\r\n")); } else { Serial.print(F("\nIP Addr: ")); cc3000.printIPdotsRev(ipAddress); Serial.print(F("\nNetmask: ")); cc3000.printIPdotsRev(netmask); Serial.print(F("\nGateway: ")); cc3000.printIPdotsRev(gateway); Serial.print(F("\nDHCPsrv: ")); cc3000.printIPdotsRev(dhcpserv); Serial.print(F("\nDNSserv: ")); cc3000.printIPdotsRev(dnsserv); Serial.println(); } #endif #endif #ifdef ETHERNET // start the Ethernet connection: if (Ethernet.begin(mac) == 0) { #ifdef LOG Serial.println("Failed to configure Ethernet using DHCP"); #endif // try to congifure using IP address instead of DHCP: Ethernet.begin(mac, ip); } // give the Ethernet shield a second to initialize: delay(1000); #ifdef LOG Serial.print("My IP address: "); for (byte thisByte = 0; thisByte < 4; thisByte++) { // print the value of each byte of the IP address: Serial.print(Ethernet.localIP()[thisByte], DEC); Serial.print("."); } Serial.println(); #endif Serial.println("connecting..."); #endif // Display init #ifdef MATRIX_LED lc.shutdown(0, false); #endif #ifdef MATRIX_NEO FastLED.addLeds<NEOPIXEL, 2>(leds, 64); #endif } //----------------------------------------------------------------------- // Main loop //----------------------------------------------------------------------- void loop() { load(); } //----------------------------------------------------------------------- // Loading the movie from the network //----------------------------------------------------------------------- void load() { unsigned long start_time = millis(), count = 0; int row_pos = 0, col_pos; unsigned char row; #ifdef LOG Serial.println(F("------------")); #endif #ifdef WIFI101 client.setTimeout(5000); if (client.connect("zekube.toto.com", 80)) { #endif #ifdef CC3000 uint32_t ip = 0; Adafruit_CC3000_Client client; if (cc3000.getHostByName("zekube.toto.com", &ip)) client = cc3000.connectTCP(ip, 80); #ifdef LOG else Serial.println(F("Couldn't resolve!")); #endif #ifdef ETHERNET if (client.connect("zekube.toto.com", 80)) { #endif if (ip && client.connected()) { #endif #ifdef ETHERNET if (client.connect("zekube.toto.com", 80)) { #endif client.print(F("GET /device/movie")); client.print(F("?serial=")); client.print(device_serial); client.print(F("&orientation=")); client.print(device_orientation); client.print(F("&move=")); client.print(device_move); client.println(F(" HTTP/1.0")); client.println(F("Host: zekube.toto.com")); client.println(F("User-Agent: ZeKube/1")); client.print(F("SetCookie: ZEKUBE_SESSION=")); client.println(device_session); client.println(); // Wait firt octet from the server while (client.connected() && !client.available()) ; // Wait for the stream marker if (client.find(stream_marker) && client.readBytes((char*)&stream_format, 1) && client.readBytes((char*)&stream_speed, 2) && client.readBytes((char*)&stream_audio_trunk_size, 2) && client.readBytes((char*)&stream_video_trunk_size, 2)) { #ifdef LOG Serial.print(F("Format ")); Serial.print(stream_format); Serial.print(F(", ")); Serial.print(stream_speed); Serial.print(F(" image(s)/s ,")); // Serial.print(stream_audio_trunk_size); // Serial.print(F(" audio B(s), ")); // Serial.print(stream_video_trunk_size); // Serial.print(F(" video B(s).")); Serial.println(F("")); #endif while (client.connected()) { while (client.available()) { row = client.read(); #ifdef LEDMATRIX // lc.setRow(0, row_pos, row); lc.setColumn(0, 7-row_pos, row); #endif #ifdef MATRIX_NEO for(int col = 0; col < 8; col++) if ((7-row_pos) % 2) leds[((7-row_pos)*8) + col] = (row & (1 << col)) ? 0x1f000c - (abs(count-col)/3)%0xffff + (count+row_pos)/5*0x0000ff : 0x000000; else leds[((7-row_pos)*8 + 7) - col] = (row & (1 << col)) ? 0x1f000c - (abs(count-col)/3)%0xffff + (count+row_pos)/5*0x0000ff : 0x000000; #endif #ifdef LOG #ifdef DEBUG Serial.print(row_pos); Serial.print(F(" = ")); Serial.println(row, BIN); #endif #endif if (++row_pos >= 8) { row_pos = 0; count++; #ifdef MATRIX_NEO FastLED.show(); #endif delay((unsigned long)(1000 / stream_speed)); #ifdef LOG #ifdef DEBUG Serial.println(F("-")); #endif #endif } } } } #ifdef LOG else Serial.println(F("No data !")); #endif } #ifdef LOG else Serial.println(F("GET failed")); #endif delay(1000); #ifdef WIFI101 client.stop(); delay(1000); # asm volatile (" jmp 0"); #endif #ifdef CC3000 client.close(); delay(1000); cc3000.disconnect(); delay(1000); asm volatile (" jmp 0"); #endif #ifdef ETHERNET client.stop(); delay(1000); asm volatile (" jmp 0"); #endif }
Notes et limites de cette version :
- le SSID et son mot de passe sont en dur dans le code (à revoir). Le top du top serait de recherche les SSID ouvert et éventuellement de passer par des requêtes DNS pour initialiser une connexion au serveur web.
- le numéro de série est en dur dans le code (à revoir). Il faudrait pouvoir en générer un au premier démarrage du Kube, et l'afficher à la demande.
- pas de gestion du son et de l'accéléromètre (testé dans une version précédente, à revoir).
Montage Ethernet avec Arduino UNO
Ici c'est un afficheur, fait maison, à base de LED RBG Neo Pixel qui sert de matrice. Les Neo Pixels sont des LED adressables en série qui permette un affichage sur 24 bits :
Cette version utilise le shield ethernet Arduino officiel sur lequel a été soudé l'option POE. Du coup l'ensemble carte Arduino et afficheur peuvent être alimenté directement par un switch ethernet POE. Dans mon cas, n'ayant pas switch POE sous la main, j'ai utilisé un injecteur POE.
Les NEO pixels s'utilisent de façon différente par rapport à la matrice LED à travers le MAX7219, car chaque LED contient un registre à décalage de 3*8 octets et toutes les LED sont sérialisées. D'autre par, ces LED permettent un affichage en 24 bits. Il faut donc modifier le code du croquis Arduino et le code de génération des notifications. J'ai développé une version du système pour répondre à ce besoin mais je ne l'ai pas fusionné avec le code courant. A revoir.
Serveur Web
Configuration Apache
La configuration du host virtuel Apache est simplement :
<VirtualHost *:80> ServerName zekube.toto.com ServerAdmin webmaster@toto.com DocumentRoot /home/public/www/zekube.toto.com/htdocs ErrorLog /var/log/apache2/zekube.toto.com-error.log CustomLog /var/log/apache2/zekube.toto.com-access.log vhost_combined IndexOptions FancyIndexing FoldersFirst IgnoreCase NameWidth=* DescriptionWidth=* VersionSort ScanHTMLTitles # Pour mysqli php_flag magic_quotes_gpc Off <Directory /home/public/www/zekube.toto.com/htdocs> AllowOverride All Require all granted </Directory> RewriteEngine on RewriteCond %{HTTP_HOST} zekube.toto.com RewriteRule device/movie /home/public/www/zekube.toto.com/htdocs/device.php [L] LogLevel warn </VirtualHost>
Scripts PHP
Le schéma de la base de données est :
CREATE DATABASE /*!32312 IF NOT EXISTS*/ `zekube` /*!40100 DEFAULT CHARACTER SET latin1 */; USE `zekube`; -- -- Table structure for table `kubes` -- DROP TABLE IF EXISTS `kubes`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `kubes` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user` int(11) DEFAULT NULL, `serial` varchar(10) DEFAULT NULL, `code` varchar(4) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `kubes` (`serial`,`code`), UNIQUE KEY `serial` (`serial`) ) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table `notifications` -- DROP TABLE IF EXISTS `notifications`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `notifications` ( `id` int(11) NOT NULL AUTO_INCREMENT, `serial` int(11) DEFAULT NULL, `time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `class` varchar(30) DEFAULT NULL, `data` longtext, PRIMARY KEY (`id`), KEY `notifications` (`serial`,`time`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; -- -- Table structure for table `users` -- DROP TABLE IF EXISTS `users`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `users` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) DEFAULT NULL, `password` varchar(50) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`,`password`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
La page d'accueil et la page Welcome :
root@server:/home/public/www/zekube.toto.com/htdocs/index.php
<?php session_start(); ?> <h3>ZeKube official website</h3> <p><a href=welcome.php>Introduction to ZeKube</a> <p><a href=register.php>Registrer a new Kube</a> <p><a href=manage.php>Manage your Kube</a> root@server:/home/public/www/zekube.toto.com/htdocs/welcome.php <?php session_start(); ?> <p><a href=index.php>Go home</a> <h3>Welcome <?= empty($_SESSION) ? '' : $_SESSION['username'] ?></h3> <p>ZeKube is the last notification web gadget. <p>Part of the "Internet ot the thing" this small and nice notifier can alert you on many events happening on internet. From a personnel schedule to your source of information. <p>Features are : <ul> <li>Small/nice size</li> <li>Audio and video (8*8 digits) alerting</li> <li>Internet wifi access</li> </ul>
Gestion des utilisateurs
Il n'y a pas encore de gestion (ajout/suppression) des utilisateurs. La création d'un nouvel utilisateur doit donc se faire directement dans la base de données.
En revanche il y a bien un script de login des utilisateurs :
root@server:/home/public/www/zekube.toto.com/htdocs/login.php
<?php @session_start(); ?> <?php $db = new mysqli('localhost', 'zekube', '******', 'zekube'); if (!empty($_REQUEST['command']) and $_REQUEST['command'] == 'login' && !empty($_REQUEST['username']) && !empty($_REQUEST['password'])) { $result = $db->query("select * from users where username='$_REQUEST[username]' and password='$_REQUEST[password]'"); if ($result) { $record = $result->fetch_assoc(); $_SESSION['username'] = $record['username']; $_SESSION['id'] = $record['id']; } else { print "Invalid user; try again !"; } } else if (!empty($_REQUEST['command']) and $_REQUEST['command'] == 'create' && !empty($_REQUEST['username']) && !empty($_REQUEST['password'])) { $result = $db->query("insert into users set username='$_REQUEST[username]', password='$_REQUEST[password]'"); if ($result) { $_SESSION['username'] = $_REQUEST['username']; $_SESSION['id'] = $db->mysql_insert_id(); } else { print "Error, can't create user : "; print mysql_error(); } } else if (!empty($_REQUEST['command']) and $_REQUEST['command'] == 'disconnect') { session_unset(); header("Location: /"); } if (empty($_SESSION['username'])) { echo <<<END <form action=$_SERVER[PHP_SELF]> <p>Login : <input type=text name=username /> <p>Password : <input type=password name=password /> <p><input type=submit name=command value=login /> <p>or create a new account : <p><input type=submit name=command value=create /> </form> END; } else { echo <<<END <p><a href=login.php?command=disconnect>Disconnect</a></p> <p>Hello $_SESSION[username]</p> END; } ?>
Ce script est automatiquement appelé depuis les autres scripts, et vérifie à chaque appel quel utilisateur est connecté. Si ce n'est pas le cas, la page de login est affiché pour saisie des identifiants.
Gestion des Kubes
Le page register.php permet d'attacher un nouveau Kube à l'utilisateur courant :
- attacher un kube par son numéro de série puis saisie du code unique affiché sur le kube (nécessite d'être devant le périphérique)
Note: le kube ne doit pas déjà être attaché à un utilisateur (sinon il faut d'abord le détacher - voir script suivant)
root@server:/home/public/www/zekube.toto.com/htdocs/register.php
<?php session_start(); ?> <p><a href=index.php>Go home</a> <h3>Welcome <?= empty($_SESSION) ? '' : $_SESSION['username'] ?></h3> <?php if (empty($_SESSION['username'])) { echo "<p>To add a new Kube, you first need to login with your personnal account :"; include "login.php"; } else { $db = new mysqli('localhost', 'zekube', '******', 'zekube'); if (!empty($_REQUEST['command']) && $_REQUEST['command'] == 'attach' && !empty($_REQUEST['serial']) && !empty($_REQUEST['code'])) { $result = $db->query("update kubes set user='$_SESSION[id]' where serial='$_REQUEST[serial]' and code='$_REQUEST[code]' and user is null"); if ($db->affected_rows > 0) { header("Location: manage.php"); } else { print $result->error(); print "Error : no free Kube match this serial and code; try again !"; } } else if (!empty($_REQUEST['command']) && $_REQUEST['command'] == 'register' && !empty($_REQUEST['serial'])) { $code = rand(1111, 9999); $result = $db->query("update kubes set code='$code' where serial='$_REQUEST[serial]' and user is null"); if ($db->affected_rows == 0) { print $result->error(); print "Error : unknown kube or this serial number is already attached to a user (you ?) !"; } else { echo <<<END <p>To attach your new Kube to your account, you must now enter the code displayed on the Kube : <p> <form action=register.php> <p><input type=text name=serial value='$_REQUEST[serial]' readonly /> <p><input type=text name=code /> <p><input type=submit name=command value=attach /> </form> END; } } else { echo <<<END <p>To add your new Kube, you must first switch it on your kube and enter it's serial number : <p> <form action=register.php> <p><input type=text name=serial /> <p><input type=submit name=command value=register /> </form> END; } } ?>
La page manage.php permet d'afficher le tableau de bord de l'utilisateur courant, permettant de :
- lister les kube rattachés à l'utilisateur
- détacher un kube pour le libérer et le rendre enregistrable pour un autre utilisateur
- envoyer un message de notification au kube (appel l'API)
root@server:/home/public/www/zekube.toto.com/htdocs/manage.php
<?php session_start(); ?> <p><a href=index.php>Go home</a> <h3>Welcome <?= empty($_SESSION) ? '' : $_SESSION['username'] ?></h3> <?php $db = new mysqli('localhost', 'zekube', '******', 'zekube'); if (empty($_SESSION['username'])) { echo "<p>To manage your Kube(s), you first need to login with your personnal account :"; include "login.php"; } else { if (!empty($_REQUEST['command']) && $_REQUEST['command'] == 'detach') { $db->query("delete from kubes where serial='$_REQUEST[serial]'"); } print "<h3>Your Kube(s) is/are :</h3>"; $result = $db->query("select * from kubes where user='$_SESSION[id]'"); while ($record = $result->fetch_assoc()) { echo <<<END <p>$record[serial] (code=$record[code]) <a href=manage.php?command=detach&serial=$record[serial]>detach</a> <form action=api.php><input type=text name=data><input type=hidden name=serial value=$record[serial]><input type=hidden name=class value=NotifyMessage></form> <a href=api.php?serial=$record[serial]&class=NotifyAudio&data=sirene.wav>Audio test</a> END; } } ?>
Les étapes de la procédure d'enregistrement en vidéo sont :
Affichage de l'URL du site web car le Kube n'est pas encore enregistré :
Affichage du code de rattachement du Kube :
Affichage par défaut en absence de notification (horloge) :
Modules de notification
Le Kube appele toujours la même URL http://zekube.toto.com/movie?... redirigée vers device.php par une RewriteRule dans la configuration Apache. Le code de ce script est :
root@server:/home/public/www/zekube.toto.com/htdocs/device.php
<?php @session_start(); ?> <?php include "tools.php"; set_time_limit(120); $db = new mysqli('localhost', 'zekube', '******', 'zekube'); $serial = !empty($_REQUEST['serial']) ? $_REQUEST['serial'] : 0; $result = $db->query("select * from kubes where serial='$serial'"); if ($result->num_rows > 0) { $record = $result->fetch_assoc(); $code = $record['code']; $user = $record['user']; if (!empty($code)) { if (empty($user)) { send_text(" $code ", 1, 1, false); } else { if (!NotificationPop($serial, $db)) { if ($_REQUEST['orientation'] == 'front') NotifyAudio($serial, 'sirene.wav'); else if ($_REQUEST['move'] == 'anticlockwise') NotifyMessage($serial, $_REQUEST['orientation']); else if ($_REQUEST['move'] == 'shake') NotifyMessage($serial, '...'); else NotifyClock($serial); } } } else { send_text(" HTTP : / / ZEKUBE.TOTO.COM ", 1, 1, false); } } else { $db->query("insert into kubes (serial) values('$_REQUEST[serial]')"); } ?>
Le script contenant le code effectif de codage 'vidéo' des notifications est :
root@server:/home/public/www/zekube.toto.com/htdocs/tools.php
<?php //==================================================================================== // Notification management //==================================================================================== function NotificationPush($serial, $class, $data, $db) { $db->query("insert into notifications (serial, class, data) values ('$serial', '$class', '$data')"); } function NotificationPop($serial, $db) { $result = $db->query("select * from notifications where serial='$serial' order by time limit 1"); if ($result->num_rows > 0) { $record = $result->fetch_assoc(); $db->query("delete from notifications where id='$record[id]'"); $record['class']($serial, $record['data']); return true; } else return false; } //==================================================================================== // Notification type functions //==================================================================================== function NotifyMessage($serial, $text) { send_text($text, 1, 1, false); } function NotifySpeech($serial, $text) { send_text($text, 1, 1, false, "jabber.avi"); system("echo \"$text\" | espeak -v fr -s 100 --stdout | sox -t wavpcm - -q -b 8 -c 1 -e unsigned-integer -r 15000 -t raw jabber.wav"); send_mux("jabber.avi", "jabber.wav", 10); } function NotifyAudio($serial, $file) { header("Content-type: text/plain"); print "ZK1" . chr(0) . chr(208) . chr(7) . chr(250) . chr(0) . chr(0) . chr(0); //passthru("wget -q -O - \"http://mp3.live.tv-radio.com/franceinfo/all/franceinfo-32k.mp3\" | sox -t mp3 - -q -b 8 -c 1 -e unsigned-integer -r 15000 -t wavpcm -"); //shell_exec("cat /home/antoine/workspace/ZeKube_website/htdocs/source2.mp3 | sox -t mp3 - -q -b 8 -c 1 -e unsigned-integer -r 15000 -t wavpcm test.wav"); //passthru("cat /home/antoine/workspace/ZeKube_website/htdocs/test.wav"); //shell_exec("stream_audio.sh"); print file_get_contents("media/$file"); } function NotifyTime($serial, $format) { $format = empty($format) ? ' H:i ' : $format; send_text(date($format), 1, 1, false); } function NotifyClock($serial) { send_clock(time(), 1, 1, false); } function NotifyVibrate($serial, $delay) { send_vibrate($delay); } //==================================================================================== // Stream generation //==================================================================================== function send_text($text, $colorate, $speed, $debug, $file = NULL) { $size = 7; $font = "./arial.ttf"; $out = ""; if (empty($file)) { if ($debug) header('Content-Type: image/png'); else header('Content-Type: text/plain'); $out = fopen('php://output', 'w'); fwrite($out, "ZK1" . chr(0) . chr(10) . chr(0) . chr(0) . chr(0) . chr(8) . chr(0)); } else $out = fopen($file, 'w'); $position = imagettfbbox($size, 0, $font, $text); $max_step = $position[4] - $position[0]; $im = @imagecreatetruecolor($position[4] - $position[0], 8) or die('Impossible de crée un flux d\'image GD'); $text_color = imagecolorallocate($im, 254, 254, 254); imagefttext($im, $size, 0, 0, 7, -$text_color , $font, $text); if ($debug) { imagepng($im); } else { for ($step = 0; $step <= $max_step - 8; $step++) { for ($repeat = 0; $repeat < $speed; $repeat++) { for ($color = 1; $color < 2; $color++) { for ($row = 0; $row < 8; $row++) { $one_line = 0; for ($col = 0; $col < 8; $col++) $one_line = ($one_line << 1) + (imagecolorat($im, $step + $col, $row) == 0 ? 0 : (($color & $colorate) != 0 ? 1 : 0)); fwrite($out, chr($one_line)); } } } } } fclose($out); imagedestroy($im); } function send_mux($videofile, $audiofile, $interleave) { $video = fopen($videofile, 'r'); $audio = fopen($audiofile, 'r'); print "ZK1" . chr(0) . chr(120) . chr(0) . chr($interleave) . chr(0) . chr(1) . chr(0); while (feof($video) === FALSE || feof($audio) === FALSE) { $v = fread($video, 1); $a = fread($audio, $interleave); if ($v === FALSE) $v = chr(0); if ($a === FALSE) $a = chr(0); print str_pad($a, $interleave, chr(0)) . $v; } fclose($video); fclose($audio); } function send_clock($time, $colorate, $speed, $debug, $file = NULL) { $out = fopen('php://output', 'w'); header('Content-Type: image/png'); fwrite($out, "ZK1" . chr(0) . chr(1) . chr(0) . chr(0) . chr(0) . chr(8) . chr(0)); for ($second = 0; $second < 60; $second++) { $im = @imagecreatetruecolor(8, 8) or die('Impossible de créer un flux d\'image GD'); $text_color = imagecolorallocate($im, 254, 254, 254); // Draw the clock $heure = localtime($time + $second, TRUE); $h = $heure[tm_hour]; $m = $heure[tm_min]; $s = $heure[tm_sec]; imageline ($im, 3.5, 3.5, 3.5 + 2*sin(deg2rad(($h * 360) / 12)), 3.5 - 2*cos(deg2rad(($h * 360) / 12)), $text_color); imageline ($im, 3.5, 3.5, 3.5 + 4.5*sin(deg2rad(($m * 360) / 60)), 3.5 - 4.5*cos(deg2rad(($m * 360) / 60)), $text_color); imagesetpixel($im, 3.5 + 4.5*sin(deg2rad(($s * 360) / 60)), 3.5 - 4.5*cos(deg2rad(($s * 360) / 60)), $text_color); if ($debug) { imagepng($im); } else { for ($repeat = 0; $repeat < $speed; $repeat++) { for ($color = 1; $color < 2; $color++) { for ($row = 0; $row < 8; $row++) { $one_line = 0; for ($col = 0; $col < 8; $col++) $one_line = ($one_line << 1) + (imagecolorat($im, $col, $row) == 0 ? 0 : (($color & $colorate) != 0 ? 1 : 0)); fwrite($out, chr($one_line)); } } } } imagedestroy($im); } fclose($out); } function send_vibrate($delay = 10) { header('Content-Type: text/plain'); print("ZK2" . chr($delay)); } ?>
Le code du script d'API est super simple :
root@server:/home/public/www/zekube.toto.com/htdocs/api.php
<?php include "tools.php"; $serial = $_REQUEST['serial']; $class = $_REQUEST['class']; $data = empty($_REQUEST['data']) ? $_REQUEST['text'] : $_REQUEST['data']; $db = new mysqli('localhost', 'zekube', '******', 'zekube'); NotificationPush($serial, $class, $data, $db); ?>
L'ajout de nouveaux types de notification est assez simple :
- ajouter une fonction Notify*** dans le script tools.php, et c'est tout car l'API utilise le nom de la fonction (parameter class=) pour empiler une nouvelle notification.
- exemple d'appel à l'API : http://zekube.toto.com/api.php?serial=1111&class=NotifyMessage&data=Test+message.
Notes :
- il y a déjà des fonctions d'affichage de texte défilant, d'horloge numérique et analogique
- la fonction d'affichage de texte défilant nécessite le fichier de police arial.ttf (à mettre dans le dossier du serveur web au même endroit que le script tools.php)
- un début de multiplexage audio/vidéo existe dans le script tools.php mais le croquis Arduino ne contient plus le code permettant de le gérer (à revoir).
- les scripts ne sont absolument pas sécurisés, donc à ne pas mettre en accès public sur internet.
Mise en boîte
Version TinyDuino
La 'mise en boîte' de la première version, la plus compacte, s'est faite en réalisant à la main un petit circuit imprimé. Il aurait été plus propre de graver un vrai circuit, mais faute de moyen je suis passé par une plaque pastillée, un vrai cauchemar vue la taille du circuit et le nombre de ponts à souder !
Pour le boîtier physique, rien de telle qu'une impression 3D.
Le circuit imprimé
Préparation du circuit imprimé (sans les ponts vers la matrice LED) sous Fritzing; histoire de ne pas se tromper du premier coup ;-) :
Et au boulot :
Impression 3D du boîtier
Après quelques mesures et une rapide modélisation sous FreeCAD, puis impression 3D cela donne :
Version Ethernet-POE avec grand afficheur RGB
Pas de circuit imprimé à réaliser, car on reste avec la carte Arduino UNO et le shield Ethernet officiel. Il y a juste à souder le module POE sur la carte Ethernet.
Pour le boîtier, je réutilise la version officielle du projet Arduino, mais attention il n'y a pas assez de place pour le module POE et donc il ne ferme pas parfaitement. A revoir ou à modéliser en 3D.
Bien sûr il faut brancher le tout sur un switch POE ou un switch standard en y ajoutant un injecteur POE (le petit boitier noir présent ici, avec son alimentation externe).
Améliorations à prévoir
Coté matériel :
- support du son : ajout d'un ampli (simple transistor ou shield avec ampli...), multiplexage du son dans le stream
- gestion des matrice LED bi/tri-color : par registre à décalage et timing précis pour gestion des nuances
- gestion des mouvements et interruptions
- interface LiFi pour configurer l'accès WiFi (https://hackaday.io/project/153408-esp8266-screen-set-wifi-credentials)
Coté logiciel :
- configuration WiFi : par WiFi en mode serveur, par LiFi, par le portail web
- push : websocket, HTTP/2...
- sécurisation des connexions : HTTPS, certificat client
- tunnel par DNS
- notifications scriptables en javascript : coté navigateur web pour test, coté serveur pour implémentation
- notification effaçable/remplaçable
- développer des notifications standards : IM-XMPP/IRC, Supervision-Shinken, Bureautique-emails/calendrier, Social-Twitter/FaceBook...
- synchronisation des ZeKube : gestion des placements
Bonnes notifications
Antoine