home castoo
chapitre electronique
Electronique PAC

Construire un lanceur de balles pour toutous.

mars 2026

Un jeu pour chien : Le lanceur de balle.

Mon chien adore jouer à la balle. Le problème c’est que je ne suis pas toujours disponible pour faire joujou avec lui.
J’ai maintenant un deuxième chien. La demande est donc double et intense !
J’ai donc décidé de leur apprendre à jouer avec un lanceur de balles.

Deux possibilités :


Acheter un lanceur dans le commerce, et je vois que pour un bon modèle, il faut ouvrir grand son porte-monnaie. De plus les balles ne sont pas forcément standard, il faut donc prévoir d’en racheter régulièrement vu que mes deux petits monstres ont un grand besoin de se faire les dents.
Deuxième possibilité, celle que je vais adopter : Construire moi-même un lanceur de balle. Certes, il ne sera pas aussi performant que ceux du commerce, mais mes chiens seront contents de leur nouveau joujou (enfin je l’espère) car j’imagine bien qu’une fois le lanceur construit, il va falloir apprendre aux chiens à déposer la balle dans le capteur. Ceci n’étant pas gagné vu que mes chiens sont jeunes et un peu fou fou. Je vous livre ici le montage de la réalisation si cela peut vous inspirer…


Les actions à réaliser

Détecter la présence d’une balle.
Ouvrir une trappe, lorsque les moteurs seront à la bonne vitesse pour laisser tomber la balle dans le lanceur.
Faire tourner les deux moteurs qui actionnent les roues lanceuses.
Attendre qu’une nouvelle balle, soit positionnée dans le lanceur.

En option :

Une page HTML qui permet :
De choisir la vitesse de chaque moteur.
De tester l’ouverture et la fermeture de la trappe.
La mise en route et la vitesse des moteurs.
Un lancement à distance, sans intervention du chien.
Les paramètres vitesses moteurs sont mémorisés en eeprom afin de retrouver la config aux prochain démarrage.
Perso j’alimente le montage avec une ancienne batterie de voiture qui est rechargée par du solaire.

La réalisation :

La structure générale est en bois.
Le capteur de balle, la trappe sont des pièces imprimées en 3D.
Les balles sont des balles de tennis.
Le processeur est un Wemos ESP8266 mini (3 euros).
Les moteurs sont des DC12 volts modele 755 (18 euros les deux)
Les drivers de moteur (pont en H haute puissance 43A) sont des BTS7960. (6 euros les deux) Le capteur infrarouge (1 euro). La trappe est actionnée par un servomoteur « Graupner C677 » mais d’autres modèles sont utilisables.
L'alimentation 12volts dans mon cas est assuré par une batterie de récupération rechargée par un panneau solaire.

Attention dans le code la librairie « servo.h » doit être modifiée car quand la version pour esp8266 est utilisée l’angle du servo est limité à 90 degrés et nous avons besoin des 180 degrés. Info trouvée sur site « reddit » voir ce lien : Modif à réaliser pour angle de 180 degrés.

PAC sonde de temperature DS18B20
Pour asservir le moteur je vais utiliser un petit module qui transforme une modulation de fréquence (PWM) en une tension variable entre 0 et 10v. J'ai testé deux modeles avec des résultats différents.
PAC Convertisseur PWM vers 0 a 10v PAC Convertisseur PWM vers 0 a 10v
Cette tension commande un variateur de vitesse triphasé du commerce qui utilise la PWM pour moduler le 380v du secteur vers le moteur triphasé. ce système contrairement à une variation de la tension permet de conserver du couple même à basse vitesse. Le modèle sélectionné est disponible sur le marché chinois à un prix très compétitif et il possède donc la possibilité d’être asservie par une tension analogique de 0 à 10v.
PAC Regulateur de tension PAC Regulateur de tension PAC Regulateur de tension

Un petit processeur type ESP8266-12 Wemos D1 Pro est utilisé, son antenne wifi extérieur devrait permettre de relever les mesures d’un peu plus loin que le sous-sol. Il permet de creer un petit serveur wifi en mode AP afin de gerer l'interface IHM. La mémoire et la vitesse de ce type de microprocesseur sont largement suffisants pour l’utilisation envisagée de plus le prix est très abordable. Ce circuit possède une mémoire EEPROM qui sera utile pour mémoriser certaines informations en cas de coupure de courant. Les 60 dernières mesures sont sauvegardées pour jusqu’à 8 sondes. Le délai entre deux mesures est parametrable entre 2 secondes et une heure ceci afin de pouvoir élaborer une stratégie de régulation dans un deuxième temps.
PAC ESP 8266 Wemos D1 Pro
Afin de suivre les horaires des mesures un circuit horloge est également utilisé. Le circuit fonctionne en mode I2C et est protégé par une pile qui garantie une sauvegarde de l’heure pendant environ 5 ans.
PAC circuit horloge


J’ai également prévu un petit boîtier imprimable en 3D.
PAC Boitier 3D

Les fichiers STL pour l'impression sont disponibles dans la partie "Impression3D" du site.



Le schéma du montage est celui-ci :
PAC chema electronique

Un petit jeu de LEGO en modules electronique


PAC Détail electronique
Les fils des sondes (jusqu'à 8) sont rassemblés sur le connecteur ainsi qu'un petit cable qui permettra l'asservissement du variateur de vitesse du moteur.

L'interface accessible en html est simple.
Selectionner le SSID Wifi : CHAUFFAGE-AP
Passe : 12345678
Adresse : http://192.168.4.1


PAC Menu HTML

Aprés avoir saisie l'adresse dans un navigateur le menu principal suivant apparait.



PAC Menu HTML EEPROM

En cas de coupure secteur il est important que l'asservissement puisse redémarer dans de bonnes conditions. C'est pourquoi des données essentiels sont sauvegardées dans une mémoire non volatile. Le nom des différentes sondes et le délai entre deux mesures. La valeur des seuils de la PWM sera sauvegardée dans une prochaine version...



PAC Menu HTML délai entre les mesures

Il est possible de régler le délai entre deux mesures entre 2 secondes et 1 heure. Ceci afin de donner le maximum de possibilité pour déterminer l'utilité de chaque sonde. Le délai est exprimé en seconde.



PAC Menu HTML Sonde

Cet écran donne l'adresse réseau des sondes et il permet d'attribuer un nom à chacune des sondes.



PAC Menu HTML mesures

Cet écran est trés important dans la premiere phase de la recherche d'optimisation, c'est lui qui va permettre d'élaborer les différents parametres pour gerer l'asservissement de la soufflerie.
Il est possible de suivre la date et l'heure des mesures ainsi que les valeurs de 8 sondes.



PAC Menu HTML Graphique

Le graphique donne une vue générale des valeurs des sondes aux horaires de captures.



PAC Menu HTML Mise à l'heure

Il est important de connaitre l'horaire des mesures, en principe l'horloge interne est à jour, mais avec les changement d'horaire été/hiver il est possible ici de se resynchroniser.



PAC Menu HTML Modif PWM

Il est possible ici de tester la conversion PWM vers la tension de commande du variateur 0 à 10v. Cette variation sera gerer aprés la formulation de l'optimisation par une formule qui tiendra compte des différentes mesures des sondes et le processeur l'appliquera automatiquement au variateur du moteur triphasé de la soufflerie.




Le code de base qui doit vous permettre de démarrer pour vos propres projets...
Il y a des choses à modifier pour l'optimiser mais il sera revu lorsque l'optimisation sera effective...


        #include <Arduino.h>
        #include <Wire.h>
        #include <OneWire.h>
        #include <DallasTemperature.h>
        #include <ESP8266WiFi.h>
        #include <ESP8266WebServer.h>
        #include <RTClib.h>
        #include "ESP_EEPROM.h"
        
        RTC_DS3231 rtc;
        DateTime timeinfo;
        unsigned long temps_qui_passe; // Mesure le delai depuis la derniere mesure
        int nb_sec_inter_mesure = 10; // temps en seconde entre deux mesure
        unsigned long temp_t; // nb_sec_inter_mesure * 10000 pour résultat en milliseconde (évite un warning de compilation)
        
        #define MAXTABTEMP 60
        struct gr_temp { // Info du graph
          uint8_t v_j; // jour (1 - 31)
          uint8_t v_h; // heure (0 - 23)
          uint8_t v_m; // minute (0 - 60)
          uint8_t v_s; // seconde (0 - 60)
          float v_t1; // temp1
          float v_t2; // temp2
          float v_t3; // temp2
          float v_t4; // temp4
          float v_t5; // temp5
          float v_t6; // temp6
          float v_t7; // temp7
          float v_t8; // temp8
        };
        gr_temp tgr_temp[MAXTABTEMP];
        
        float val_mesure[8];
        
        #define PinPWM D5 // Gpio PWM commande moteur convertion 0-10v
        int val_PWM = 255; // PWM au max
        //int i2c_sda = D2; // I2C 
        //int i2c_scl = D1; // I2C
        #define ONE_WIRE_BUS D6 // Gpio capteur temperature
        #define TEMPERATURE_PRECISION 12 // (avant ct 9) resolution
        OneWire oneWire(ONE_WIRE_BUS);
        DallasTemperature sensors(&oneWire);
        int nb_capteur;
        DeviceAddress tempDeviceAddress;
        bool capteur_HS = false;
        
        #define MAXTCAPTEUR 8
        struct t_capt { // tableau des capteurs
          uint8_t v_n;      // num
          uint8_t v_ad[8];  // adresse
        //  char v_nom[11];     // nom 10 car max
          String v_coul;    // couleur
        };
        t_capt tb_capteurs[MAXTCAPTEUR];
        
        // Définir les informations du point d'accès
        const char* ap_ssid = "CHAUFFAGE-AP"; // Nom du réseau Wi-Fi de l'ESP8266
        const char* ap_password = "12345678"; // Mot de passe du réseau Wi-Fi
        
        // Créer une instance du serveur web
        ESP8266WebServer server(80);
        
        // Variables pour les valeurs des capteurs
        float temperature;
        float humidity;
        
        String const v_css_body = "body { background-color: grey; font-family: Sans-Serif; Color: orange; text-align: center; display: flex; justify-content: center; align-items: center; height: 100vh;}";
        String const v_css_lien = "a.btn { text-decoration: none; padding: 10px; font-family: arial; font-size: 1em; color: #FFFFFF; background-color: #b2a2c7; border-radius: 24px;-webkit-border-radius: 24px; -moz-border-radius: 24px; border: 4px solid #ffffff; box-shadow: 3px 3px 8px #444444; -webkit-box-shadow: 3px 3px 8px #444444; -moz-box-shadow: 3px 3px 8px #444444; }";
        String const v_css_lien_vol = "a.btn:hover { padding: 10px; color: #ffff00; background-color: #5f497a; border: 4px solid #fbd5b5; box-shadow: 1px 1px 4px #777777; -webkit-box-shadow: 1px 1px 4px #777777; -moz-box-shadow: 1px 1px 4px #777777; }";
        
        // gestion de l'eeprom
        struct s_memo_rom {
            int delai_inter_mesure; // Delai entre deux mesures de temperature 
          int seuil_min_pwm; // Seuil mini pour la PWM du moteur
          int seuil_max_pwm; // Seuil maxi pour la PWM du moteur
            char nom_s[8][10]; // Nom des sondes
        } memo_rom, v_ram;
        int adr_deb_memo_rom = 0;
        
        // Affiche compte-rendu enregistrement eeprom
        String aff_eeprom(){
          String html_s = "<!DOCTYPE html>";
          html_s += "<html lang='fr'><head> <meta charset='UTF-8'><head>";
          html_s += "<style> " + v_css_lien + v_css_lien_vol + " </style>";
          html_s += "<title>Lecture EEPROM</title></head>";
          html_s += "<body  style='background-color: grey; font-family: Sans-Serif; Color: orange; text-align: center;'><br/><br/><br/>";
          html_s += "<table align='center' border='2'>";
          html_s += "<caption><h1>Liste des données mémorisées (en cas de coupure secteur)</h1></caption>";
          html_s += "<tr><td>Délai entre les mesures de température : </td><td>" + String(v_ram.delai_inter_mesure) + "</td></tr>";
          html_s += "<tr><td>Seuil minimum PWM commande moteur : </td><td>" + String(v_ram.seuil_min_pwm) + "</td></tr>";
          html_s += "<tr><td>Seuil maximum PWM commande moteur : </td><td>" + String(v_ram.seuil_max_pwm) + "</td></tr>";
          for(int i=0; i<8; i++)  html_s += "<tr><td>Nom capteur 0 : </td><td>" + String(v_ram.nom_s[i]) + "</td></tr>";
          html_s += "</table><br/><br/><br/>";
          html_s += "<p align='center'><br/><br/><a class='btn' href='/' target='_self'>Retour Accueil</a></p>";
          html_s += "<p align='center'><br/>Ces données doivent permettre un redémarrage autonome suite à une coupure secteur, les autres données peuvent être recalculées.</p>";
          html_s += "</body></html>";
          return html_s;
        }
        
        // Lecture eeprom
        void lecture_eeprom(){
            EEPROM.begin(sizeof(s_memo_rom));
            delay(300);
            EEPROM.get(adr_deb_memo_rom, memo_rom);
            delay(300);
            v_ram.delai_inter_mesure = memo_rom.delai_inter_mesure;
            v_ram.seuil_min_pwm = memo_rom.seuil_min_pwm;
            v_ram.seuil_max_pwm = memo_rom.seuil_max_pwm;
          for(int i=0; i<8; i++) strcpy(v_ram.nom_s[i], memo_rom.nom_s[i]);
            EEPROM.end();
            delay(300);
            aff_eeprom();
        }
        
        // Ecriture eeprom
        void ecriture_eeprom(){
            EEPROM.begin(sizeof(s_memo_rom));
            delay(300);
            memo_rom.delai_inter_mesure = v_ram.delai_inter_mesure;
            memo_rom.seuil_min_pwm = v_ram.seuil_min_pwm;
            memo_rom.seuil_max_pwm = v_ram.seuil_max_pwm;
          for(int i=0; i<8; i++) strcpy(memo_rom.nom_s[i], v_ram.nom_s[i]);
            EEPROM.put(adr_deb_memo_rom, memo_rom);
            delay(300);
            EEPROM.commit();
            delay(300);
            aff_eeprom();
            EEPROM.end();
            delay(300);
        }
        
        // -----------------------------------------------------------------------------------------------------------------------------------
        // Initialise la totalité des tableaux de variables
        void vinit_tab_val(){
          for(int v_adr = 0; v_adr <= MAXTABTEMP-1; v_adr++){
            tgr_temp[v_adr].v_j = 0;
            tgr_temp[v_adr].v_h = 0;
            tgr_temp[v_adr].v_m = 0;
            tgr_temp[v_adr].v_s = 0;
            tgr_temp[v_adr].v_t1 = 0;
            tgr_temp[v_adr].v_t2 = 0;
            tgr_temp[v_adr].v_t3 = 0;
            tgr_temp[v_adr].v_t4 = 0;
            tgr_temp[v_adr].v_t5 = 0;
            tgr_temp[v_adr].v_t6 = 0;
            tgr_temp[v_adr].v_t7 = 0;
            tgr_temp[v_adr].v_t8 = 0;
          }
        }
        
        String htmldateheure() {
          String html_s = "<!DOCTYPE html>";
          html_s += "<html lang='fr'><head> <meta charset='UTF-8'><head>";
          html_s += "<style> " + v_css_lien + v_css_lien_vol + " </style>";
          html_s += "<title>Initialisation Date / Heure</title></head>";
          html_s += "<body  style='background-color: grey; font-family: Sans-Serif; Color: orange; text-align: center;'><br/><br/><br/>";
          html_s += "<table align='center'>";
          html_s += "<caption><h1>Définir la date et l'heure</h1></caption><tr><td>";
          html_s += "<form action='/setDateTime' method='POST'>";
          html_s += "<br/>Date et Heure (YYYY-MM-DDTHH:MM):";
          html_s += "<input type='datetime-local' name='datetime'></br><br/><br/><br/></td></tr>";
          html_s += "<tr><td><br/><br/><br/><button>Valider la modification</button> <input type='reset' value='Annuler la modification' /></td></tr>";
          html_s += "</table><br/><br/><br/><br/></form>";
          html_s += "<p align='center'><br/><br/><a class='btn' href='/' target='_self'>Retour Accueil</a></p>";
          html_s += "</body></html>";
          return html_s;
        }
        
        String modif_delai_m(){
          String html_s = "<html lang='fr'><head> <meta charset='UTF-8'><head>";
          html_s += "<title>Modif delai 2 dates</title>";
          html_s += "<style> " + v_css_lien + v_css_lien_vol + " </style></head>";
          html_s += "<body style='background-color: grey; font-family: Sans-Serif; Color: orange; text-align: center;'><br/><br/><br/>";
          html_s += "<table width='80%' align='center'><caption><h1>Définir le délai entre deux mesures</h1></caption><tr><td>";
          html_s += "<form action='/setDelaiMesure' method='POST'>";
          html_s += "Délai en seconde (mini 2 secondes - maxi 3600):";
          html_s += "<input type='number' name='v_d_mesure' min='2' max='3600' value='" + String(nb_sec_inter_mesure) + "'/></br><br/><br/><br/>";
          html_s += "</td></tr><tr><td><button>Valider la modification</button> <input type='reset' value='Annuler la modification' /></td></tr>";
          html_s += "</form></table><br/><br/><br/><br/>";
          html_s += "<p align='center'><br/><br/><a class='btn' href='/' target='_self'>Retour Accueil</a></p>";
          html_s += "</body></html>";
          return html_s;
        }
        
        String modif_PWM(){
          String html_s = "<html lang='fr'><head> <meta charset='UTF-8'><head>";
          html_s += "<title>Modif PWM</title>";
          html_s += "<style> " + v_css_lien + v_css_lien_vol + " </style></head>";
          html_s += "<body style='background-color: grey; font-family: Sans-Serif; Color: orange; text-align: center;'><br/>";
          html_s += "<table width='80%' align='center'><caption><h1>Modification de la vitesse du moteur</h1></caption>";
          html_s += "<tr><td>Valeur PWM : " + String(val_PWM) + "</br><br/><br/><br/></td></tr>";
          html_s += "<tr><td align='center'><form action='/inc_vit'><button><h1>+</h1></button> Augmenter la vitesse.</form></td></tr> ";
          html_s += "<tr><td align='center'><form action='/dec_vit'><button><h1>-</h1></button> Diminuer la vitesse.</form></td></tr><br/><br/>";
          html_s += "</table><br/><br/><br/><br/>";
          html_s += "<p align='center'><br/><br/>La vitesse du moteur est commandée par une tension qui varie de 0 à 10 volts (10 volts -> Vitesse max).</p>";
          html_s += "<br/><br/><p align='center'><br/><br/><a class='btn' href='/' target='_self'>Retour Accueil</a></p>";
          html_s += "</body></html>";
          return html_s;
        }
        
        // --------- Demande de page inexistante ------
        void page_inexistante() {
            String page_inexist = "Page inexistante";
            page_inexist += "<br/>URL: ";
            page_inexist += server.uri();
            page_inexist += "<br/>Method: ";
            page_inexist += (server.method() == HTTP_GET) ? "GET" : "POST";
            page_inexist += "<br/>Arguments: ";
            page_inexist += server.args();
            page_inexist += "<br/>";
            for (uint8_t i = 0; i < server.args(); i++) { page_inexist += " " + server.argName(i) + ": " + server.arg(i) + "<br/>"; }
            server.send(404, "text/plain", page_inexist);
            #ifdef debug_sur_usb
                Serial.println("Envoi : page_inexistante");
            #endif
        }
        
        void printAddress(DeviceAddress deviceAddress)
        {
          // exemple : 28FF5246601705AF
          for (uint8_t i = 0; i < 8; i++)
          {
            if (deviceAddress[i] < 16) Serial.print("0");
            Serial.print(deviceAddress[i], HEX);
          }
        }
        
        String adr_string(DeviceAddress deviceAddress)
        {
          // exemple : 28FF5246601705AF
          char ch_temp[20];
          sprintf(ch_temp, "0x%02x%02x%02x%02x%02x%02x%02x%02x",
                  deviceAddress[0],
                  deviceAddress[1],
                  deviceAddress[2],
                  deviceAddress[3],
                  deviceAddress[4],
                  deviceAddress[5],
                  deviceAddress[6],
                  deviceAddress[7]
                );
                Serial.print(ch_temp);
                return String(ch_temp);
        }
        
        String page_capteurs() {
          // Page de description des capteurs
          String html_s = "<html lang='fr'><head> <meta charset='UTF-8'> <meta name='viewport' content='width=device-width, initial-scale=1.0'><title>PAC Chauffage</title>";
          html_s += "<style> " + v_css_body + v_css_lien + v_css_lien_vol + " </style>";
          html_s += "</head><body>";
          html_s += "<table width='80%' border=2>";
          html_s += "<caption><h1>Identification des Capteurs</h1></caption>";
          html_s += "<tr><th align='center'>Numero</th>";
          html_s += "<th align='center'>Adresse</th>";
          html_s += "<th align='center'>Nom</th>";
          html_s += "<th align='center'>Modif.</th></tr>";
          html_s += "<form action='/setNomCapt' method='POST'>";
          for (uint8_t i = 0; i < 8; i++){
            html_s += "<tr><td align='center'>";
            if(nb_capteur > i) html_s += String(tb_capteurs[i].v_n);
            html_s += "</td><td align='center'>" +  adr_string(tb_capteurs[i].v_ad) + "</td>";
            html_s += "<td align='center'>" + String(v_ram.nom_s[i]) + "</td>";
            html_s += "<td align='center'>";
            String v_temp;
            if(nb_capteur > i){  v_temp = String(v_ram.nom_s[i]); html_s += "(de 4 à 10 car.) <input type='text' id='nom_capt_" + String(i) + "' name='nom_capt_" + String(i) + "' minlength='4' maxlength='10' value='" + v_temp + "'/>";}
            html_s += "</td></tr>";
          }
          html_s += "<tr><td align='center' colspan='4'><br/><p align='center'><a class='btn' href='/' target='_self'>Retour Accueil</a> <button>Valider les modifications</button> <input type='reset' value='Annuler les modifications' /><br/><br/></p></td></tr>";
          html_s += "</form>";
          html_s += "</table>";
          html_s += "</body></html>";
          return html_s;
        }
        
        String page_valeurs(){
          // Page de description des capteurs
          String html_s = "<html lang='fr'><head> <meta charset='UTF-8'><title>PAC Chauffage</title>";
          html_s += "<style> " + v_css_lien + v_css_lien_vol + "</style>";
          html_s += "</head><body style='background-color: grey; font-family: Sans-Serif; Color: orange; text-align: center;'>";
          html_s += "<div align='center'><table width='80%' border=2>";
          html_s += "<caption><h1>Valeurs des Capteurs</h1></caption>";
          html_s += "<tr>";
          html_s += "<th width='10%' align='center'>Jour</th>";
          html_s += "<th width='10%' align='center'>Heure</th>";
          html_s += "<th width='10%' align='center'>Minute</th>";
          html_s += "<th width='10%' align='center'>Seconde</th>";
          for(int i=0; i<8; i++)  html_s += "<th width='5%' align='center'>" + String(v_ram.nom_s[i]) + "</th>";
          html_s += "</tr>";
          for(int vadr=0; vadr <= MAXTABTEMP-1; vadr++){
            if(tgr_temp[vadr].v_j){
                html_s += "<tr>";
                html_s += "<td width='10%' align='center'>" + String(tgr_temp[vadr].v_j) + "</td>";
                html_s += "<td width='10%' align='center'>" + String(tgr_temp[vadr].v_h) + "</td>";
                html_s += "<td width='10%' align='center'>" + String(tgr_temp[vadr].v_m) + "</td>";
                html_s += "<td width='10%' align='center'>" + String(tgr_temp[vadr].v_s) + "</td>";
                html_s += "<td width='5%' align='center'>" + String(tgr_temp[vadr].v_t1) + "</td>";
                html_s += "<td width='5%' align='center'>" + String(tgr_temp[vadr].v_t2) + "</td>";
                html_s += "<td width='5%' align='center'>" + String(tgr_temp[vadr].v_t3) + "</td>";
                html_s += "<td width='5%' align='center'>" + String(tgr_temp[vadr].v_t4) + "</td>";
                html_s += "<td width='5%' align='center'>" + String(tgr_temp[vadr].v_t5) + "</td>";
                html_s += "<td width='5%' align='center'>" + String(tgr_temp[vadr].v_t6) + "</td>";
                html_s += "<td width='5%' align='center'>" + String(tgr_temp[vadr].v_t7) + "</td>";
                html_s += "<td width='5%' align='center'>" + String(tgr_temp[vadr].v_t8) + "</td>";
                html_s += "</tr>";
            }
          }
          html_s += "</table></div><p align='center'><br/><br/><a class='btn' href='/' target='_self'>Retour Accueil</a></p>";
          html_s += "</body></html>";
          return html_s;
        }
        
        String page_graph() {
          // Page graph
          String html_s = "<html lang='fr'><head> <meta charset='UTF-8'> <meta name='viewport' content='width=device-width, initial-scale=1.0'><title>PAC Chauffage</title>";
          html_s += "<style>";
          html_s += ".chart-container { width: 600px; height: 400px; margin: 0 auto;}";
          html_s += ".chart {position:relative; width: 600px; height: 500px; background-color: #fff; border: 1px solid #ccc; margin:0 auto;}";
          html_s += ".line-chart {width: 100%; height: calc(100% - 80px);}";
          html_s += ".y-axis {position: absolute; top: 20px; bottom: 30px; left: 20; width: 40px; height: 390; display: flex; flex-direction: column; justify-content: space-between;}";
          html_s += ".y-tick {text-align: right; font-size: 12px; padding-right: 5px; font-weight: bold; color: #999;}";
          html_s += ".x-axis {position: absolute; bottom: 5px; left: 70px; right: 40px; height: 20px; display: flex; justify-content: space-between;}";
          html_s += ".x-axis2 {position: absolute; bottom: 20px; left: 70px; right: 40px; height: 20px; display: flex; justify-content: space-between;}";
          html_s += ".x-axis3 {position: absolute; bottom: 35px; left: 70px; right: 40px; height: 20px; display: flex; justify-content: space-between;}";
          html_s += ".x-axis4 {position: absolute; bottom: 50px; left: 70px; right: 40px; height: 20px; display: flex; justify-content: space-between;}";
          html_s += ".x-tick {font-size: 12px; text-align: center; font-weight: bold; color: #999;}";
          html_s += "</style>";
          html_s += "</head><body style='background-color: grey;'>";
          html_s += "<div class='chart-container'><div class='chart'>";
          html_s += "<svg viewBox='0 0 150 120' preserveAspectRatio='none' class='line-chart'>";
          html_s += "<line x1='0' y1='7' x2='150' y2='7' stroke='#f1f1f1' stroke-width='0.5'/><line x1='0' y1='25.5' x2='150' y2='25.5' stroke='#f1f1f1' stroke-width='0.5'/><line x1='0' y1='44' x2='150' y2='44' stroke='#f1f1f1' stroke-width='0.5'/>";
          html_s += "<line x1='0' y1='62.5' x2='150' y2='62.5' stroke='#f1f1f1' stroke-width='0.5'/><line x1='0' y1='81' x2='150' y2='81' stroke='#f1f1f1' stroke-width='0.5'/><line x1='0' y1='99.5' x2='150' y2='99.5' stroke='#f1f1f1' stroke-width='0.5'/><line x1='0' y1='118' x2='150' y2='118' stroke='#f1f1f1' stroke-width='0.5'/>";
          html_s += "<polyline fill='none' stroke='" + tb_capteurs[0].v_coul + "' troke-width='0.7' points='";
          for(int vadr=0; vadr <= MAXTABTEMP-1; vadr++){
            if(tgr_temp[vadr].v_j){
                html_s += " " + String(20 + (vadr*2)) + "," + String(100-tgr_temp[vadr].v_t1);
            }
          }
          html_s += "'/>";
          if(nb_capteur > 1){
            html_s += "<polyline fill='none' stroke='" + tb_capteurs[1].v_coul + "' troke-width='0.7' points='";
            for(int vadr=0; vadr <= MAXTABTEMP-1; vadr++){
              if(tgr_temp[vadr].v_j){
                  html_s += " " + String(20 + (vadr*2)) + "," + String(100-tgr_temp[vadr].v_t2);
              }
            }
            html_s += "'/>";
          }
          if(nb_capteur > 2){
            html_s += "<polyline fill='none' stroke='" + tb_capteurs[2].v_coul + "' troke-width='0.7' points='";
            for(int vadr=0; vadr <= MAXTABTEMP-1; vadr++){
              if(tgr_temp[vadr].v_j){
                  html_s += " " + String(20 + (vadr*2)) + "," + String(100-tgr_temp[vadr].v_t3);
              }
            }
            html_s += "'/>";
          }
          if(nb_capteur > 3){
            html_s += "<polyline fill='none' stroke='" + tb_capteurs[3].v_coul + "' troke-width='0.7' points='";
            for(int vadr=0; vadr <= MAXTABTEMP-1; vadr++){
              if(tgr_temp[vadr].v_j){
                  html_s += " " + String(20 + (vadr*2)) + "," + String(100-tgr_temp[vadr].v_t4);
              }
            }
            html_s += "'/>";
          }
          if(nb_capteur > 4){
            html_s += "<polyline fill='none' stroke='" + tb_capteurs[4].v_coul + "' troke-width='0.7' points='";
            for(int vadr=0; vadr <= MAXTABTEMP-1; vadr++){
              if(tgr_temp[vadr].v_j){
                  html_s += " " + String(20 + (vadr*2)) + "," + String(100-tgr_temp[vadr].v_t5);
              }
            }
            html_s += "'/>";
          }
          if(nb_capteur > 5){
            html_s += "<polyline fill='none' stroke='" + tb_capteurs[5].v_coul + "' troke-width='0.7' points='";
            for(int vadr=0; vadr <= MAXTABTEMP-1; vadr++){
              if(tgr_temp[vadr].v_j){
                  html_s += " " + String(20 + (vadr*2)) + "," + String(100-tgr_temp[vadr].v_t6);
              }
            }
            html_s += "'/>";
          }
          if(nb_capteur > 6){
            html_s += "<polyline fill='none' stroke='" + tb_capteurs[6].v_coul + "' troke-width='0.7' points='";
            for(int vadr=0; vadr <= MAXTABTEMP-1; vadr++){
              if(tgr_temp[vadr].v_j){
                  html_s += " " + String(20 + (vadr*2)) + "," + String(100-tgr_temp[vadr].v_t7);
              }
            }
            html_s += "'/>";
          }
          if(nb_capteur > 7){
            html_s += "<polyline fill='none' stroke='" + tb_capteurs[7].v_coul + "' troke-width='0.7' points='";
            for(int vadr=0; vadr <= MAXTABTEMP-1; vadr++){
              if(tgr_temp[vadr].v_j){
                  html_s += " " + String(20 + (vadr*2)) + "," + String(100-tgr_temp[vadr].v_t8);
              }
            }
            html_s += "'/>";
          }
          html_s += "</svg>";
          html_s += "<div class='y-axis'><div class='y-tick'>100</div><div class='y-tick'>80</div><div class='y-tick'>60</div><div class='y-tick'>40</div><div class='y-tick'>20</div><div class='y-tick'>0</div><div class='y-tick'>-20</div></div>";
          html_s += "<div class='x-axis'>";
          for(int vadr=0; vadr <= MAXTABTEMP-1; vadr+=10){
            html_s += "<div class='x-tick'>S:" + String(tgr_temp[vadr].v_s) + "</div>";
          }
          html_s += "</div><div class='x-axis2'>";
          for(int vadr=0; vadr <= MAXTABTEMP-1; vadr+=10){
            html_s += "<div class='x-tick'>M:" + String(tgr_temp[vadr].v_m) + "</div>";
          }
          html_s += "</div><div class='x-axis3'>";
          for(int vadr=0; vadr <= MAXTABTEMP-1; vadr+=10){
            html_s += "<div class='x-tick'>H:" + String(tgr_temp[vadr].v_h) + "</div>";
          }
          html_s += "</div><div class='x-axis4'>";
          for(int vadr=0; vadr <= MAXTABTEMP-1; vadr+=10){
            html_s += "<div class='x-tick'>J:" + String(tgr_temp[vadr].v_j) + "</div>";
          }
          html_s += "</div></div>";
          html_s += "</div><br><br><br><br><br><br><br><br>";
          html_s += "<table align='center'><tr>";
          for(uint8_t i = 0; i < 8; i++){
            html_s += "<td>" + String(v_ram.nom_s[i]) + " : <font color='" + tb_capteurs[i].v_coul + "'>=====</font></td>";
            if(i==3) html_s += "</tr><tr>";
          }
          html_s += "</tr></table>";
          html_s += "<br><br><p align='center'><a class='btn' href='/' target='_self'>Retour Accueil</a>";
          html_s += "</body></html>";
          return html_s;
        }
        
        String handleRoot() {
          // Serve la page principale (HTML)
          String html_s = "<html lang='fr'><head> <meta charset='UTF-8'><title>PAC Chauffage</title>";
          html_s += "<style> " + v_css_body +  v_css_lien + v_css_lien_vol + "</style>";
          html_s += "</head><body><table width='100%'>";
          html_s += "<caption><h1>Optimisation PAC</h1></caption>";
          if(capteur_HS){
            html_s += "<tr><td align='center'><br/><h1>PROBLEME INIT CIRCUIT HORLOGE ===>>>> FAIRE UN MARCHE ARRET !</h1><br/></td></tr>";
          }
          html_s += "<tr><td align='center' width='100%'>";
          html_s += "Nous sommes le " + String(timeinfo.day()) + "/" + String(timeinfo.month()) + "/" +  String(timeinfo.year());
          html_s += " il est " + String(timeinfo.hour()) + " H " + String(timeinfo.minute());
          html_s += "</td></tr>"; 
          html_s += "<tr><td align='center'><br/><p><a class='btn' href='/dh' target='_self'>Modif date / heure</a></p><br/></td></tr>";
          html_s += "<tr><td align='center'><br/><p><a class='btn' href='/aff_eeprom' target='_self'>Affichage données EEPROM</a></p><br/></td></tr>";
          html_s += "<tr><td align='center'>Nb maximum valeurs mémorisées : " + String(MAXTABTEMP) + "</td></tr>";
          html_s += "<tr><td align='center'>Délai entre deux mesures : " + String(nb_sec_inter_mesure) + " (secondes)</td></tr>";
          html_s += "<tr><td align='center'><br><p align='center'><a class='btn' href='/modif_delai_m' target='_self'>Modifier délai entre 2 mesures</a></p></td></tr> ";
          html_s += "<tr><td align='center'><br><p align='center'><a class='btn' href='/init_tab_val' target='_self'>Init tableau mesures</a></p></td></tr> ";
          html_s += "<tr><td align='center'><br><br>Nb de capteur detecté : " + String(nb_capteur) + "</td></tr>";
          html_s += "<tr><td align='center'><br><p align='center'><a class='btn' href='/capteur' target='_self'>Liste des capteurs</a></p></td></tr>";
          html_s += "<tr><td align='center'>";
            html_s += "<br><br><a class='btn' href='/page_graph' target='_self'>Graphique</a>  ";
            html_s += "<a class='btn' href='/page_valeurs' target='_self'>Liste des valeurs</a>";
          html_s += "</td></tr>";
          html_s += "<tr><td align='center'>";
            html_s += "<br><br><a class='btn' href='/modif_PWM' target='_self'>Modif. vitesse moteur</a>  ";
          html_s += "</td></tr></table>";
          html_s += "</body></html>";
          return html_s;
        }
        
        void printTemperature(DeviceAddress deviceAddress)
        {
          float tempC = sensors.getTempC(deviceAddress);
          if (tempC == DEVICE_DISCONNECTED_C)
          {
            Serial.println("Capteur en erreur");
            return;
          }
          Serial.print(" => ");
          Serial.print(tempC);
          Serial.println("°");
        }
        
        void memo_mesure(){
          uint8_t  vjour, vheure, vminute, vseconde;
          // décallage vers le bas de tout le tableau
          for(int vadr = MAXTABTEMP - 2; vadr >= 0; vadr--){
            tgr_temp[vadr+1].v_j = tgr_temp[vadr].v_j;
            tgr_temp[vadr+1].v_h = tgr_temp[vadr].v_h;
            tgr_temp[vadr+1].v_m = tgr_temp[vadr].v_m;
            tgr_temp[vadr+1].v_s = tgr_temp[vadr].v_s;
            tgr_temp[vadr+1].v_t1 = tgr_temp[vadr].v_t1;
            tgr_temp[vadr+1].v_t2 = tgr_temp[vadr].v_t2;
            tgr_temp[vadr+1].v_t3 = tgr_temp[vadr].v_t3;
            tgr_temp[vadr+1].v_t4 = tgr_temp[vadr].v_t4;
            tgr_temp[vadr+1].v_t5 = tgr_temp[vadr].v_t5;
            tgr_temp[vadr+1].v_t6 = tgr_temp[vadr].v_t6;
            tgr_temp[vadr+1].v_t7 = tgr_temp[vadr].v_t7;
            tgr_temp[vadr+1].v_t8 = tgr_temp[vadr].v_t8;
          }
          // Memo derniere valeur
          vjour = timeinfo.day(); 
          vheure = timeinfo.hour();
          vminute = timeinfo.minute();
          vseconde = timeinfo.second();
          tgr_temp[0].v_j = vjour;
          tgr_temp[0].v_h = vheure;
          tgr_temp[0].v_m = vminute;
          tgr_temp[0].v_s = vseconde;
          tgr_temp[0].v_t1 = val_mesure[0];
          tgr_temp[0].v_t2 = val_mesure[1];
          tgr_temp[0].v_t3 = val_mesure[2];
          tgr_temp[0].v_t4 = val_mesure[3];
          tgr_temp[0].v_t5 = val_mesure[4];
          tgr_temp[0].v_t6 = val_mesure[5];
          tgr_temp[0].v_t7 = val_mesure[6];
          tgr_temp[0].v_t8 = val_mesure[7];
        }
        
        void modif_param_post() {
          if (server.hasArg("datetime")) {
              String dateTime = server.arg("datetime"); // Récupère la valeur du formulaire
              int year, month, day, hour, minute;
              sscanf(dateTime.c_str(), "%d-%d-%dT%d:%d", &year, &month, &day, &hour, &minute);
              // Met à jour l'horloge RTC
              rtc.adjust(DateTime((uint16_t)year, (uint8_t)month, (uint8_t)day, (uint8_t)hour, (uint8_t)minute, 0));
              timeinfo = rtc.now();
        
          }
          server.send(200, "text/html", handleRoot());
        }
        
        void setDelaiMesure() {
          if (server.hasArg("v_d_mesure")) {
              int v_d_mesure = server.arg("v_d_mesure").toInt(); // Récupère la valeur du formulaire
              nb_sec_inter_mesure = v_d_mesure;
              v_ram.delai_inter_mesure = nb_sec_inter_mesure;
              temp_t = nb_sec_inter_mesure * 1000;
              ecriture_eeprom();
          }
          server.send(200, "text/html", handleRoot());
        }
        
        void inc_PWM() {
          if(val_PWM<255){
            val_PWM += 10;
            if(val_PWM>255) val_PWM = 255;
            analogWrite(PinPWM, val_PWM);
          }
          server.send(200, "text/html", modif_PWM());
        }
        
        void dec_PWM() {
          if(val_PWM>1){
            val_PWM -= 10;
            if(val_PWM<1) val_PWM = 1;
            analogWrite(PinPWM, val_PWM);
          }
          server.send(200, "text/html", modif_PWM());
        }
        
        
        void setNomCapt() {
          String v_nom;
          if(server.hasArg("nom_capt_0")) {v_nom = server.arg("nom_capt_0"); v_nom.toCharArray(v_ram.nom_s[0], v_nom.length()+1);}
          if(server.hasArg("nom_capt_1")) {v_nom = server.arg("nom_capt_1"); v_nom.toCharArray(v_ram.nom_s[1], v_nom.length()+1);}
          if(server.hasArg("nom_capt_2")) {v_nom = server.arg("nom_capt_2"); v_nom.toCharArray(v_ram.nom_s[2], v_nom.length()+1);}
          if(server.hasArg("nom_capt_3")) {v_nom = server.arg("nom_capt_3"); v_nom.toCharArray(v_ram.nom_s[3], v_nom.length()+1);}
          if(server.hasArg("nom_capt_4")) {v_nom = server.arg("nom_capt_4"); v_nom.toCharArray(v_ram.nom_s[4], v_nom.length()+1);}
          if(server.hasArg("nom_capt_5")) {v_nom = server.arg("nom_capt_5"); v_nom.toCharArray(v_ram.nom_s[5], v_nom.length()+1);}
          if(server.hasArg("nom_capt_6")) {v_nom = server.arg("nom_capt_6"); v_nom.toCharArray(v_ram.nom_s[6], v_nom.length()+1);}
          if(server.hasArg("nom_capt_7")) {v_nom = server.arg("nom_capt_7"); v_nom.toCharArray(v_ram.nom_s[7], v_nom.length()+1);}
          ecriture_eeprom();
          server.send(200, "text/html", handleRoot());
        }
        
        void setup() {
            Serial.begin(115200);
            #ifndef ESP8266
              while (!Serial);
            #endif
            delay(500);
            if (! rtc.begin()) {
              Serial.println("On ne trouve pas le circuit Horloge RTC");
              capteur_HS = true;
              delay(2000);
              if (! rtc.begin()) {
                Serial.println("On ne trouve toujours pas le circuit Horloge RTC");
                capteur_HS = true;
                delay(1000);
              }else{
                capteur_HS = false;
              }
            }
            Serial.println("Circuit Horloge RTC OK");
            lecture_eeprom();
            delay(1000);
            analogWriteFreq(2000); // Frequence de PWM à 2 Khz
            analogWrite(PinPWM, 255); // Moteur lancé à fond
            //rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // Date et heure compilation mettre en comment
            Wire.begin();
            WiFi.softAP(ap_ssid, ap_password); // ESP8266 en mode Point d'Accès
            Serial.println("Point d'Accès démarré");
            Serial.print("Adresse IP: ");
            Serial.println(WiFi.softAPIP()); // L'adresse IP sera généralement 192.168.4.1
            // Configurer les routes du serveur
            server.on ( "/",  [](){ server.send(200, "text/html", handleRoot()); });
            server.on ( "/dh",  [](){ server.send(200, "text/html", htmldateheure()); });
            server.on ( "/page_valeurs",  [](){ server.send(200, "text/html", page_valeurs()); });
            server.on ( "/capteur",  [](){ server.send(200, "text/html", page_capteurs()); });
            server.on ( "/page_graph",  [](){ server.send(200, "text/html", page_graph()); });
            server.on ( "/modif_delai_m",  [](){ server.send(200, "text/html", modif_delai_m()); });
            server.on ( "/modif_PWM",  [](){ server.send(200, "text/html", modif_PWM()); });
            server.on ( "/aff_eeprom",  [](){ server.send(200, "text/html", aff_eeprom()); });
            server.on("/setDateTime", modif_param_post);
            server.on("/setNomCapt", setNomCapt);
            server.on("/setDelaiMesure", setDelaiMesure);
            server.on("/inc_vit", inc_PWM);
            server.on("/dec_vit", dec_PWM);
            server.on("/init_tab_val", vinit_tab_val);
            server.onNotFound(page_inexistante);
            server.begin(); // Démarrer le serveur web
            Serial.println("Serveur démarré");
            sensors.begin();
            nb_capteur = sensors.getDeviceCount();
            Serial.print("Locating devices...");
            Serial.print("Found ");
            Serial.print(nb_capteur, DEC);
            Serial.println(" devices.");
            Serial.print("Parasite power is: ");
            if (sensors.isParasitePowerMode()) Serial.println("ON");
            else Serial.println("OFF");
            for (int i = 0; i < nb_capteur; i++)
            {
              if (sensors.getAddress(tempDeviceAddress, i))
              {
                Serial.print("Capteur ");
                Serial.print(i, DEC);
                tb_capteurs[i].v_n = i;
                Serial.print(" adresse : ");
                printAddress(tempDeviceAddress);
                for (uint8_t n = 0; n < 8; n++){ tb_capteurs[i].v_ad[n] = tempDeviceAddress[n]; }
                Serial.println();
                Serial.print("Resolution ");
                Serial.println(TEMPERATURE_PRECISION, DEC);
                sensors.setResolution(tempDeviceAddress, TEMPERATURE_PRECISION);
                Serial.print("Resolution actually set to: ");
                Serial.print(sensors.getResolution(tempDeviceAddress), DEC);
                Serial.println();
              } else {
                Serial.print("Capteur ");
                Serial.print(i, DEC);
                Serial.print(" pas d'adresse detectee.");
              }
            }
            String v_defaut = "Sonde_x"; bool v_init = false;
            for(uint8_t x=0; x<8; x++){ if(v_ram.nom_s[x][0]=='0'){ v_defaut.toCharArray(v_ram.nom_s[x], v_defaut.length()+1); v_init=true; }}
            if(v_init) ecriture_eeprom();
            tb_capteurs[0].v_coul = "Orange";    
            tb_capteurs[1].v_coul = "Yellow";    
            tb_capteurs[2].v_coul = "Green";    
            tb_capteurs[3].v_coul = "Purple";    
            tb_capteurs[4].v_coul = "Pink";    
            tb_capteurs[5].v_coul = "Blue";    
            tb_capteurs[6].v_coul = "Magenta";    
            tb_capteurs[7].v_coul = "Cyan";    
            timeinfo = rtc.now();
            temps_qui_passe = millis();
            nb_sec_inter_mesure = v_ram.delai_inter_mesure;
            temp_t = nb_sec_inter_mesure*1000;
            vinit_tab_val();
        }
        
        void loop() {
            for(int x = 0; x < 8; x++){ val_mesure[x] = 0; }
            timeinfo = rtc.now();
            if((millis() - temps_qui_passe) >= temp_t){
                sensors.requestTemperatures();
                for (int i = 0; i < nb_capteur; i++){
                  if (sensors.getAddress(tempDeviceAddress, i)){
                    val_mesure[i] = sensors.getTempC(tempDeviceAddress);
                    //printTemperature(tempDeviceAddress); 
                  }
                }
                memo_mesure();
                temps_qui_passe = millis();
              }
            server.handleClient(); // Gérer les requêtes HTTP
        }
        
    



Une petite video pour montrer les difficultés rencontrées avec le premier convertisseur PWM vers tension 0 à 10v.



accueil electronique

Bricolage Robotique Informatique Peinture Voyage
Téléc. portail Le robot "mécano" Astuces informatique Henri Bertrou Auvergne
Bat. Iphone 6S Le robot "solaire" Réseau couche app. Jean-Michel Castille Floride
Robot piscine Servo et IR" Réseau Les couches New York
Xiaomi M365 Le robot "thymio" Réseaux Outils L'Ouest américain
Mac Mini Le robot "Rovio" Unités grandeur inf. L'Ile Maurice
Putty SSH Windows L'Italie / Venise
Bases Raspberry Tunisie
Termius IPhone/IPad Grece
Le vieux ZX 81
...
Navigation La Rochelle CNC / Imp3D Electronique Programmation
Rencontre dauphins Les Minimes Construction CNC Alim. TPL 5110 Doc. programme
Analyse NMEA 0183 Le Vieux port CNC du commerce Carte ESP8266 Indent programme
graph. NMEA 0183 L'Ile de Ré Martyr CNC ESP8266 1 relai Prog. objet
Analyse trames AIS A visiter Réa. imp. 3D ESP8266 Alarme Prog. procédurale
Analyse AIS TCP-IP Cura impression 3D ESP8266 MQTT
Sortie en ketch Plateau CR10 ESP8266 Temp.
Echange GPS C80 Anémomètre.
HP Sun-Odyssey CNC / 3D en vrac MCP9808 Librairie
LCD yanmar Saisie Oled
Testeur nmea esp1 i2c