Projet connecté : une carte Vélib tangible

12 minutes de lecture

Projet connecté : une carte Vélib tangible

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 :

Le Cyclope

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 :

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é) :

CADMapper

Puis définissez quelques réglages :

  • Les hauteurs inconnues des immeubles à 25m par défaut ;
  • Activez la topographie pour un rendu plus réaliste.

CADMapper settings

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 :

Sketchup socle

Puis choisissez Eneroth Terrain Volume dans le menu Extension :

Sketchup socle

Enfin sélectionnez la surface en dessous de votre modèle afin de diminuer la hauteur du socle avec l’outil Push/Pull :

Sketchup socle

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 :

Sketchup carré Sketchup 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 :

Sketchup pull

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 :

Sketchup slots

💡 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 :

Sketchup final Sketchup 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 :

Cura

Vous pouvez sauvegarder le fichier et démarrer l’impression !

Et voici le résultat après 8h33 d’impression :

Cura

Vue de haut

Cura

Vue de bas avec supports

Cura

Retrait des supports

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.

Wemos D1 Mini

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.

LEDs RGB

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.

LEDs RGB

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) :

Cura

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

IDE

Rendez-vous ensuite dans le menu Outils > Type de cartes > Gestionnaire de cartes, puis tapez esp82 et téléchargez le paquet : IDE

Enfin choisissez la carte LOLIN(WEMOS) D1 R2 & mini :

IDE

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 :

Fingerprint

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 :

Assistant JSON

💡 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();
}

Le Cyclope

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 🖖

Continuer la discussion sur Twitter