27 mars 2019
Projet connecté : une carte Vélib tangible
9 minutes de lecture
Cet article revient sur la réalisation de mon dernier petit projet IoT : le Cyclope. Une carte tangible affichant les disponibilités des stations Vélib avoisinantes. Le projet consiste à générer un modèle 3D du quartier désiré, y ajouter un support, brancher le hardware (LEDs et microcontrôleur) et enfin coder le programme embarqué.
Voici le Cyclope terminé une fois toutes ces étapes passées :
Les trois LEDs représentent trois stations Vélib. La couleur de la LED renseigne sur la disponibilité des vélos, à savoir :
- Bleu : + de 7 vélos disponibles ;
- Rose : - de 7 vélos disponibles ;
- Rouge : aucun vélo disponible.
Matériel nécessaire
Pour réaliser le Cyclope, voici ce qu'il vous faut :
- 1 x WeMos D1 mini (~10€ les 3) ;
- 3 x LEDs RGB avec carte intégrée (~2€ les 10).
Et comme autres outils (que vous devriez avoir sous la main) :
- Fer à souder ;
- Fils électriques ;
- Câble 5V micro USB ;
- Du double face ;
- Pistolet à colle ;
- Imprimante 3D.
Si vous n'avez pas d'imprimante 3D (ce qui paraît normal), vous pouvez passer par des services d’impression en ligne tels que 3DHubs, Freelabster, Slupteo ou bien pourquoi pas en acheter une (j’utilise une Creality Ender 3, trouvable à partir de 170€ sur Gearbest).
Génération du modèle 3D
La première étape consiste à générer un modèle 3D de votre quartier. Pour cela, je vous conseille d'utiliser l'excellente webapp CADmapper. Cet outil en ligne permet d’exporter des tronçons de carte en modèle 3D.
Commencez par sélectionner la zone voulue (je suis parti sur un format carré) :
Puis définissez quelques réglages :
- Les hauteurs inconnues des immeubles à 25m par défaut ;
- Activez la topographie pour un rendu plus réaliste.
J’ai choisi d’utiliser SketchUp Pro (vous pouvez l'essayer pendant 30 jours) pour modifier mon modèle et l’ai donc exporté en format SketchUp 2015+. Cliquez sur le bouton Create File pour lancer le rendu de votre modèle 3D, puis Download afin de télécharger le fichier au format skp.
Modification du modèle 3D
En l’état, le modèle n’est pas prêt à être imprimé, voici les étapes nécessaires :
- Génération d’un socle ;
- Création d’un emplacement pour le microcontrôleur ;
- Création des emplacements pour les LEDs (position des stations Vélib).
Génération d’un socle
Une fois le modèle ouvert dans SketchUp, nous pouvons voir qu'il n'y a aucun socle. Commencez tout d'abord par redimensionner votre modèle dans les dimensions désirées (10cm sur 10cm dans mon cas, le modèle d'origine étant à taille réelle…). Nous pouvons ensuite générer le socle en passant par l'extension gratuite Terrain Volume. Installez-la, puis sélectionnez la base de votre modèle :
Puis choisissez Eneroth Terrain Volume dans le menu Extension :
Enfin sélectionnez la surface en dessous de votre modèle afin de diminuer la hauteur du socle avec l'outil Push/Pull :
Création d’un emplacement pour le microcontrôleur
Nous allons désormais créer des rebords pour venir y glisser notre hardware. Pour cela, vous pouvez tracer un carré avec l'outil crayon à quelques millimètres du bord pour ensuite l'extruder toujours avec l'outil Push/Pull :
Nous créons ensuite un trou pour pouvoir passer le futur câble d'alimentation. Pour cela tracez un demi-cercle sur le rebord avec l'outil Arc puis sélectionnez-le pour enlever de la matière :
Création des emplacements pour les LEDs
Enfin, nous allons créer des petits trous pour venir y glisser nos LEDs. J’ai créer trois cylindres (de 1cm de rayon) que j’ai ensuite soustrait à mon socle. Vous pouvez passer en mode Radiographie afin de rendre la tâche plus aisée :
💡 N’hésitez pas à utiliser l’outil quotation pour faciliter votre travail.
Pour soustraire un cylindre, sélectionnez-le ainsi que votre socle puis allez dans Outils > Solides > Soustraire. C'est terminé ! Voici le rendu final :
Impression 3D
Il est temps d'imprimer votre modèle ! Exportez-le au format STL et importez-le dans le logiciel Ultimaker Cura afin de le préparer. Je laisse les réglages par défaut avec le preset de l'imprimante Ender 3, cochez juste l'option Generate Support afin de combler le vide du socle avec un pattern facilement retirable par la suite :
Vous pouvez sauvegarder le fichier et démarrer l'impression !
Et voici le résultat après 8h33 d'impression :
Mise en place du hardware
Commençons par détailler le choix du hardware.
Wemos D1 Mini
La Wemos D1 Mini est un microcontrôleur basé sur le module ESP8266 ESP-12E compatible Arduino. Vous pouvez vous le procurer entre 3€ et 5€ sur Aliexpress ou Banggood. Il a pour principaux atouts de proposer le WiFi nativement et d'être très abordable (comparé à un Particle Photon). Il sera en charge de se connecter au réseau WiFi, interroger l'API Vélib et éclairer les LEDs selon les données récupérées.
LEDs RGB avec carte intégrée
Utiliser des LEDs RGB avec carte intégrée permet de simplifier le code ainsi que le cablâge.
En effet, une LED RGB traditionnelle dipose de 4 pins : 3 pour modifier le rouge, bleu et vert et une quatrième pour la masse. Pour trois LEDs il nous aurait donc fallu 9 pins analogiques pour modifier les teintes.
Les LEDs avec carte intégrée permettent de piloter autant de LEDs que l'on veut avec seulement 3 pins : un pour la masse, un pour l'alimentation et enfin un dernier pour définir les couleurs. Il suffit juste de brancher ces LEDs en série pour pouvoir ensuite les allumer indépendamment car celles-ci sont addressables via le code.
Montage
Le montage demande quelques fils et des points de soudure (un peu difficile car ces points doivent être petits). J'ai simplement mis un point avec un pistolet à colle dans les trous (cela permet également de diffuser la lumière) et y est mis mes LEDs.
Concernant le microcontrôleur, je l'ai collé avec du double face (on aurait pu prévoir une attache directement dans le modèle 3D) :
Les pin DATA (celles du milieu) des LEDs doivent être reliées à la pin D1 de votre carte, le GROUND et le POWER aux pin GND et 5V.
Code
Dernière étape : coder le programme pour piloter le montage ! Si vous ne l'avez pas déjà fait, téléchargez l'IDE Arduino. Afin que ce-dernier puisse gérer notre carte, ajoutez cette url dans le gestionnaire de cartes supplémentaires (accessible via les préférences) :
http://arduino.esp8266.com/versions/2.5.0/package_esp8266com_index.json
Rendez-vous ensuite dans le menu Outils > Type de cartes > Gestionnaire de cartes, puis tapez esp82 et téléchargez le paquet :
Enfin choisissez la carte LOLIN(WEMOS) D1 R2 & mini :
Tout est prêt, passons au code ! (le code complet se trouve à la fin de cet article)
Connexion au WiFi
Afin que notre carte puisse récupérer les disponibilités des stations Vélib nous devont nous connecter à notre box via le WiFi. Pour cela nous allons utiliser la libraire WifiManager qui est un gestionnaire de connexion WiFi pour ESP8266. Le gros plus réside dans le fait qu'il propose un portail de configuration Web pour rentrer les identifiants lors de la première connexion :
void setup() {
WiFiManager wifiManager;
wifiManager.autoConnect("Cyclope");
}
Si aucun identifiant existe, la carte va émettre un réseau WiFi avec le SSID Cyclope sur lequel nous pouvons nous connecter pour rentrer les identifiants.
Client HTTPS
Nous allons récupérer les disponibilités des stations depuis cette url (requête XHR de la carte du site Vélib).
Afin de pouvoir faire une requête https, nous devons récupérer le fingerprint SHA1 du certificat du site. Pour cela, sous Firefox cliquez sur le cadenas vert > afficher les détails > plus d'informations > Afficher le certificat :
Vous pouvez ensuite l'utiliser pour effectuer vos appels :
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266HTTPClient.h>
#include <WiFiManager.h>
const uint8_t fingerprint[20] = {0x70, 0x6A, 0xD4, 0xA1, 0x9C, 0x9B, 0x43, 0x7F, 0x3F, 0x63, 0x37, 0xF9, 0xAA, 0xBA, 0x1B, 0x8B, 0xE6, 0x73, 0x07, 0x28};
void loop() {
std::unique_ptr<BearSSL::WiFiClientSecure>client(new BearSSL::WiFiClientSecure);
client->setFingerprint(fingerprint);
HTTPClient https;
if (https.begin(*client, "https://www.velib-metropole.fr/webapi/map/details?gpsTopLatitude=48.869089510397&gpsTopLongitude=2.399058037443126&gpsBotLatitude=48.86516072493194&gpsBotLongitude=2.385158294612239&zoomLevel=15.982579190351553")) {
int httpCode = https.GET();
if (httpCode > 0) {
Serial.printf("[HTTPS] GET... code: %d\n", httpCode);
} else {
Serial.printf("[HTTPS] GET... failed, error: %s\n", https.errorToString(httpCode).c_str());
}
https.end();
} else {
Serial.printf("[HTTPS] Unable to connect\n");
}
Serial.println("Wait 10s before next round...");
delay(10000);
}
Nous devons maintenant stocker le payload JSON dans notre code. Nous allons utiliser la librairie ArduinoJson (version 6). Déclarons un buffer pour stocker notre payload :
const size_t capacity = JSON_ARRAY_SIZE(3) + 3*JSON_OBJECT_SIZE(2) + 3*JSON_OBJECT_SIZE(6) + 3*JSON_OBJECT_SIZE(15) + 940;
DynamicJsonBuffer jsonBuffer(capacity);
L'outil https://arduinojson.org/v6/assistant nous permet de définir la variable capacity
. Collez votre payload JSON afin de récupérer le code :
💡 Pour en savoir plus sur la définition de la taille du buffer, rendez-vous dans la partie "How to specify the capacity?" de la doc de ArduinoJson.
Pilotage des LEDs
Les LEDs que nous utilisons sont compatibles avec la librairie https://github.com/adafruit/Adafruit_NeoPixel. Celle-ci va nous permettre de piloter facilement nos LEDs de manière indépendante, voici un exemple d'utilisation dans notre cas :
#include <Arduino.h>
#include <WiFiManager.h>
#define PIN 5 // pin D1
#define NUMPIXELS 3
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
void setup() {
pixels.setBrightness(80);
pixels.begin();
}
void loop() {
// …
String payload = https.getString();
JsonArray& stations = jsonBuffer.parseArray(payload);
JsonObject& menilmontant = stations[2];
int menilmontantBikes = menilmontant["nbBike"];
int menilmontantEBikes = menilmontant["nbEbike"];
setStationColor(menilmontantBikes + menilmontantEBikes, 0);
// …
delay(10000);
}
void setStationColor(uint8_t bikeCount, uint8_t ledId) {
if (bikeCount >= 7) {
pixels.setPixelColor(ledId, pixels.Color(76,131,243)); // Blue
} else if (bikeCount < 7 && bikeCount > 0) {
pixels.setPixelColor(ledId, pixels.Color(213,119,210)); // Pink
} else {
pixels.setPixelColor(ledId, pixels.Color(243,76,76)); // Red
}
pixels.show();
}
Code complet
#include <Arduino.h>
#include <ArduinoJson.h>
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266HTTPClient.h>
#include <Adafruit_NeoPixel.h>
#include <WiFiManager.h>
const uint8_t fingerprint[20] = {0x70, 0x6A, 0xD4, 0xA1, 0x9C, 0x9B, 0x43, 0x7F, 0x3F, 0x63, 0x37, 0xF9, 0xAA, 0xBA, 0x1B, 0x8B, 0xE6, 0x73, 0x07, 0x28};
#define PIN 5 // pin D1
#define NUMPIXELS 3
const size_t capacity = JSON_ARRAY_SIZE(3) + 3*JSON_OBJECT_SIZE(2) + 3*JSON_OBJECT_SIZE(6) + 3*JSON_OBJECT_SIZE(15) + 940;
DynamicJsonBuffer jsonBuffer(capacity);
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);
void setup() {
WiFi.persistent(false);
Serial.begin(115200);
WiFiManager wifiManager;
wifiManager.autoConnect("Cyclope");
pixels.setBrightness(80);
pixels.begin();
}
void loop() {
std::unique_ptr<BearSSL::WiFiClientSecure>client(new BearSSL::WiFiClientSecure);
client->setFingerprint(fingerprint);
HTTPClient https;
if (https.begin(*client, "https://www.velib-metropole.fr/webapi/map/details?gpsTopLatitude=48.869089510397&gpsTopLongitude=2.399058037443126&gpsBotLatitude=48.86516072493194&gpsBotLongitude=2.385158294612239&zoomLevel=15.982579190351553")) {
int httpCode = https.GET();
if (httpCode > 0) {
// HTTP header has been send and Server response header has been handled
Serial.printf("[HTTPS] GET... code: %d\n", httpCode);
if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
String payload = https.getString();
JsonArray& stations = jsonBuffer.parseArray(payload);
JsonObject& menilmontant = stations[2];
JsonObject& sorbier = stations[3];
JsonObject& amandiers = stations[4];
int menilmontantBikes = menilmontant["nbBike"];
int menilmontantEBikes = menilmontant["nbEbike"];
int sorbierBikes = sorbier["nbBike"];
int sorbierEBikes = sorbier["nbEbike"];
int amandiersBikes = amandiers["nbBike"];
int amandiersEBikes = amandiers["nbEbike"];
setStationColor(menilmontantBikes + menilmontantEBikes, 0);
setStationColor(amandiersBikes + amandiersEBikes, 1);
setStationColor(sorbierBikes + sorbierEBikes, 2);
}
} else {
Serial.printf("[HTTPS] GET... failed, error: %s\n", https.errorToString(httpCode).c_str());
}
https.end();
} else {
Serial.printf("[HTTPS] Unable to connect\n");
}
jsonBuffer.clear();
Serial.println("Wait 10s before next round...");
delay(10000);
}
void setStationColor(uint8_t bikeCount, uint8_t ledId) {
if (bikeCount >= 7) {
pixels.setPixelColor(ledId, pixels.Color(76,131,243)); // Blue
} else if (bikeCount < 7 && bikeCount > 0) {
pixels.setPixelColor(ledId, pixels.Color(213,119,210)); // Pink
} else {
pixels.setPixelColor(ledId, pixels.Color(243,76,76)); // Red
}
pixels.show();
}
Vous avez désormais toutes les informations pour construire votre propre Cyclope, n'hésitez pas à forker le projet pour construire d'autres formes de maps tangibles !
Peace 🖖