Projets Fablab Staff

Système d'arrosage automatique

Système d'arrosage automatique

Rainmaker v1.5

TLDR; L'intégralité des fichiers sources sont téléchargeables en PJ de cette page sous forme d'archive ZIP.

Ce projet est la suite de celui commencé par les emplois-étudiant. Il avait presque abouti mais le circuit consommait trop de courant et l'autonomie de la batterie prévue n'était pas suffisante.

La première chose à faire était donc de vérifier la consommation de courant du circuit au repos et lorsque la pompe était activée. En faisant cela on pouvait constater que le circuit consommait environ 30mA au repos, ce qui est beaucoup trop. La batterie de 9V prévue étant capable de fournir ~650mAh. La batterie se vide ainsi en moins de 24h.

Première piste de solution : mettre le microcontrôleur en veille (sleep) pour économiser de l'énergie. Lorsqu'il se met en veille, le microcontrôleur éteint tous les systèmes non-essentiels et ne garde que le timer d'actif (ou une entrée pour un bouton en fonction du besoin) pour pouvoir se réveiller.

La bibliothèque Arduino LowPower n'étant pas compatible avec le microcontrôleur utilisé (ATTiny412), il a fallu se tourner vers une programmation directe des registres. Le projet Sleeping Lighthouse est un bon exemple pour notre besoin et nous allons reprendre ce code pour tester la consommation de courant. L'auteur du projet a mesuré une consommation d'à peine quelques µA.

Dans notre cas cela n'a malheureusement pas fonctionné... on était toujours à une consommation de l'ordre de 30mA.

Deuxième piste de solution : supprimer le régulateur de tension. Le régulateur de tension consomme une certaine quantité d'énergie pour passer des 9V de la batterie aux 5V nécessaires pour le microcontrôleur. Faisons un essai en le supprimant et en alimentant le circuit directement avec une alimentation externe.

On mesure désormais une consommation de 20mA... c'est mieux mais pas suffisant !

Troisième piste de solution : il n'y a pas de court-circuits mais il y a peut-être une fuite de courant en raison de mauvaises soudures. On va donc essayer avec un autre circuit avec le même microcontrôleur.

La consommation mesurée est de 300µA. On tient notre solution : usiner un nouveau circuit et le souder au four pour que les soudures soient parfaites, ne plus utiliser de batterie 9V avec un régulateur de tension mais une batterie CR2032 de 3V. C'est contre-intuitif puisque ces petites batteries ont une capacité plus faible de 250mAh, mais l'économie d'énergie réalisée en n'utilisant pas de régulateur de tension est bien plus importante.

Calcul de la consommation

On a une capacité de batterie en mAh, un circuit au repos qui consomme normalement 0.3mA et des pics de consommation de 150mA pour la pompe qui durent une trentaine de secondes une fois tous les 2 jours environ.

Cette réponse a été générée par l'IA Gemini avec le prompt suivant : "I want to estimate the longevity of my battery for a circuit i made. My battery has 240mAh of capacity, my circuit consumes 0.4mA of current most of the time and once every 2 days it consumes 150mA for 30 seconds. How can I do the math?"

  1. Convertir la durée d'impulsion en heures : La durée d'impulsion est donnée en secondes (30 secondes) mais on a besoin qu'elle soit en heures pour correspondre à la consommation continue (mesurée en milliAmpères-heures). Il y a 3600 secondes dans une heure, donc :

durée_impulsion_heures = durée_impulsion_secondes / 3600 = 30 secondes / 3600 = 0,0083 heures

  1. Calculer la consommation moyenne par impulsion par jour : Le circuit consomme 150 mA pendant 30 secondes tous les 2 jours. Pour trouver la consommation moyenne par jour, divisez la consommation totale par impulsion (courant x durée) par le nombre de jours par cycle d'impulsion :


consommation_moyenne_impulsion_par_jour = courant_impulsion * durée_impulsion_heures / nombre_de_jours_par_cycle_impulsion
consommation_moyenne_impulsion_par_jour = 150 mA * 0,0083 heures / 2 jours
consommation_moyenne_impulsion_par_jour = 0,6225 mAh/jour

  1. Calculer la consommation totale par jour : Additionnez la consommation continue et la consommation moyenne par impulsion par jour pour trouver la consommation quotidienne totale :


consommation_totale_par_jour = consommation_continue + consommation_moyenne_impulsion_par_jour
consommation_totale_par_jour = 0,4 mA/jour + 0,6225 mAh/jour
consommation_totale_par_jour = 1,0225 mAh/jour

  1. Estimer la durée de vie de la batterie: Divisez la capacité de la batterie (240 mAh) par la consommation quotidienne totale (1,0225 mAh/jour) pour obtenir l'autonomie estimée de la batterie en jours :


autonomie_batterie = capacité_batterie / consommation_totale_par_jour
autonomie_batterie = 240 mAh / 1,0225 mAh/jour
autonomie_batterie ≈ 23,4 jours

D'après ces calculs, la durée de vie de notre batterie peut être estimée à environ 23 jours.

Après réalisation du nouveau circuit on tombe même à une consommation de 0.1µA ! Ce qui allonge la durée de vie de notre batterie à 255 jours !

Le circuit

Schéma

Schematic.png

Le circuit est très simple : on a le capteur d'humidité résistif qui va renvoyer une tension analogique et une broche connectée à la pompe via un mosfet qui sert donc d'interrupteur.

Routage

On obtient ensuite un circuit simple face simple :

Layout.png

On peut noter que pour simplifier le circuit et s'économiser une résistance 0Ω, on alimente le capteur d'humidité directement par une broche du microcontrôleur. Comme le capteur est grosso-modo un simple pont diviseur de tension, il ne consomme que très peu de courant et cela ne pose aucun problème. Il ne faut juste pas oublier de l'activer dans le programme.

3D.png

Réalisation

Le PCB a ensuite été usiné sur la graveuse laser LPKF.

pcb-fresh.jpeg

Pour avoir une soudure parfaite il a été aussi nécessaire de graver un stencil en laiton pour pouvoir appliquer la pâte à braser.

soudure.jpeg

Le boîtier

Modélisation

Le boîtier a été modélisé dans Fusion360 en partant du modèle 3D du circuit exporté de KiCAD. Le PCB est fixé sur le boîtier à l'aide de 2 vis M3 et de 2 trous de fixation.

boitier3D.png

Le boîtier est refermé par un couvercle dévissable. Faire des vis dans Fusion est très facile, mais il y a des choses à respecter pour qu'elles s'impriment correctement. Pour cela, j'ai suivi le tutoriel suivant : 

boitier-coupe.png

Impression 3D

Le boîtier a été imprimé sur la Prusa MK4 et l'impression préparée sur Orca Slicer :

Orca.png

Seul le couvercle a besoin de quelques supports pour être imprimé correctement, j'ai donc configuré chaque objet individuellement (cf capture). Le reste des paramètres est classique : 0.28mm de hauteur de couche et 15% de remplissage.

Slice.png

Ce qui nous donne une durée d'impression de 1h09 pour 32g de PLA Prusament Galaxy Black.

[photo]

Découpe laser

La découpe a été réalisée sur la Trotec Speedy 360 en PMMA transparent 3mm.

Firmware

La programmation du microcontrôleur a été faite en passant directement par les registres pour la mise en veille et également pour la lecture du convertisseur digital analogique (ADC).

On injecte le programme dans l'ATTiny412 à l'aide d'un programmeur UPDI et du connecteur UPDI prévu sur le circuit.

[photo]

#include <avr/sleep.h>
#define  F_CPU  4000000 // 4 MHz

#define SLEEP     255       // sleep for 255 cycles, so 2mn
#define INTERVAL  720       // 1440mn in 24h, sleep is 2mn long, so 720 intervals
#define WATER     45000     // watering duration 45 seconds

volatile uint8_t rtcIntSemaphore;  // flag from RTS interrupt that may be used by polPUMP function
uint16_t counter = 0;

void setup() {
  initSerialGPIO();     // initialize serial and GPIO

  init32kOscRTCPIT();   // init the 32K internal Osc and RTC-PIT for interrupts
  initSleepMode();      // set up the sleep mode
}

void loop() {
  while(1) {
    counter += 1;

    if( counter > INTERVAL ){
      counter = 0;

      ADC0_init();
      int humidity = ADC0_read();

      if( humidity < 50 ){ // 0 means dry and wet is above 600
        pumpOn(); // turn on PUMP
        delay(WATER);
        pumpOff(); // turn off PUMP
      }
    }

    sleepNCycles(SLEEP); // cycles are about 500ms each / max 255 cycles so 2mn
  }
}

void ADC0_init(void) {
  // Disable digital input buffer
  PORTA.PIN2CTRL &= ~PORT_ISC_gm;
  PORTA.PIN2CTRL |= PORT_ISC_INPUT_DISABLE_gc;

  // Disable pull-up resistor 
  PORTA.PIN2CTRL &= ~PORT_PULLUPEN_bm; 

  ADC0.CTRLC =  ADC_PRESC_DIV4_gc       // CLK_PER divided by 4 
              | ADC_REFSEL_VDDREF_gc;   // VDD reference 
  ADC0.CTRLA =  ADC_RESSEL_10BIT_gc     // 10-bit mode 
              | ADC_ENABLE_bm;          // ADC Enable: enabled 
  ADC0.MUXPOS = ADC_MUXPOS_AIN2_gc;     // Select ADC channel 
}

uint16_t ADC0_read(void) {
  ADC0.COMMAND = ADC_STCONV_bm;                 // Start ADC conversion

  while ( !(ADC0.INTFLAGS & ADC_RESRDY_bm) );   // Wait until ADC conversion done 
  ADC0.INTFLAGS = ADC_RESRDY_bm;                // Clear the interrupt flag by writing 1

  return ADC0.RES;
}


//////////////////////////////////////////////////////////////////
//  ISR(RTC_PIT_vect)
//
//  Interrupt Service Routine for the RTC PIT interrupt
//
ISR(RTC_PIT_vect) {
  RTC.PITINTFLAGS = RTC_PI_bm;  // clear the interrupt flag   
  rtcIntSemaphore = 1;          // mark to PUMP function that interrupt has occurred

}


//////////////////////////////////////////////////////////////////
//  initSerialGPIO()
//
//  initialize all needed GPIO
//
void initSerialGPIO(void) {
  PORTA.DIRSET = PIN1_bm; //  set port to output for PUMP
  PORTA.OUTCLR = PIN1_bm; // turn off PUMP

  PORTA.DIRSET = PIN3_bm; //  set port to output for SENSOR
  PORTA.OUTSET = PIN3_bm; // turn on SENSOR
}


//////////////////////////////////////////////////////////////////
//  init32kOscRTCPIT()
//
//  initialize the internal ultra low power 32 kHz osc and periodic Interrupt timer
//
//  these two peripherals are interconnected in that the internal 32 kHz
//  osc will not start until a peripheral (PIT in this case) calls for it,
//  and the PIT interrupt should not be enabPUMP unit it is confirmed that
//  the 32 kHz osc is running and stable
//
//  Note that there is ERRATA on the RTC counter,  see:
//  https://ww1.microchip.com/downloads/en/DeviceDoc/ATtiny212-214-412-414-416-SilConErrataClarif-DS80000933A.pdf
//
//  That document currently states "Any write to the RTC.CTRLA register resets the 15-bit prescaler
//  resulting in a longer period on the current count or period".  So if you load the prescaler
//  value and then later enable the RTC (both on the same register) you get a very long
//  (max?) time period.  My solution to this problem is to always enable the RTC in the same
//  write that sets up the prescaler.  That seems to be an effective work-around. 
//
void init32kOscRTCPIT(void) {
  _PROTECTED_WRITE(CLKCTRL.OSC32KCTRLA, CLKCTRL_RUNSTDBY_bm);   // enable internal 32K osc   
  RTC.CLKSEL = RTC_CLKSEL_INT1K_gc;                             // Select 1.024 kHz from 32KHz Low Power Oscillator (OSCULP32K) as clock source
  RTC.PITCTRLA = RTC_PERIOD_CYC512_gc | RTC_PITEN_bm;           // Enable RTC-PIT with divisor of 512 (~500 milliseconds)
  while (!(CLKCTRL.MCLKSTATUS & CLKCTRL_OSC32KS_bm));           // wait until 32k osc clock has stabilized
  while (RTC.PITSTATUS > 0);                                    // wait for RTC.PITCTRLA synchronization to be achieved
  RTC.PITINTCTRL = RTC_PI_bm;                                   // allow interrupts from the PIT device  
}

//////////////////////////////////////////////////////////////////
//  initSleepMode()
//
//  this doesn't invoke sleep, it just sets up the type of sleep mode
//  to enter when the MCU is put to sleep by calling "sleep_cpu()"
//
void initSleepMode(void) {
  SLPCTRL.CTRLA = SLPCTRL_SMODE_PDOWN_gc;   // set sleep mode to "power down"
  SLPCTRL.CTRLA |= SLPCTRL_SEN_bm;          // enable sleep mode
}


//////////////////////////////////////////////////////////////////
//  sleepNCycles(uint16_t val)
//
//  cause system to go to sleep for N cycles, where each cycle
//  is about 500mS.  Note that processor does wakeup every 500 ms
//  but goes back to sleep almost immediately if it has not yet
//  done the desired number of cycles.
//
//  At 4 MHz MCU speed, the awake duration of each iternation in the
//  for loop below is approximately 1.5 microseconds.
//
void sleepNCycles(uint8_t val) {
  // first cycle may not be full cycle
  disableAllPeripherals(); // all off

  for (uint8_t i = 0; i < val ; i++) {
    sleep_cpu();  // put MCU to sleep
  }
  // now awake, sleep cycles complete, continue on
  initSerialGPIO(); // initialize serial and GPIO 
}


//////////////////////////////////////////////////////////////////
//  disableAllPeripherals()
//
//  To achieve minimum current consumption during sleep, it's best to
//  disable everything that might draw current during sleep.
//  This function disables everything except the RTC-PIT and 32K
//  internal low power oscillator.  This function does not disable
//  the counter used by the Arduino framework to implement millis(),
//  although the clock that drives that counter is suspended during sleep.
//
void disableAllPeripherals(void) {
  PORTA.DIRCLR = PIN0_bm; //  set port A0 to input
  PORTA.DIRCLR = PIN1_bm; //  set port A1 to input
  PORTA.DIRCLR = PIN2_bm; //  set port A2 to input
  PORTA.DIRCLR = PIN3_bm; //  set port A3 to input
  PORTA.DIRCLR = PIN6_bm; //  set port A4 to input
  PORTA.DIRCLR = PIN7_bm; //  set port A5 to input        
        
  PORTA.PIN0CTRL = PORT_ISC_INPUT_DISABLE_gc; // disable input buffers
  PORTA.PIN1CTRL = PORT_ISC_INPUT_DISABLE_gc;
  PORTA.PIN2CTRL = PORT_ISC_INPUT_DISABLE_gc;
  PORTA.PIN3CTRL = PORT_ISC_INPUT_DISABLE_gc;
  PORTA.PIN6CTRL = PORT_ISC_INPUT_DISABLE_gc;
  PORTA.PIN7CTRL = PORT_ISC_INPUT_DISABLE_gc;
}

void pumpOn(void)     {PORTA.OUTSET = PIN1_bm;} // turn on active high PUMP on PA1
void pumpOff(void)    {PORTA.OUTCLR = PIN1_bm;} // turn off active high PUMP on PA1

Assemblage du système

Montage des inserts

Les inserts filetés s'insèrent dans une impression 3D à l'aide d'un fer à souder. Il faut prévoir un trou de dimensions légèrement plus petit (-0.2mm) que l'insert lui-même de manière à bien accrocher le plastique quand il va fondre.

inserts.jpeg

Branchements

[photo]

Montage final

[photo]

Système d'arrosage automatique

Rainmaker v2

Trophée concours innovation

Objectif : créer un trophée pour le concours innovation organisé par le fablab. L'idée est de faire quelque chose de simple et sobre avec du PMMA et un socle en bois de chêne.

Le logo a été réalisé à l'aide de ChatGPT et du GPT "Logo Creator". Le prompt était très simple : réalise un logo simple en noir et blanc pour un concours d'innovation en utilisant une ampoule comme inspiration.

Le GPT demande quel niveau de complexité sur une échelle de 1 à 10 à laquelle j'ai répondu 2, et la palette de couleurs, soit noir et blanc. L'image produite était satisfaisante dès le premier coup :

image.png

Je lui ai ensuite demandé de faire un rendu 3D pour voir à quoi cela ressemblerait :

image.png

Je lui ai ensuite demandé de modifier légèrement la première proposition pour qu'elle ressemble plus au rendu 3D (fond noir au niveau de la coupe) et de rajouter le contour du logo.

image.png

A partir de là il a suffit de reprendre le PNG dans Inkscape, de le vectoriser et de rajouter le logo du Fablab. Le texte a été écrit avec la police "Good Timing" trouvée sur Dafont. 

Fichier SVG : trophee.svgCleanShot 2025-06-02 at 11.43.37@2x.png

Fabrication

Le fichier SVG a ensuite été découpé et gravé dans du PMMA 6mm avec une largeur de 12cm.

Le socle a été découpé dans du bois de chêne 20mm à la shopbot. Cercle de diamètre 12cm avec une rainure de 6mm. Le dessin a été réalisé directement dans VCarve.

Il aurait été mieux de faire un chanfrein directement avec la fraiseuse, c'est un oubli.

Pour finir : ponçage au papier de verre jusqu'à 240, puis collage à la colle chaude.

image.png

Circuit de Noël

Siphon toilettes SU

Réalisation d'un siphon de rechange pour les évacuations au sol des toilettes de Sorbonne.

Modélisation réalisée sur Fusion 360.

CleanShot 2025-12-12 at 11.20.55@2x.pngCleanShot 2025-12-12 at 11.20.37@2x.png

CleanShot 2025-12-12 at 11.18.37@2x.png

Imprimé en PETG sur la Prusa MK4, avec 80% de remplissage et 5 coques pour une étanchéité parfaite.

PXL_20251211_140407430.jpgPXL_20251211_140307311.jpgPXL_20251211_140322087.jpg

Live Data Logger

Ce projet permet de visualiser en temps réel les données environnementales (Température, Humidité, Pression par exemple) et le niveau de batterie d'un montage ESP32, directement dans un navigateur web, sans passer par un serveur cloud ni une base de données externe.

CleanShot 2026-01-12 at 18.12.52@2x.png

🌟 Fonctionnalités


🛠️ Matériel Requis

Câblage (I2C)

Les deux capteurs utilisent le bus I2C. Ils peuvent être connectés en parallèle sur les mêmes broches de l'ESP32.

Capteur (Pin) ESP32 (Pin)
VCC 3.3V
GND GND
SDA GPIO 21 (D21)
SCL GPIO 22 (D22)

Note : Vérifiez l'adresse I2C du BME280. Le code cherche par défaut l'adresse 0x77 ou 0x76.


💻 Installation Logicielle

1. Bibliothèques Arduino

Installez les bibliothèques suivantes via le Gestionnaire de bibliothèques de l'IDE Arduino :

2. Bibliothèques Web (ESP32)

Ces bibliothèques doivent souvent être installées manuellement (téléchargement ZIP) si elles ne sont pas dans le gestionnaire :


📂 Le Code Source

Le projet se compose de deux fichiers à placer dans le même dossier de sketch Arduino.

1. Fichier webpage.h

Ce fichier contient l'interface HTML, le CSS (style) et le JavaScript (logique client).

Créez un nouvel onglet dans l'IDE Arduino nommé webpage.h et collez ceci :

C++

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <title>ESP32 Multi-Stream</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  <style>
    body { 
      background-color: #0b0c10; color: #66fcf1; 
      font-family: 'Courier New', Courier, monospace; 
      text-align: center; margin: 0; padding: 10px;
    }
    .header-container { display: flex; justify-content: space-between; align-items: center; padding: 0 10px; margin-bottom: 10px; }
    h2 { margin: 0; text-transform: uppercase; letter-spacing: 2px; text-shadow: 0 0 10px rgba(102, 252, 241, 0.5); font-size: 1.2rem; }
    .battery-wrapper { display: flex; align-items: center; gap: 8px; }
    .battery-icon { width: 40px; height: 20px; border: 2px solid #66fcf1; border-radius: 4px; position: relative; padding: 2px; }
    .battery-icon::after { content: ''; position: absolute; right: -5px; top: 4px; width: 3px; height: 8px; background: #66fcf1; border-radius: 0 2px 2px 0; }
    .battery-level { height: 100%; width: 100%; background: #66fcf1; border-radius: 2px; transition: width 0.5s, background-color 0.5s; }
    #bat-text { font-weight: bold; font-size: 0.9rem; }
    .card { background: #1f2833; border: 1px solid #45a29e; box-shadow: 0 0 20px rgba(69, 162, 158, 0.2); border-radius: 8px; padding: 10px; margin: 15px auto; height: 60vh; width: 95vw; }
    #chart-container { position: relative; height: 100%; width: 100%; }
    #values-panel { display: flex; flex-wrap: wrap; justify-content: center; gap: 15px; margin: 20px auto; }
    .val-box { border: 1px solid #45a29e; border-radius: 5px; padding: 10px 20px; min-width: 100px; background: #0b0c10; }
    .val-label { font-size: 0.7em; color: #c5c6c7; display: block; }
    .val-number { font-size: 1.4em; font-weight: bold; }
    button { background: transparent; border: 1px solid #66fcf1; color: #66fcf1; padding: 10px 20px; font-size: 14px; margin: 10px; cursor: pointer; text-transform: uppercase; transition: 0.3s; }
    button:hover { background: #66fcf1; color: #0b0c10; box-shadow: 0 0 15px #66fcf1; }
    button:disabled { border-color: #444; color: #444; cursor: not-allowed; box-shadow: none; }
    #btn-stop:hover { background: #ff0055; border-color: #ff0055; box-shadow: 0 0 15px #ff0055; color: white; }
  </style>
</head>
<body>
  <div class="header-container">
    <h2>Monitoring</h2>
    <div class="battery-wrapper">
      <span id="bat-text">--%</span>
      <div class="battery-icon"><div id="bat-level" class="battery-level"></div></div>
    </div>
  </div>
  <div id="values-panel"><span style="color:#888">Connexion...</span></div>
  <div class="card"><div id="chart-container"><canvas id="sensorChart"></canvas></div></div>
  <div class="controls">
    <button id="btn-stop" onclick="stopLogging()">STOP</button>
    <button id="btn-save" onclick="downloadData()" disabled>CSV</button>
  </div>
<script>
  var gateway = `ws://${window.location.hostname}/ws`;
  var websocket;
  var chart;
  var dataLog = []; 
  var isLogging = true;
  var sensorConfig = [];

  function initChart() {
    var ctx = document.getElementById('sensorChart').getContext('2d');
    chart = new Chart(ctx, {
        type: 'line',
        data: { labels: [], datasets: [] },
        options: { 
          animation: false, responsive: true, maintainAspectRatio: false,
          interaction: { mode: 'index', intersect: false },
          scales: { x: { ticks: { color: '#888' }, grid: { color: '#333' } }, y: { ticks: { color: '#66fcf1' }, grid: { color: '#333' } } },
          plugins: { legend: { labels: { color: '#fff' } } }
        }
    });
  }

  function initWebSocket() {
    websocket = new WebSocket(gateway);
    websocket.onopen = () => console.log('WS Connecté');
    websocket.onclose = () => setTimeout(initWebSocket, 2000);
    websocket.onmessage = onMessage;
  }

  function onMessage(event) {
    if (!isLogging) return;
    var msg = event.data;
    if (msg.indexOf("config") > 0) { setupInterface(JSON.parse(msg)); return; }
    try {
        var data = JSON.parse(msg);
        var t = new Date().toLocaleTimeString();
        var values = data.v; 
        var battery = data.b;
        if(battery !== undefined) {
           document.getElementById('bat-text').innerText = battery + "%";
           var bar = document.getElementById('bat-level');
           bar.style.width = battery + "%";
           if(battery > 50) bar.style.backgroundColor = "#66fcf1";
           else if(battery > 20) bar.style.backgroundColor = "#ffcc00";
           else bar.style.backgroundColor = "#ff3333";
        }
        values.forEach((val, index) => { var el = document.getElementById(`val-${index}`); if(el) el.innerText = val.toFixed(1); });
        dataLog.push({time: t, vals: values});
        chart.data.labels.push(t);
        chart.data.datasets.forEach((dataset, index) => { if(values[index] !== undefined) dataset.data.push(values[index]); });
        chart.update();
    } catch(e) { console.error("Erreur Parsing", e); }
  }

  function setupInterface(cfg) {
    sensorConfig = cfg.sensors;
    chart.options.scales.y.min = cfg.min;
    chart.options.scales.y.max = cfg.max;
    chart.data.datasets = sensorConfig.map(s => ({
        label: s.name, borderColor: s.color, backgroundColor: s.color,
        borderWidth: 2, pointRadius: 0, tension: 0.3, fill: false, data: []
    }));
    chart.update();
    var panel = document.getElementById("values-panel");
    panel.innerHTML = "";
    sensorConfig.forEach((s, index) => {
        panel.innerHTML += `<div class="val-box" style="border-color:${s.color}"><span class="val-label" style="color:${s.color}">${s.name}</span><span id="val-${index}" class="val-number">--</span></div>`;
    });
  }

  function stopLogging() {
    isLogging = false;
    websocket.close();
    document.getElementById('btn-stop').style.display = 'none';
    document.getElementById('btn-save').disabled = false;
  }

  function downloadData() {
    let header = "Time"; sensorConfig.forEach(s => header += "," + s.name);
    let csv = header + "\n";
    dataLog.forEach(row => { csv += row.time; row.vals.forEach(val => csv += "," + val); csv += "\n"; });
    var link = document.createElement("a"); link.href = "data:text/csv;charset=utf-8," + encodeURI(csv); link.download = "data.csv"; link.click();
  }

  window.onload = () => { initChart(); initWebSocket(); };
</script>
</body>
</html>
)rawliteral";

2. Programme Principal (.ino)

C'est le code qui gère les capteurs et le serveur. Assurez-vous de modifier le SSID et le MOT DE PASSE.

C++

// =================================
// --- INCLUDE CAPTEURS ---
// =================================
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
Adafruit_BME280 bme; // I2C

#include <Adafruit_MAX1704X.h>
Adafruit_MAX17048 maxlipo;

// =================================
// --- Include WiFi / serveur ---
// =================================
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include "webpage.h"

// =================================
// --- CONFIGURATION UTILISATEUR ---
// =================================
const char* ssid = "SSID";
const char* password = "MDP";

// Définissez ici l'échelle fixe de votre graphique
const int Y_MIN = 0;   
const int Y_MAX = 100;

// DEFINITION DES COURBES (Nom, Couleur Hexa)
struct Sensor {
  String name;
  String color;
};

Sensor sensors[] = {
  { "Temperature", "#ff3333" },  // Rouge
  { "Humidité",    "#33ff33" },  // Vert Néon
  { "Pression",    "#3388ff" }   // Bleu
};

// Calcul automatique du nombre de capteurs
const int SENSOR_COUNT = sizeof(sensors) / sizeof(sensors[0]);

// Vitesse d'envoi (ms)
const int SEND_INTERVAL = 1000;
// =================================


// INITIALISATION SERVEUR & WEBSOCKET
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");

int sensorValue = 0;

void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
  if (type == WS_EVT_CONNECT) {
    Serial.println("Client connecté ! Envoi de la config...");
    // Envoi de la configuration JSON (échelle, noms des capteurs, couleurs)
    String json = "{\"type\":\"config\",\"min\":" + String(Y_MIN) + ",\"max\":" + String(Y_MAX) + ",\"sensors\":[";
    for (int i = 0; i < SENSOR_COUNT; i++) {
      json += "{\"name\":\"" + sensors[i].name + "\",\"color\":\"" + sensors[i].color + "\"}";
      if (i < SENSOR_COUNT - 1) json += ",";
    }
    json += "]}";
    client->text(json);
  }
}

void setup() {
  Serial.begin(9600);
  
  // --- SETUP MAX17048 (Batterie) ---
  while (!maxlipo.begin()) {
    Serial.println(F("Couldnt find Adafruit MAX17048?\nMake sure a battery is plugged in!"));
    delay(2000);
  }

  // --- SETUP BME280 (Environnement) ---
  unsigned status = bme.begin();  
  if (!status) {
      Serial.println("Erreur BME280 ! Vérifiez le câblage.");
      while (1) delay(10);
  }

  // Configuration en mode "Forced" pour économiser l'énergie
  bme.setSampling(Adafruit_BME280::MODE_FORCED,
                Adafruit_BME280::SAMPLING_X1, // Température
                Adafruit_BME280::SAMPLING_X1, // Pression
                Adafruit_BME280::SAMPLING_X1, // Humidité
                Adafruit_BME280::FILTER_OFF   );
  
  // --- WIFI ---
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connexion au WiFi...");
  }
  Serial.println(WiFi.localIP());

  // --- SERVEUR ---
  ws.onEvent(onEvent);
  server.addHandler(&ws);

  server.on("/favicon.ico", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(204); });
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send_P(200, "text/html", index_html); });
  
  server.begin();
}

void loop() {
  // Lecture des capteurs
  bme.takeForcedMeasurement(); // Demande une nouvelle mesure

  float readings[SENSOR_COUNT];
  readings[0] = bme.readTemperature();            // Temp
  readings[1] = bme.readHumidity();               // Hum
  readings[2] = bme.readPressure() / 100.0F;      // Pression (hPa)

  ws.cleanupClients(); 
  
  if (ws.count() > 0) {
    // Construction JSON : {"v": [val1, val2, val3], "b": 85.5}
    String json = "{\"v\":[";
    for(int i=0; i<SENSOR_COUNT; i++){
      json += String(readings[i]);
      if(i < SENSOR_COUNT -1) json += ",";
    }
    json += "], \"b\":" + String(maxlipo.cellPercent()) + "}";
    
    ws.textAll(json);
  }
  
  delay(SEND_INTERVAL);
}


🚀 Utilisation

  1. Téléversez le code sur l'ESP32.

  2. Ouvrez le Moniteur Série (Baudrate 9600) pour voir l'adresse IP (ex: 192.168.1.45).

  3. Connectez votre ordinateur ou téléphone au même réseau WiFi.

  4. Ouvrez un navigateur et tapez l'adresse IP.

  5. Le tableau de bord s'affiche : les courbes se dessinent et le niveau de batterie apparait en haut à droite.

  6. Cliquez sur STOP puis CSV pour récupérer vos données.

Conception d'une centrifugeuse à partir d'un disque dur

Objectif du projet: nous souhaitons réaliser une centrifugeuse à partir d'un disque dur et de quelques composants électroniques.

Définition: Une centrifugeuse est un appareil de laboratoire utilisé pour séparer les constituants d'un mélange (liquides ou solides) en les faisant tourner très rapidement, afin de répartir les substances selon leur densité.

Une centrifugeuse est une roue tournant à très grande vitesse. Les substances sont placées dans les porte-tubes situés sur le pourtour de la roue. Les disques durs d'ordinateur à rotation rapide, logés dans un boîtier en aluminium, sont parfaitement adaptés à la fabrication de petites centrifugeuses artisanales.

De plus ce projet rentre dans le cadre de l'économie circulaire en donnant une seconde vie à des appareils en fin de vie. 

Pour réaliser ce projet nous avons suivi les instructions données sur:  https://www.gaudi.ch/GaudiLabs/?page_id=328 

Comment?:

wiki.PNG

Pour procéder à la réalisation de cette centrifugeuse, nous commençons par démonter un disque dur afin de retirer toutes les pièces et garder uniquement son moteur. Pour remplacer sa partie électronique ce dernier sera relié par 3 fils à un contrôleur ESC qui permet de réguler la vitesse du moteur (piloter le moteur). On ajoute également un CCPM SERVO CONSISTENCY MASTER qui va offrir une interface utilisateur pour contrôler la vitesse de la centrifugeuse.

Le contrôleur de vitesse électronique (ESC) est connecté à l'alimentation 12 V à l'aide des deux gros fils (Le noir correspond à la masse (GND), le rouge au 12V).

Pour en savoir plus sur le fonctionnement d'un contrôleur avec un moteur sans broche voici une vidéo explicative assez bien détaillée : https://www.youtube.com/watch?v=OZNxbxL7cdc&t=149s

wiki 2.PNG

Les disques sont retirés et remplacés par un porte-tubes en acrylique préalablement découpé au laser (schéma sur l'image ci-dessus). Emboîter ce dernier correctement sur l'axe du moteur et le fixer à l'aide de son support d'origine.

NB:  Seuls les petits tubes à centrifuger (0,5 ml) sont compatibles avec ce type de montage.

Pour confectionner les éléments du boîtier, nous allons découper au laser (utiliser du MDF 3mm) les pièces schématisées sur l'image ci-dessus, puis faire le montage et le fixer à l'aide des vis pour assurer une bonne stabilité et faciliter l'ouverture du couvercle.