Wiki

Reso-nance numérique | Arts et cultures libres

Outils du site


Panneau latéral

projets:serveurwebsurbatterie:accueil

Ceci est une ancienne révision du document !


idée cadeau pour jeunes développeurs

  • Porteur du projet : reso-nance (Laurent)
  • Date : déc/2019
  • Licence : libre !
  • Contexte : Noël
  • Fichiers : code

Description

Cadeau original et intriguant pour développeurs en herbe ou mordus d'objets connectés, ce circuit simple, fun et réutilisable leur permet de s'amuser avant de le transformer en un tout autre projet. Un ESP8266 alimenté sur batterie crée un réseau WIFI et héberge un quizz personnalisable sur un serveur web utilisant bootstrap et jquery. Une fois le quizz complété, une page web affiche le nombre de bonnes réponses. Quand toutes les réponses sont validées, un piezo joue la mélodie de “we wish you a merry christmas” et allume la led bleue intégrée.

Les questions du quizz peuvent être facilement personnalisées et l'heureux destinataire peut réutiliser le circuit en y flashant son propre code pour réaliser toutes sorte d'applications web ou d'objet connecté.

Matériaux

Le circuit ne comporte que 5 composants :

  • un ESP8266
  • une batterie lithium 18650
  • un shield de charge pour la batterie basé sur le TP5470
  • un interrupteur ON/OFF
  • un piezo

Il est facilement réalisé sur une plaque proto en époxy ou bakélite pour plus de durabilité.

Code

Le code est ici réparti en plusieurs fichiers :

  • le code arduino qui gère le wifi, le serveur web, valide les réponses et joue la mélodie en PWM sur le piezo
  • le code HTML/JS assure l'affichage graphique et l'interface utilisateur (UI)
  • les librairies externes ( bootstrap, popper et jQuery) qui permettent d'obtenir facilement une interface web responsive sont stockées sous forme de zip sur le SPIFFS

Le SPIFFS

Les ESP8266 comportent le plus souvent 4Mo de mémoire flash qui peut être répartie en une section réservée au code et une réservée au SPIFFS. Le SPIFFS ou SPI FileSystem est une des manières de gérer une partie de la mémoire flash comme s'il s'agissait d'un disque dur rudimentaire contenant plusieurs fichiers. Ces fichiers peuvent alors être téléversés sur l'ESP 8266 à l'aide d'un plugin pour l'IDE d'arduino Lors du téléversement du code depuis l'IDE d'arduino, il est nécessaire de lui indiquer la taille allouée au SPIFFS (ici 1Mo ou plus) : une fois la carte LOLIN/WEMOS D1 sélectionnée, le sous-menu outil→flash size permettra d'allouer de la mémoire au SPIFFS

Code Arduino

Pour téléverser le code depuis Arduino, la librairie ESP8266 ainsi que le plugin SPIFFS doivent être préalablement installés. Le code initialise le SPIFFS, indique les routes vers les librairies statiques stockées dans le SPIFFS, stocke et vérifie les bonnes réponses et joue la mélodie sur le piezo via la librairie “notes.h” qui converti chaque note en fréquence en Hz. Il est commenté ligne par ligne (en anglais) ++ Code ESP8266.ino

/*Copyright 2019 Reso-nance Numérique <laurent@reso-nance.org>
 
  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.
 
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
 
  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  MA 02110-1301, USA.
 
 
      -------------- Pinout --------------
      pin 12 (GPIO18) ------> Piezo + 
      (put a 220 Ohm resistor between piezo- and gnd) 
  this sketch needs at least 1M SPIFF (the code uses ~29ko of FLASH and SPIFFS files needs ~197ko)
*/
 
#include <ESP8266WiFi.h> // manage the softAP functionnality
#include <ESP8266WebServer.h> // self explanatory
#include <FS.h> // for the SPIFFS (SPI file system)
#include "pitches.h" // convert human readable musical notes into pitches in hertz
 
#define SERIAL_DEBUG
#define BUZZER_PIN D2
 
IPAddress    apIP(10, 0, 0, 1);  // Defining a static IP address: local & gateway
// Default IP in AP mode is 192.168.4.1
 
const char *ssid = "JoyeuxNoel";
// const char *password = "joyeuxnoel";
const String goodAnswers[]={"0", "2", "1", "3", "1", "3", "0", "1", "3", "0"}; // in the order of the questions
const unsigned int answerCount = 10; // since answers are stored as const Strings, we cannot use the sizeof(goodAnswers)/sizeof(String) trick here
 
int wish_melody[] = { // "we wish you a merry christmas" notes. One is off, to be corrected...
  NOTE_B3, // theses notes will be converted to pitch in Hz by the pitches.h library
  NOTE_F4, NOTE_F4, NOTE_G4, NOTE_F4, NOTE_E4,
  NOTE_D4, NOTE_D4, NOTE_D4,
  NOTE_G4, NOTE_G4, NOTE_A4, NOTE_G4, NOTE_F4,
  NOTE_E4, NOTE_E4, NOTE_E4,
  NOTE_A4, NOTE_A4, NOTE_B4, NOTE_A4, NOTE_G4,
  NOTE_F4, NOTE_D4, NOTE_B3, NOTE_B3,
  NOTE_D4, NOTE_G4, NOTE_E4,
  NOTE_F4
};
 
int wish_tempo[] = {// relative note durations defining the rythme of the melody
  4,
  4, 8, 8, 8, 8,
  4, 4, 4,
  4, 8, 8, 8, 8,
  4, 4, 4,
  4, 8, 8, 8, 8,
  4, 4, 8, 8,
  4, 4, 4,
  2
};
 
ESP8266WebServer server(80);
 
#ifdef SERIAL_DEBUG // I defined theses two functions to avoid using "#ifdef... #endif" each time
#define debugPrint(x)  Serial.print (x)
#define debugPrintln(x)  Serial.println (x)
#else
#define debugPrint(x)
#define debugPrintln(x)
#endif
 
void handleNotFound() {// 404 manager, no fancy html here, just plain text sent by the server
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();// the unreacheable address
  message += "\nMethod: ";
  message += ( server.method() == HTTP_GET ) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
  for ( uint8_t i = 0; i < server.args(); i++ ) {
    message += " " + server.argName ( i ) + ": " + server.arg ( i ) + "\n";
  }
  server.send ( 404, "text/plain", message );
}
 
void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, HIGH);
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(BUZZER_PIN, LOW);
 
#ifdef SERIAL_DEBUG
  Serial.begin(115200);
#endif
  debugPrintln();
  debugPrintln("Configuring access point...");
 
  //set-up the custom IP address
  WiFi.mode(WIFI_AP_STA);
  WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));   // subnet FF FF FF 00 or 255.255.255.0
 
  WiFi.softAP(ssid);
  // WiFi.softAP(ssid, password); // we can add a password if needed
  WiFi.hostname("quizz"); // on mac and linux clients, you may reach it by http://hostname.local
  IPAddress myIP = WiFi.softAPIP();
  debugPrint("AP IP address: ");
  debugPrintln(myIP);
 
  SPIFFS.begin(); // initiate the SPI filesystem
 
// server routes, much like PhP, each URL is linked to a function
  server.onNotFound ( handleNotFound );
  server.on("/", fileindex);
  server.on("/index.html", fileindex);
  server.on("/bootstrap.min.css", bootstrapCSS);
  server.on("bootstrap.min.css", bootstrapCSS);
  server.on("/popper.min.js", popper);
  server.on("/bootstrap.min.js", bootstrapJS);
  server.on("bootstrap.min.js", bootstrapJS);
  server.on("jquery-3.3.1.min", jquery);
  server.on("/jquery-3.3.1.min", jquery);
  server.on("/bootstrap-theme.min.css", bootstrapThemeCSS);
  server.on("bootstrap-theme.min.css", bootstrapThemeCSS);
  server.on("/favicon.ico", favicon);
  server.on("/verify", validateAnswers);
  server.begin();
  debugPrintln("HTTP server started");
}
 
void loop() {
  server.handleClient();// shortest loop ever, aren't libraries a wonderful thing ?
}
 
void validateAnswers(){ // this function is called when the user click on the validate button on the UI
  String questionsID[answerCount];// this array will contain the questions ID as Strings (ie : {"1", "2"...})
  unsigned int validatedAnswers = 0;// this will increment each time an answer is counted as valid
  for (unsigned int i=0; i<answerCount; i++) { // for each answer :
    const String test = server.arg(String(i)); // create a string from the answer number since the data received in POST are always strings
    if (test == goodAnswers[i]) validatedAnswers++; // good job
    else debugPrintln("wrong answer : "+String(i)+" is "+test+" instead of "+goodAnswers[i]);
  }
  debugPrintln(String(validatedAnswers)+"/"+String(answerCount)+" good answers");
  // now will contruct a String containing JSON data {"goodAnswers":"XX", "totalQuestions":"YY"}
  String json =  "{\"goodAnswers\":\""+String(validatedAnswers)+"\",\"totalQuestions\":\""+String(answerCount)+"\"}";
  server.send(200, "application/json", json);// we send this json string back to the UI
  if (validatedAnswers == answerCount) {// if every answer is correct
    digitalWrite(LED_BUILTIN, LOW);// we turn the led ON
    singWeWishYou(2); // play the melody twice
    digitalWrite(LED_BUILTIN, HIGH);// and turn the led back off
  }
  else digitalWrite(LED_BUILTIN, HIGH);// else, we turn the led off, won't hurt if its already off.
}
 
void singWeWishYou(int loops) { // this function will play "we wish you a merry christmas" once on the buzzer
  // iterate over the notes of the melody:
  debugPrintln(" 'We wish you a Merry Christmas'");
  int size = sizeof(wish_melody) / sizeof(int);
  for (unsigned int i=0; i<loops; i++) {
    for (int thisNote = 0; thisNote < size; thisNote++) {
 
      // to calculate the note duration, take one second
      // divided by the note type.
      //e.g. quarter note = 1000 / 4, eighth note = 1000/8, etc.
      int noteDuration = 1000 / wish_tempo[thisNote];
 
      tone(BUZZER_PIN,wish_melody[thisNote]);// the tone library is builtin to the ESP8266 and allow to play tone by PWM on a buzzer
 
      // to distinguish the notes, set a minimum time between them.
      // the note's duration + 30% seems to work well:
      int pauseBetweenNotes = noteDuration * 1.30;
      ESP.wdtFeed();// if we don't feed the infamous watchdog, it could reboot the ESP8266 on long routines.
      delay(pauseBetweenNotes);
 
      // stop the tone playing:
      noTone(BUZZER_PIN);
    }
  }
}
 
// the functions below are routes to files stored in the SPIFFS
void fileindex()
{
  File file = SPIFFS.open("/index.html", "r");
  size_t sent = server.streamFile(file, "text/html");
}
// we have zipped the static library to save space on the ESP8266 flash
void bootstrapCSS()
{
  File file = SPIFFS.open("/bootstrap.min.css.gz", "r");
  size_t sent = server.streamFile(file, "text/css");
}
void bootstrapThemeCSS()
{
  File file = SPIFFS.open("/bootstrap-theme.min.css.gz", "r");
  size_t sent = server.streamFile(file, "text/css");
}
void popper()
{
  File file = SPIFFS.open("/popper.min.js.gz", "r");
  size_t sent = server.streamFile(file, "application/javascript");
}
void bootstrapJS()
{
  File file = SPIFFS.open("/bootstrap.min.js.gz", "r");
  size_t sent = server.streamFile(file, "application/javascript");
}
 
void jquery()
{
  File file = SPIFFS.open("/jquery-3.3.1.min.gz", "r");
  size_t sent = server.streamFile(file, "application/javascript");
}
void favicon()
{
  File file = SPIFFS.open("/favicon.ico", "r");
  size_t sent = server.streamFile(file, "image/x-icon");
}

==== HTML/JS ==== Pour plus de flexibilité, le quizz est stocké dans un tableau javascript sous la forme d'un objet par question :

[{question:"texte de la première question", answer:["réponse une", "réponse deux"]},
 {question:"texte de la seconde question", answer:["réponse une", "réponse deux"]},
  ...]

Le code HTML est généré dynamiquement pour créer une div de la classe well dans un jumbotron dans laquelle chaque réponse est représentée par un bouton radio. Un tableau est mis à jour avec chaque bouton radio coché pour chaque question dès que l'utilisateur choisit une réponse. Ce tableau est envoyé sous forme de requête POST contenant toutes les réponses (/verify?1=0&2=1&3=0…) au serveur qui répond en JSON le nombre de réponses correctes et le nombre de questions totales. L'interface web affiche alors un message indiquant ces statistiques. Si toutes les réponses sont validées, un message de félicitations est lu et le piezo joue alors la mélodie deux fois. Les codes HTML et JS étant très courts, ils sont rassemblés dans un seul et même fichier (commenté en anglais) : html/JS

| html/JS
  <!-- Copyright 2019 Reso-nance Numérique <laurent@reso-nance.org>
 
  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.
 
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.
 
  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  MA 02110-1301, USA. -->
 
<html lang="fr">
  <head>
    <meta charset="utf-8">
    <meta http-equiv='content-type' content='text/html;charset=utf-8' />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="laurent">
    <link rel="icon" href="/favicon.ico">
    <title>Joyeux Noël !</title>
 
    <link href="/bootstrap.min.css" rel="stylesheet">
    <link href="/bootstrap-theme.min.css" rel="stylesheet">
  </head>
 
  <body>
  <div class="jumbotron text-center">
    <h1 style="padding-bottom:20px"><b>sauras-tu répondre correctement à toutes ces questions ?</b></h1>
    <!-- this div will be dynamically filled with questions and answers -->
    <div id="quizz"></div>
    <!-- the validation button will send every answer as a POST request to the ESP8266 webserver -->
    <button type="button" class="btn btn-success  btn-lg" id="validate">Valider</button>
    <!-- this div will contain the response of the server after validation of the answers -->
    <div id="results" style="margin-top:20px"></div>
  </div>
 
    <script src="/jquery-3.3.1.min"></script>
    <script src="/bootstrap.min.js"></script>
    <!-- since the JS is quite small, we may as well write it in the same file as the HTML -->
    <script type="text/javascript" charset="utf-8">
 
      $(document).ready(function() {
        console.log("YAY, jquery is working !");// just to be sure the routes and the SPIFFS are working
        var quizzItems = [] // this global var will contain objects with questions and answers
        // each item is an object like {question:"the question text", answers=["possible answer 1", "possible answer 2"...]}
        quizzItems.push({question:'Quelle est la syntaxe correcte pour insérer un script externe nommé "xxx.js"?', answers:[
          "<script src=\"xxx.js\">".replace(/</g,'&lt;').replace(/>/g,'&gt;'), // we need to replace < and > by &lt; an &gt; to avoid being interpreted by the browser as actual tags
          "<script name=\"xxx.js\">".replace(/</g,'&lt;').replace(/>/g,'&gt;'),
          "<script href=\"xxx.js\">".replace(/</g,'&lt;').replace(/>/g,'&gt;')
        ]});
        quizzItems.push({question:"En javascript, quel évènement se produit quand l'utilisateur clique sur un élement HTML?", answers:[
          "onchange",in JS, which event occurs when the user clicks on an HTML element?
          "onmouseover",
          "onclick",
          "onmouseclick"
        ]});
        quizzItems.push({question:"Comment démarre une boucle while en javascript ?", answers:[
          "while i = 1 to 10",
          "while (i <= 10) ",
          "while (i <= 10; i++)"
        ]});
        quizzItems.push({question:"quelle expression/résulta se vérifiera dans une console javascript?", answers:[
          '"10"+1; -> "101"',
          '"10"-1; -> 9',
          '"b" + "a" + +"a" + "a"; -> baNaNa',
          "all of the above"
        ]});
        quizzItems.push({question:"Quelle classe bootstrap offre un conteneur de pleine largeur remplissant l'intégralité du viewport?", answers:[
          ".container-fixed ",
          ".container-fluid",
          ".container"
        ]});
        quizzItems.push({question:"Sur combien de colonnes est basée la grille bootstrap ?", answers:[
          "3",
          "6",
          "9",
          "12"
        ]});
        quizzItems.push({question:'Que sélectionnera ce sélecteur jQuery : $("div")?', answers:[
          "All div elements",
          "The first div element"
        ]});
        quizzItems.push({question:'Quel sélecteur jQuery retournera tous les éléments <div> contenant un tag <h2> ?', answers:[
          '$("div h2")',
          '$("div").has("h2")',
          '$("div:children(h2)")',
          '$("h2","div")'
        ]});
        quizzItems.push({question:"Les expressions régulières (regexp) sont :", answers:[
          "un langage utilisé par les sorcières durant les sabbats obscurs",
          "un outil cryptique mais flexible pour manipuler des chaines",
          "un mal nécessaire",
          "toutes les réponses sont justes"
        ]});
        quizzItems.push({question:"Qu'est-ce qu'un serveur LAMP ?", answers:[
          "une suite logicielle libre coomprenant Linux, Apache MySQL, Php ",
          "un ordinateur enchanté invoquant un génie pour générer la documentation de chaque ligne de code non-documentée trainant par là"
        ]});
 
        var currentResponses=new Array(quizzItems.length).fill(0) // by default, the first answer (index 0) of every question is checked
 
        for (var i=0; i<quizzItems.length; i++) {
          $("#quizz").append(generateQuestion(quizzItems[i],i)); // generate the HTML and insert it to the div
        }
 
 
        function generateQuestion(quizz, index) { // will return HTML representing one question and every answer possibles
          var html = $('<div class="well" data-id="'+index.toString()+'">'); // each question will have it's bootstrop well
          html.append('<p><h2 data-id="'+index.toString()+'">'+quizz.question); // the question itself
          var answerIndex = 0;
          quizz.answers.forEach(function(answer){ // for each possible answer :
            const checked = (answerIndex == 0) ? " checked" : " "; // check the first answer by default
            // add a radio button corresponding to the answer. Only one answer can be checked at the same time, thanks to the name attribute
            html.append('<div class="radio"> <h3><label><input type="radio" class="answer" data-id="'+answerIndex.toString()+'" name="optradio'+index.toString()+'"'+checked+'> '+answer+'</label></div>');
            answerIndex++;
          });
          html.append("</h3></div>"); // close the well
          return html;
        }
 
        $(document).on('change', '.answer', function(event) { // when a radio button is checked
          const answerValue = $(this).attr('data-id'); // retrieve it's data-id (the number of the answer)
          const questionIndex = parseInt($(this).closest(".well").attr("data-id")); // find the question index in the quizzItems array
          currentResponses[questionIndex] = answerValue; // update the answer value in currentResponses
        });
 
      $("#validate").click(function(){ // when the "validate" button gets clicked :
        var postString = "/verify?" // this string will contain all the POST data
        for (var i=0; i<currentResponses.length; i++) {
          postString += i.toString()+ "=" +currentResponses[i]; // add question index and answer value
          // for exemple, if question two has it's second answer (index=1) checked, add "2=1"
          if (i < currentResponses.length - 1) postString += "&"; // add an & except for the last one
        }
        // poststring should now look like this : "/verify?1=0&2=1&3=0"...
        console.log(postString); // just to make sure
        $.getJSON(postString, function(data){ // we POST the server and wait for it's JSON response, stored in the "data" object
          if (data.goodAnswers == data.totalQuestions) {// if the number of good answers matches the number of questions
            $("#results").html("<h1>Félicitations ! Et joyeux Noël ^^</h1>");
          }
          else $("#results").html("<h1><b>bravo, tu a eu "+data.goodAnswers+"/"+data.totalQuestions+" réponses correctes</b></h1>");
        });
      });
 
      });
    </script>
  </body>
</html>
/home/resonancg/www/wiki/data/attic/projets/serveurwebsurbatterie/accueil.1577565492.txt.gz · Dernière modification: 2019/12/28 21:38 de laurent