mercredi 28 octobre 2026

Trike MoskitOS et remorque à assistance électrique avec recharge de batteries par panneaux solaires.


Trike MOSKITOS
à recharge solaire : suivi de la course du soleil par des panneaux, schéma et liste du matériel.

 le code Arduino (disponible en fin de billet) a été écrit avec l’aide de Claude (Anthropic) et des collègues. J'ai choisi le matériel et effectué les différents câblages puis testé et réglé sur mon matériel.

Présentation

Deux panneaux solaires, l'un sur le trike MoskitOS et l'autre sur sa remorque, sont inclinés chacun par un vérin électrique 12 V (course 150 mm, 20 s pour la course complète, environ 3 A). Un Arduino maître suit le soleil et commande le vérin du trike. Un Arduino esclave, sur la remorque, copie la position du maître par radio.

  • Suivi du soleil : deux capteurs de lumière (LDR) comparent la lumière à gauche et à droite et le vérin s'oriente en conséquence.
  • Modes : AUTO (suivi), MANUEL, ROUTE ou À PLAT (panneaux horizontaux pour rouler) et NUIT (panneaux remis à plat quand la lumière disparaît).
  • Commande : un petit écran tactile (ESP32) ou une page web ouverte sur un smartphone Android, tous deux en Bluetooth avec le maître. Un STOP arrête les vérins immédiatement.
  • Position : elle est calculée à partir du temps de marche des vérins, sans capteur de position. À la mise sous tension, chaque vérin descend en butée basse (22 s) pour se recaler. Un recalage identique a lieu chaque nuit.


Liste du matériel

Partie

Élément

Quantité

Remarque

Maître

Arduino Uno (ATmega328P)

1

Un Nano convient aussi, mais le Uno s'est montré plus fiable ici

Maître

Driver moteur BTS7960

1

Pour le vérin du trike

Maître

Vérin linéaire 12 V, course 150 mm, fins de course intégrées

1

Environ 3 A

Maître

Capteurs de lumière LDR

2

Gauche et droite, sur A2 et A3

Maître

Joystick analogique avec bouton

1

Commande manuelle locale

Maître

Écran LCD 16x2 avec module I2C (adresse 0x27)

1

État du système

Maître

Module Bluetooth HM-10 (DSD TECH)

1

Avec diviseur de tension sur sa broche RX

Maître

Module radio nRF24L01+PA+LNA avec antenne

1

Avec adaptateur 5 V vers 3,3 V

Esclave

Arduino Uno ou Nano

1

Remorque

Esclave

Driver moteur BTS7960

1

Pour le vérin de la remorque

Esclave

Vérin linéaire 12 V, course 150 mm

1

Identique au maître

Esclave

Module radio nRF24L01+PA+LNA avec antenne

1

Avec adaptateur 5 V vers 3,3 V

Commande

Écran tactile ESP32 (type CYD)

1

Optionnel, Bluetooth avec le HM-10

Commande

Smartphone Android avec Chrome

1

Optionnel, page web trike_smartphone.html

Alimentation

Convertisseur 48 V vers 12 V

1

Alimente les deux Arduino et les drivers

Compléments

Condensateurs 100 µF / 25 V

4

Voir la section Condensateurs

Compléments

Condensateurs 100 µF / 16 V

2

Un par adaptateur radio

Compléments

Borniers pour les + 5 V et les masses, fils

selon besoin

Fils courts pour la radio


Branchements du maître (Arduino Uno)

Élément

Broche du module

Broche de l'Arduino

Driver BTS7960

         RPWM

                                           D5

Driver BTS7960

          LPWM                         

                                           D6

Driver BTS7960

          R_EN et L_EN

                                           5 V

Driver BTS7960

          VCC

                                           5 V

Driver BTS7960

          GND (commande) 

                                           GND

LDR gauche

          point milieu du diviseur

                                           A2

LDR droite

          point milieu du diviseur

                                           A3

Joystick

          axe X

                                           A0

Joystick

          axe Y

                                           A1

Joystick

          bouton

                                           D2

HM-10

          TX+

                                           D7

HM-10

          RX

                                           D8

LCD I2C

          SDA

                                           A4

LCD I2C

          SCL    

                                           A5

LCD I2C

          VCC et GND

                                           5 V et GND     

Le moteur du vérin se branche sur les sorties M+ et M− du driver. Son alimentation puissance (12 V) arrive sur B+ et B− du driver.


Branchements de l'esclave et de la radio

L'esclave ne comporte que le driver du vérin et le module radio. La radio se branche de la même façon sur le maître et sur l'esclave.

Élément

Broche du module

Broche de l'Arduino

Driver BTS7960

RPWM

                   D5

Driver BTS7960

LPWM

                   D6

Driver BTS7960

R_EN, L_EN et VCC

5 V

Driver BTS7960

GND (commande)

                   GND

Adaptateur radio

VCC (entrée)

                   5 V (jamais 3V3, jamais VIN)

Adaptateur radio

GND

                   GND

Radio nRF24

CE

                    D9

Radio nRF24

CSN

                    D10

Radio nRF24

SCK

                    D13

Radio nRF24

MOSI

                    D11

Radio nRF24

MISO

                    D12

Radio nRF24

IRQ

                    non branché

Le nRF24 ne supporte que 3,3 V : il est alimenté par l'adaptateur 5 V vers 3,3 V, qui est lui-même relié au 5 V de l'Arduino. Branché directement sur 5 V, le module est détruit. Un module PA+LNA consomme jusqu'à 115 mA à l'émission : n'utilisez pas la broche 3V3 de l'Arduino, trop faible.

L'antenne doit toujours être vissée avant la mise sous tension et placée bien droite, à l'écart des câbles moteur (au moins 10 cm). Une antenne mal placée a provoqué des pertes de paquets lors des essais.


Alimentation et masses

Un convertisseur 48 V vers 12 V alimente tout le système. Il fournit le 12 V de chaque Arduino (prise jack ou broche VIN) et de chaque driver (bornes B+ et B−). Le 5 V des modules est produit par le régulateur de chaque Arduino.

  1. Masse commune : le − du convertisseur 12 V, la borne B− de chaque driver, un GND de chaque Arduino et le GND des modules sont reliés au même point. Une masse flottante provoque du bruit sur le bus de la radio.
  2. Bornier + 5 V et bornier masse : regroupez les fils + 5 V sur un bornier et les masses sur un autre, au lieu de fils tordus ou soudés en chaîne.
  3. Fils courts pour la radio, éloignés d'au moins 10 cm des câbles du moteur.
  4. Le fil du moteur du vérin passe par la sortie du driver (M+ et M−). Il n'est pas relié à la masse commune.

Pour tester un Arduino seul sur le PC, débranchez son 12 V : ne raccordez pas l'USB et le 12 V en même temps.


Condensateurs

Les condensateurs absorbent les pointes de courant et stabilisent l'alimentation. Ils se branchent toujours en parallèle, entre le + et le − de ce qu'ils protègent, au plus près des broches. Sur un condensateur électrolytique, la patte marquée par la bande grise (−) va sur la masse : un condensateur inversé peut chauffer ou éclater.

Emplacement

Valeur

Entre

Pourquoi

Entrée 12 V de chaque Arduino

100 µF / 25 V

+ 12 V et masse

Lisse le bruit du convertisseur 48/12 V et du moteur

Entrée de puissance de chaque driver BTS7960

100 µF / 25 V

B+ et B−

Absorbe les pointes au démarrage du vérin

Adaptateur radio 5 V vers 3,3 V, côté entrée

100 µF / 16 V

VCC (5 V) et GND de l'adaptateur

Empêche la radio de décrocher à l'émission

Sur l'adaptateur, la sortie 3,3 V est dans le support du module : le condensateur se soude donc sur les broches d'entrée (VCC et GND), sous l'adaptateur, avec des pattes de 1 cm environ. Une tension nominale plus élevée que celle indiquée convient toujours.


Programmes et bibliothèques

Appareil

Programme

Rôle

Maître (Arduino Uno)

maitre_tracker_v7.ino

Suivi du soleil, vérin du trike, radio, Bluetooth, LCD

Esclave (Arduino)

esclave_tracker_v3_final.ino

Suit la position du maître, recalage de nuit

Écran tactile (ESP32)

ecran_tactile_trike.ino

Boutons et affichage en Bluetooth

Smartphone Android

trike_smartphone.html

Mêmes commandes, ouverte dans Chrome

Bibliothèques à installer dans l'IDE Arduino (menu Croquis, Inclure une bibliothèque, Gérer les bibliothèques) : RF24 (auteur TMRh20) pour la radio, LiquidCrystal I2C pour le LCD. SPI, Wire et SoftwareSerial sont déjà fournies avec l'IDE. Le driver BTS7960 se commande directement avec analogWrite, sans bibliothèque.

Pour l'écran ESP32, choisir la carte ESP32 Dev Module, le paquet esp32 d'Espressif et le schéma de partition Huge APP.

Réglages principaux, dans le programme du maître :

  • LDR_INVERSE = true si les capteurs affichent une valeur basse en pleine lumière.
  • POSITION_HORIZONTALE = 9500 : position « à plat » du trike, en millisecondes de course (environ 135 ms par millimètre).
  • RECALAGE_NUIT = true : recalage automatique en butée basse après une nuit.

Dans l'esclave, POSITION_HORIZONTALE_ESCLAVE = 10000 règle la position à plat de la remorque.


Mise en route, tests et dépannage

  1. Mettez sous tension le maître et l'esclave en même temps. Chaque vérin descend en butée basse pendant environ 22 s (le LCD du maître affiche CALIBRATION).
  2. Sur le LCD du maître, la fin de ligne indique R+ quand la liaison avec l'esclave est bonne et R− quand elle est perdue. Sur l'écran ou le smartphone, la ligne « Remorque » montre un pourcentage, ou « perdue ».
  3. Passez en MANUEL, appuyez sur MONTER quelques secondes, puis sur STOP : les deux vérins doivent suivre.

En cas de problème radio, testez les deux modules avec deux petits programmes d'essai (radio_A_emetteur.ino et radio_B_recepteur.ino) et lisez le moniteur série à 115200 bauds :

Affichage

Signification

Action

begin = 0 ou puce detectee = 0

Le module n'est pas reconnu : les « OK » qui suivent sont faux

Vérifier les 6 fils de la radio, l'alimentation de l'adaptateur et les soudures

begin = 1, puce detectee = 1, puis « PAS DE REPONSE »

Le module est bon, mais la liaison ne passe pas

Vérifier l'autre module, les antennes (bien vissées, droites) et l'adresse commune

« OK » avec un numéro qui suit celui de l'émetteur

Liaison saine

Remettre les programmes du trike

Des « PAS DE REPONSE » intermittents

Liaison instable

Écarter les modules d'au moins 3 m, vérifier les antennes et les condensateurs

Si un vérin ne bouge pas, vérifiez le driver : il doit recevoir 12 V sur B+ et B−, 5 V sur VCC, R_EN et L_EN. Un BTS7960 qui ne produit aucun mouvement alors que ces tensions sont présentes est probablement défectueux : échangez-le avec celui de l'autre voie pour le vérifier.


Conseils, limites et précautions

  • Pas encore éprouvé sur la route : le système a été testé à l'arrêt. Les premiers essais en roulant diront comment il tient les vibrations et les parasites du moteur.
  • Position calculée au temps : elle dérive peu à peu. Le recalage à chaque mise sous tension et chaque nuit la remet en phase. Les vérins ont leurs propres fins de course, qui coupent leur alimentation en butée.
  • Un seul appareil Bluetooth à la fois : le module HM-10 n'accepte qu'une connexion. Éteignez l'écran tactile avant d'utiliser la page du smartphone, et réciproquement.
  • Android uniquement pour la page du smartphone : Chrome gère le Bluetooth des pages web, mais pas Safari ni Chrome sur iPhone.
  • Driver : le BTS7960 convient aux vérins de 3 A. Un module de 3 A comme le LMD18200T serait à sa limite et obligerait à modifier les programmes. Gardez un BTS7960 de rechange.
  • Polarité : ne jamais inverser le + et le − de l'alimentation des drivers ni des condensateurs.
  • STOP : après un STOP, la remorque termine parfois son rattrapage de quelques millimètres jusqu'à la position du trike avant de s'arrêter.
  • Adaptation : les temps de course (20 s, 22 s de calibration) et les positions « à plat » doivent être mesurés et réglés pour vos propres vérins et vos propres panneaux.



 code arduino "maitre":

 // ===== MAITRE : suivi solaire + radio nRF24L01 + Bluetooth HM-10 =====
// Ajout : commande Bluetooth 'X' = arret immediat (bouton STOP de l'ecran tactile)
#include <Wire.h>
#include <SPI.h>
#include <RF24.h>
#include <SoftwareSerial.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);
RF24 radio(9, 10);                  // CE, CSN
SoftwareSerial ble(7, 8);           // RX (depuis HM-10 TX), TX (vers HM-10 RX via diviseur)
const byte ADRESSE[6] = "SOL01";

// ---------- Broches (inchangees) ----------
const int RPWM = 5;
const int LPWM = 6;
const int LDR_GAUCHE = A2;
const int LDR_DROITE = A3;
const int PIN_JOY_X  = A0;
const int PIN_JOY_Y  = A1;
const int PIN_JOY_SW = 2;

// ---------- Reglages (inchanges) ----------
const int ZONE_MORTE   = 50;
const int SEUIL_NUIT   = 150;
const int VITESSE_AUTO = 200;
const int VITESSE_MANU = 255;
const int SEUIL_MIN = 200;
const int SEUIL_MAX = 800;

// Sens du capteur de lumiere. false : valeur haute = beaucoup de lumiere.
// true : module inverse (valeur haute dans le noir) -> on retourne la mesure.
const bool LDR_INVERSE = true;
int lireLDR(int pin) {
  int v = analogRead(pin);
  return LDR_INVERSE ? 1023 - v : v;
}

const long TEMPS_AJUSTEMENT = 3000;
const unsigned long TEMPS_VEILLE = 6000;             // temps de veille de 10mn (600000)
const long TEMPS_COURSE_TOTALE = 14000;
const unsigned long TEMPS_CALIBRATION = 18000;

// ---------- Position a plat et luminosite ----------
// Position "panneau a plat" en ms depuis le zero (a regler, voir notice)
const long POSITION_HORIZONTALE = 7000;
const int  SEUIL_JOUR = SEUIL_NUIT + 50;               // reprise du suivi au-dessus de ce seuil
const unsigned long PERIODE_LUMIERE   = 30000;         // test de luminosite toutes les 30 s
const unsigned long CONFIRMATION_NUIT = 30000;        // sombre pendant 5 min (300000) avant mise a plat, pour test 30000

// Recalage de nuit : avant la mise a plat de nuit, le verin descend jusqu'a sa butee basse
// (il coupe lui-meme son alimentation) et le compteur repart de zero. false = desactive.
const bool RECALAGE_NUIT = true;

// ---------- Messages radio ----------
struct Paquet { uint8_t mode; int32_t cible; };   // mode : 0 manuel, 1 auto, 2 a plat, 3 recalage bas
struct Retour { int32_t pos; uint8_t etat; };     // etat : 1 arret, 2 en mouvement
Paquet paquet;
Retour retour = {0, 0};
bool liaisonOk = false;
bool radioOk = false;      // module nRF24 detecte au demarrage

// ---------- Etat ----------
long positionVirtuelle = 0;
bool modeAutomatique = true;
unsigned long dernierAjustementChrono = 0;
unsigned long dernierTourManuel = 0;

bool horizontal = false;      // panneaux commandes a plat
bool enNuit = false;          // a plat a cause du manque de lumiere
unsigned long debutSombre = 0, dernierTestLumiere = 0;
bool modeRoute = false;       // a plat et verrouille (pendant les trajets)

char cmdBle = 0;
int bleSens = 0;
unsigned long bleImpulsionFin = 0;
unsigned long tRadio = 0, tStatut = 0;
bool recalage = false;        // recalage sur la butee basse en cours (l'esclave descend aussi)

bool arretDemande = false;    // commande 'X' recue : arret immediat, puis passage en manuel
bool pret = false;            // vrai apres la calibration (X ignore pendant la mise a zero)

// Envoi radio avec delai maximal. radio.write() attend sans fin si le module decroche
// (alimentation instable) : tout le maitre, moteur compris, resterait fige.
bool envoiRadio() {
  retour.etat = 0;                             // 0 = aucune reponse valide de l'esclave
  radio.startFastWrite(&paquet, sizeof(paquet), false);
  if (!radio.txStandBy(60)) {                  // attend 60 ms maximum la confirmation de l'esclave
    radio.flush_tx();                          // echec ou module fige : on vide l'envoi en attente
    return false;
  }
  // La liaison n'est declaree bonne que si l'esclave a renvoye sa position (accuse avec donnees)
  if (radio.available()) radio.read(&retour, sizeof(retour));
  return (retour.etat == 1 || retour.etat == 2);
}

// ---------- Service radio + Bluetooth (appele partout) ----------
void service() {
  while (ble.available()) {
    char c = toupper(ble.read());
    if (c == 'X') {
      // ARRET immediat : coupe le moteur tout de suite, meme au milieu d'un mouvement
      if (pret) { arretMoteur(); bleImpulsionFin = 0; arretDemande = true; }
    }
    else if (c == 'A' || c == 'M' || c == 'U' || c == 'D' || c == 'S' || c == 'H' || c == 'R' || c == '?') cmdBle = c;
  }

  unsigned long now = millis();

  // Remorque presente : envoi toutes les 300 ms. Absente : simple appel toutes les 2 s,
  // le maitre fonctionne normalement sans elle et reprend des qu'elle repond.
  if (radioOk && now - tRadio >= (liaisonOk ? 300UL : 2000UL)) {
    tRadio = now;
    paquet.mode  = recalage ? 3 : (horizontal ? 2 : (modeAutomatique ? 1 : 0));
    paquet.cible = positionVirtuelle;
    liaisonOk = envoiRadio();
  }

  if (now - tStatut >= 1000) {
    tStatut = now;
    ble.print(modeRoute ? "ROUTE" : enNuit ? "NUIT" : horizontal ? "HORIZ" : modeAutomatique ? "AUTO" : "MANUEL");
    ble.print(" P1:");  ble.print(positionVirtuelle);
    ble.print(" P2:");  ble.print(liaisonOk ? (long)retour.pos : -1L);
    ble.print(" E:");   ble.print(abs(lireLDR(LDR_GAUCHE) - lireLDR(LDR_DROITE)));
    ble.print(" R:");   ble.println(liaisonOk ? "OK" : "PERDUE");
  }
}

// Remplace delay() : la radio et le Bluetooth restent actifs
void attendre(unsigned long ms) {
  unsigned long t = millis();
  while (millis() - t < ms) {
    service();
    delay(5);
  }
}

void arretMoteur() {
  analogWrite(RPWM, 0);
  analogWrite(LPWM, 0);
}

void passerManuel() {
  modeAutomatique = false;
  enNuit = false;
  arretMoteur();
  lcd.clear();
  lcd.print("MANUEL ACTIF");
  attendre(1000);
  lcd.clear();
  dernierTourManuel = millis();
}

void passerAuto() {
  modeAutomatique = true;
  horizontal = false;
  enNuit = false;
  modeRoute = false;
  debutSombre = 0;
  arretMoteur();
  lcd.clear();
  lcd.print("RETOUR EN AUTO");
  attendre(1000);
  alignementInitialContinu();
  dernierAjustementChrono = millis();
}

// Mode ROUTE : panneaux a plat, suivi coupe, joystick verrouille
void passerRoute() {
  modeAutomatique = false;
  enNuit = false;
  modeRoute = true;
  arretMoteur();
  lcd.clear();
  lcd.print("MODE: ROUTE");
  mettreHorizontal();
  dernierTourManuel = millis();
}

void traiterCommandeBle() {
  char c = cmdBle;
  cmdBle = 0;
  switch (c) {
    case 'A': if (!modeAutomatique) passerAuto();   break;
    case 'M': if (modeAutomatique)  passerManuel(); break;
    case 'S': bleImpulsionFin = 0;                  break;
    case 'U': if (!modeAutomatique && !modeRoute) { bleSens =  1; bleImpulsionFin = millis() + 1000; } break;
    case 'D': if (!modeAutomatique && !modeRoute) { bleSens = -1; bleImpulsionFin = millis() + 1000; } break;
    case 'R': passerRoute();                        break;
    case 'H': if (modeAutomatique) passerManuel(); mettreHorizontal(); break;
    case '?': tStatut = 0;                          break;
  }
}

// Recalage de nuit sur la butee basse : le verin coupe lui-meme son alimentation en fin de course
// (comme a l'allumage). Le compteur repart de zero, donc la mise a plat qui suit est exacte et
// le suivi du lendemain part d'une position juste. L'esclave descend en meme temps (mode 3).
// Interruptible : joystick, commande 'M' ou arret 'X'.
void rehomer() {
  if (!RECALAGE_NUIT) return;
  recalage = true;
  lcd.setCursor(0, 1);
  lcd.print("RECALAGE BAS    ");
  analogWrite(RPWM, 0);
  analogWrite(LPWM, VITESSE_MANU);
  unsigned long t0 = millis(), t = t0;
  bool termine = false;
  while (!termine) {
    if (digitalRead(PIN_JOY_SW) == LOW || cmdBle == 'M' || arretDemande) break;
    service();
    delay(5);
    unsigned long now = millis();
    positionVirtuelle -= (long)(now - t);     // le compteur descend pendant le recalage
    t = now;
    if (positionVirtuelle < 0) positionVirtuelle = 0;
    termine = (now - t0 >= TEMPS_CALIBRATION);
  }
  arretMoteur();
  recalage = false;
  if (termine) positionVirtuelle = 0;         // butee basse atteinte : compteur recale
}

// Suite d'un arret 'X' : moteur coupe, la position a plat n'est plus garantie,
// et on se retrouve en manuel (le suivi auto et le mode route ne repartent pas seuls).
void traiterArret() {
  arretMoteur();
  bleImpulsionFin = 0;
  horizontal = false;            // l'esclave suit alors la position reelle du maitre et s'arrete avec lui
  if (modeAutomatique) {
    passerManuel();
  } else {
    modeRoute = false;           // un arret deverrouille le joystick
    lcd.clear();
    lcd.print("ARRET - MANUEL");
    attendre(1000);
    lcd.clear();
    dernierTourManuel = millis();
  }
}

void setup() {
  pinMode(RPWM, OUTPUT);
  pinMode(LPWM, OUTPUT);
  pinMode(PIN_JOY_SW, INPUT_PULLUP);
  arretMoteur();

  delay(20);
  bool demarrageRoute = (digitalRead(PIN_JOY_SW) == LOW);   // bouton maintenu a l'allumage

  ble.begin(9600);

  // Si le module ne repond pas, on continue SANS radio (sinon radio.write peut bloquer le Nano)
  radio.begin();
  radioOk = radio.isChipConnected();   // tolerant : begin() peut echouer (alimentation limite, module clone) alors que la liaison fonctionne
  if (radioOk) {
    radio.setPALevel(RF24_PA_LOW);
    radio.setDataRate(RF24_250KBPS);
    radio.enableAckPayload();
    radio.setRetries(5, 10);
    radio.openWritingPipe(ADRESSE);
    radio.stopListening();
  }

  lcd.init();
  lcd.backlight();
  lcd.clear();
  lcd.print("MOSKITOS SOLAIRE");
  lcd.setCursor(0, 1);
  lcd.print(radioOk ? "CALIBRATION...  " : "CALIB.(RADIO KO)");

  // Mise a zero : l'esclave fait la meme chose en parallele
  positionVirtuelle = 0;
  analogWrite(RPWM, 0);
  analogWrite(LPWM, VITESSE_MANU);   // pleine vitesse : course complete = 20 s, 22 s suffisent
  attendre(TEMPS_CALIBRATION);
  arretMoteur();
  positionVirtuelle = 0;
  pret = true;

  if (demarrageRoute) {
    while (digitalRead(PIN_JOY_SW) == LOW) { delay(10); }
    passerRoute();
    return;
  }

  lcd.setCursor(0, 1);
  lcd.print("RECHERCHE SOLEIL");
  attendre(1000);

  alignementInitialContinu();
  dernierAjustementChrono = millis();
}

void loop() {
  service();
  if (arretDemande) { arretDemande = false; traiterArret(); }
  if (cmdBle) traiterCommandeBle();

  // Bouton joystick : bascule auto / manuel (on attend le relachement AVANT d'agir)
  if (digitalRead(PIN_JOY_SW) == LOW) {
    unsigned long t0 = millis();
    while (digitalRead(PIN_JOY_SW) == LOW) { delay(10); }
    unsigned long appui = millis() - t0;
    if (appui >= 2000) {                         // appui long : route <-> auto
      if (modeRoute) passerAuto(); else passerRoute();
    } else if (appui >= 30 && !modeRoute) {      // appui court : auto <-> manuel
      if (modeAutomatique) passerManuel(); else passerAuto();
    }
  }

  if (modeAutomatique) {
    if (millis() - dernierTestLumiere >= PERIODE_LUMIERE) {
      dernierTestLumiere = millis();
      gererLuminosite();
    }
    if (!enNuit && millis() - dernierAjustementChrono >= TEMPS_VEILLE) {
      verifierEtAjusterPosition();
      dernierAjustementChrono = millis();
    }

    char buf[17];
    lcd.setCursor(0, 0);
    lcd.print(enNuit ? "MODE: NUIT      " : "MODE: AUTO      ");
    lcd.setCursor(0, 1);
    // Ligne 2 : valeurs des deux cellules (G, D) et etat de la radio.
    // Nuit = G et D tous les deux sous SEUIL_NUIT.
    snprintf(buf, sizeof(buf), "G%-4dD%-4d R%c   ",
             lireLDR(LDR_GAUCHE), lireLDR(LDR_DROITE),
             liaisonOk ? '+' : '-');
    lcd.print(buf);
  }
  else {
    unsigned long now = millis();
    long dt = now - dernierTourManuel;
    dernierTourManuel = now;
    if (dt > 250) dt = 250;

    int lectureJoyX = analogRead(PIN_JOY_X);
    int lectureJoyY = analogRead(PIN_JOY_Y);

    int sens = 0;   // +1 montee, -1 descente
    if (lectureJoyY < SEUIL_MIN || lectureJoyX < SEUIL_MIN)      sens = 1;
    else if (lectureJoyY > SEUIL_MAX || lectureJoyX > SEUIL_MAX) sens = -1;
    else if (now < bleImpulsionFin)                              sens = bleSens;
    if (modeRoute) sens = 0;                     // joystick verrouille
    if (sens != 0) horizontal = false;

    lcd.setCursor(0, 0);
    lcd.print(modeRoute ? "MODE: ROUTE     " : "MODE: MANUEL    ");
    lcd.setCursor(0, 1);

    if (sens > 0) {
      lcd.print("SYNCHRO: MONTEE^");
      analogWrite(LPWM, 0);
      analogWrite(RPWM, VITESSE_MANU);
      positionVirtuelle += dt;
    }
    else if (sens < 0) {
      lcd.print("SYNCHRO: DESC. v");
      analogWrite(RPWM, 0);
      analogWrite(LPWM, VITESSE_MANU);
      positionVirtuelle -= dt;
    }
    else {
      if (modeRoute) {
        lcd.print("ROUTE: A PLAT   ");
      } else {
        char b2[17];
        snprintf(b2, sizeof(b2), "Repos Pos:%-5ld ", (long)positionVirtuelle);
        lcd.print(b2);
      }
      arretMoteur();
    }

    positionVirtuelle = constrain(positionVirtuelle, 0, TEMPS_COURSE_TOTALE);
  }

  delay(50);
}

// Amene le panneau a une position (ms depuis le zero). Interruptible : bouton ou "M".
void allerPosition(long cible) {
  cible = constrain(cible, 0L, TEMPS_COURSE_TOTALE);
  lcd.setCursor(0, 1);
  lcd.print("MISE HORIZONTALE");
  unsigned long t = millis();
  while (positionVirtuelle != cible) {
    if (digitalRead(PIN_JOY_SW) == LOW || cmdBle == 'M' || arretDemande) break;
    long ecart = cible - positionVirtuelle;
    // Vitesse MANUELLE : la position a plat se releve en manuel, il faut la meme vitesse
    if (ecart > 0) {
      analogWrite(LPWM, 0);
      analogWrite(RPWM, VITESSE_MANU);
    } else {
      analogWrite(RPWM, 0);
      analogWrite(LPWM, VITESSE_MANU);
    }
    service();
    delay(5);
    // On compte le temps REEL ecoule (radio et Bluetooth comprises), pas un pas theorique
    unsigned long now = millis();
    long dt = now - t;
    t = now;
    if (ecart > 0) {
      positionVirtuelle += dt;
      if (positionVirtuelle >= cible) positionVirtuelle = cible;
    } else {
      positionVirtuelle -= dt;
      if (positionVirtuelle <= cible) positionVirtuelle = cible;
    }
  }
  arretMoteur();
}

// Les deux panneaux a plat : le maitre ici, l'esclave via le paquet radio (mode 2)
void mettreHorizontal() {
  horizontal = true;
  allerPosition(POSITION_HORIZONTALE);
}

// Nuit : sombre pendant 5 min => a plat. Retour du jour => nouvelle recherche du soleil.
void gererLuminosite() {
  int g = lireLDR(LDR_GAUCHE);
  int d = lireLDR(LDR_DROITE);
  bool sombre = (g < SEUIL_NUIT && d < SEUIL_NUIT);
  bool jour   = (g > SEUIL_JOUR || d > SEUIL_JOUR);

  if (!enNuit) {
    if (sombre) {
      if (debutSombre == 0) {
        debutSombre = millis();
      } else if (millis() - debutSombre >= CONFIRMATION_NUIT) {
        enNuit = true;
        rehomer();
        mettreHorizontal();
      }
    } else {
      debutSombre = 0;
    }
  } else if (jour) {
    enNuit = false;
    horizontal = false;
    debutSombre = 0;
    lcd.clear();
    lcd.print("JOUR: RECHERCHE");
    alignementInitialContinu();
    dernierAjustementChrono = millis();
  }
}

void alignementInitialContinu() {
  int valGauche = lireLDR(LDR_GAUCHE);
  int valDroite = lireLDR(LDR_DROITE);
  int ecart = abs(valGauche - valDroite);

  // Diagnostic : valeurs des LDR et raison du (non-)mouvement, 2,5 s a l'ecran
  char dbg[17];
  snprintf(dbg, sizeof(dbg), "G:%-4d D:%-4d   ", valGauche, valDroite);
  lcd.setCursor(0, 0);
  if (valGauche < SEUIL_NUIT && valDroite < SEUIL_NUIT) lcd.print("TROP SOMBRE     ");
  else if (ecart <= ZONE_MORTE)                          lcd.print("DEJA ALIGNE     ");
  else                                                   lcd.print("RECHERCHE...    ");
  lcd.setCursor(0, 1);
  lcd.print(dbg);
  attendre(2500);

  if (valGauche < SEUIL_NUIT && valDroite < SEUIL_NUIT) return;

  while (ecart > ZONE_MORTE) {
    if (arretDemande) break;     // arret 'X' : traite par loop()
    // Bouton joystick : passage en manuel (attendre le relachement)
    if (digitalRead(PIN_JOY_SW) == LOW) {
      while (digitalRead(PIN_JOY_SW) == LOW) { delay(10); }
      modeAutomatique = false;
      lcd.clear();
      lcd.print("MANUEL ACTIF");
      break;
    }
    // Commande Bluetooth : passage en manuel
    if (cmdBle == 'M') {
      cmdBle = 0;
      modeAutomatique = false;
      lcd.clear();
      lcd.print("MANUEL ACTIF");
      break;
    }

    bool sensGauche = (valGauche > valDroite);
    if ((sensGauche && positionVirtuelle >= TEMPS_COURSE_TOTALE) || (!sensGauche && positionVirtuelle <= 0)) {
      lcd.setCursor(0, 0);
      lcd.print("EN BUTEE        ");
      attendre(1500);
      break;
    }

    if (sensGauche) {
      analogWrite(LPWM, 0);
      analogWrite(RPWM, VITESSE_AUTO);
      positionVirtuelle += 100;
    } else {
      analogWrite(RPWM, 0);
      analogWrite(LPWM, VITESSE_AUTO);
      positionVirtuelle -= 100;
    }
    attendre(100);
    arretMoteur();
    attendre(50);

    valGauche = lireLDR(LDR_GAUCHE);
    valDroite = lireLDR(LDR_DROITE);
    ecart = abs(valGauche - valDroite);
    lcd.setCursor(0, 1);
    lcd.print("Calage Eca: "); lcd.print(ecart); lcd.print("    ");
  }
  arretMoteur();
}

void verifierEtAjusterPosition() {
  int valGauche = lireLDR(LDR_GAUCHE);
  int valDroite = lireLDR(LDR_DROITE);
  int ecart = abs(valGauche - valDroite);
  if (valGauche < SEUIL_NUIT && valDroite < SEUIL_NUIT) return;

  if (ecart > ZONE_MORTE) {
    bool sensGauche = (valGauche > valDroite);

    // Duree bornee : on ne depasse jamais la course disponible
    long duree = sensGauche
        ? min(TEMPS_AJUSTEMENT, TEMPS_COURSE_TOTALE - positionVirtuelle)
        : min(TEMPS_AJUSTEMENT, positionVirtuelle);

    if (duree > 0) {
      if (sensGauche) {
        analogWrite(LPWM, 0);
        analogWrite(RPWM, VITESSE_AUTO);
        positionVirtuelle += duree;
      } else {
        analogWrite(RPWM, 0);
        analogWrite(LPWM, VITESSE_AUTO);
        positionVirtuelle -= duree;
      }
      unsigned long t0 = millis();
      while (millis() - t0 < (unsigned long)duree && !arretDemande) { service(); delay(5); }
      long fait = (long)(millis() - t0);
      if (fait < duree) {        // interrompu par 'X' : on corrige la position avec la duree reellement faite
        positionVirtuelle += sensGauche ? (fait - duree) : (duree - fait);
      }
      arretMoteur();
    }
  }
}

 

 

code arduino  "esclave"

 // ===== ESCLAVE : second panneau, suit la position du maitre par radio =====
// Version de controle : affiche l'etat de la radio sur le moniteur serie (USB, 115200 bauds).
// Mettre DEBUG_SERIE a false une fois la liaison verifiee.
const bool DEBUG_SERIE = false;
#include <SPI.h>
#include <RF24.h>

RF24 radio(9, 10);                  // CE, CSN
const byte ADRESSE[6] = "SOL01";    // identique au maitre

// ---------- Broches ----------
const int RPWM = 5;
const int LPWM = 6;

// ---------- Reglages ----------
// true si le second panneau est monte "en miroir" (le verin monte quand le premier descend)
const bool INVERSER_SENS = false;

const int  VITESSE_AUTO = 200;
const int  VITESSE_MANU = 255;
const long TEMPS_COURSE_TOTALE = 20000;
// Position "panneau a plat" du second panneau, en ms depuis son zero (a regler)
const long POSITION_HORIZONTALE_ESCLAVE = 10000;
const unsigned long TEMPS_CALIBRATION = 22000;
const unsigned long TIMEOUT_LIAISON   = 20000;  // arret si plus de nouvelles du maitre
const long TOL_DEMARRAGE = 150;                 // ecart (ms) pour demarrer
const long TOL_ARRET     = 40;                  // ecart (ms) pour s'arreter

// ---------- Messages radio (identiques au maitre) ----------
struct Paquet { uint8_t mode; int32_t cible; };
struct Retour { int32_t pos; uint8_t etat; };
Paquet paquet;
Retour retour = {0, 1};

long positionVirtuelle = 0;
long cible = 0;
uint8_t mode = 1;
bool enMouvement = false;
bool recuUneFois = false;
unsigned long dernierRx = 0;
unsigned long dernierTour = 0;
unsigned long nbRecus = 0, dernierAffiche = 0;
bool radioOk = false;      // module nRF24 reellement detecte au demarrage
unsigned long debutRecalage = 0;
bool enRecalage = false;

// sens : +1 monter, -1 descendre, 0 arret
void moteur(int sens, int vitesse) {
  int pinAvance = INVERSER_SENS ? LPWM : RPWM;
  int pinRecul  = INVERSER_SENS ? RPWM : LPWM;
  if (sens > 0) {
    analogWrite(pinRecul, 0);
    analogWrite(pinAvance, vitesse);
  } else if (sens < 0) {
    analogWrite(pinAvance, 0);
    analogWrite(pinRecul, vitesse);
  } else {
    analogWrite(pinAvance, 0);
    analogWrite(pinRecul, 0);
  }
}

void majAck() {
  retour.pos  = positionVirtuelle;
  retour.etat = enMouvement ? 2 : 1;
  radio.writeAckPayload(1, &retour, sizeof(retour));
}

void setup() {
  pinMode(RPWM, OUTPUT);
  pinMode(LPWM, OUTPUT);
  moteur(0, 0);

  if (DEBUG_SERIE) Serial.begin(115200);
  bool debutOk = radio.begin();
  radioOk = radio.isChipConnected();   // tolerant : begin() peut echouer sur une alimentation limite ou un clone
  if (DEBUG_SERIE) {
    Serial.println(F("ESCLAVE demarre"));
    Serial.print(F("radio.begin = "));
    Serial.println(debutOk);
    Serial.print(F("puce nRF24 detectee = "));
    Serial.println(radio.isChipConnected());
    Serial.println(F("Calibration 22 s (la radio n'ecoute pas encore)..."));
  }
  radio.setPALevel(RF24_PA_LOW);
  radio.setDataRate(RF24_250KBPS);
  radio.enableAckPayload();
  radio.openReadingPipe(1, ADRESSE);

  // Mise a zero (en meme temps que le maitre). Pendant ce temps la radio
  // n'ecoute pas : le maitre affichera "R-" jusqu'a la fin.
  moteur(-1, VITESSE_MANU);   // pleine vitesse : course complete = 20 s, 22 s suffisent
  delay(TEMPS_CALIBRATION);
  moteur(0, 0);
  positionVirtuelle = 0;

  radio.startListening();
  if (DEBUG_SERIE) Serial.println(F("Calibration terminee : j'ecoute la radio."));
  majAck();
  dernierTour = millis();
}

void loop() {
  unsigned long now = millis();
  long dt = now - dernierTour;
  dernierTour = now;
  if (dt > 100) dt = 100;

  // Module non detecte : on n'ecoute rien (sinon le bruit du bus SPI serait pris pour des ordres)
  if (radioOk && radio.available()) {
    radio.read(&paquet, sizeof(paquet));
    if (paquet.mode > 3) { delay(20); return; }   // paquet invalide (bruit) : ignore
    mode = paquet.mode;   // 0 manuel, 1 auto, 2 a plat, 3 recalage bas
    if (mode == 2) cible = POSITION_HORIZONTALE_ESCLAVE;
    else           cible = constrain((long)paquet.cible, 0L, TEMPS_COURSE_TOTALE);
    dernierRx = now;
    recuUneFois = true;
    nbRecus++;
    majAck();
  }

  if (DEBUG_SERIE && now - dernierAffiche >= 1000) {
    dernierAffiche = now;
    Serial.print(F("radio OK = "));
    Serial.print(radioOk);
    Serial.print(F("  paquets recus = "));
    Serial.print(nbRecus);
    Serial.print(F("  mode = "));
    Serial.print(mode);
    Serial.print(F("  cible = "));
    Serial.print(cible);
    Serial.print(F("  pos = "));
    Serial.println(positionVirtuelle);
  }

  // Securite : pas de nouvelles du maitre => on s'arrete
  if (!recuUneFois || now - dernierRx > TIMEOUT_LIAISON) {
    moteur(0, 0);
    enMouvement = false;
    delay(20);
    return;
  }

  // Mode 3 : recalage de nuit. On descend jusqu'a la butee basse (le verin coupe son alimentation),
  // tant que le maitre l'indique, 22 s au maximum, puis la position repart de zero.
  if (mode == 3) {
    if (!enRecalage) { enRecalage = true; debutRecalage = now; }
    if (now - debutRecalage < TEMPS_CALIBRATION) {
      moteur(-1, VITESSE_MANU);
      positionVirtuelle -= dt;
      if (positionVirtuelle < 0) positionVirtuelle = 0;
      enMouvement = true;
    } else {
      moteur(0, 0);
      positionVirtuelle = 0;
      enMouvement = false;
    }
    delay(20);
    return;
  }
  enRecalage = false;

  long ecart = cible - positionVirtuelle;

  if (!enMouvement && abs(ecart) > TOL_DEMARRAGE) enMouvement = true;
  if (enMouvement && abs(ecart) <= TOL_ARRET)     enMouvement = false;

  if (enMouvement) {
    int sens = (ecart > 0) ? 1 : -1;
    moteur(sens, (mode == 0 || mode == 2) ? VITESSE_MANU : VITESSE_AUTO);
    positionVirtuelle += sens * dt;
    positionVirtuelle = constrain(positionVirtuelle, 0L, TEMPS_COURSE_TOTALE);
  } else {
    moteur(0, 0);
  }

  delay(20);
}

 

Code du test des antennes, celle du maitre "émetteur":

 // ===== ARDUINO A : EMETTEUR =====
// Envoie un numero chaque seconde et affiche la reponse de l'Arduino B.
// Moniteur serie : 115200 bauds.
// Branchement nRF24L01 (Uno/Nano) : GND, VCC (voir note), CE=D9, CSN=D10, SCK=D13, MOSI=D11, MISO=D12
#include <SPI.h>
#include <RF24.h>

RF24 radio(9, 10);                  // CE, CSN
const byte ADRESSE[6] = "TEST1";

struct Message { uint32_t numero; };
struct Reponse { uint32_t recu; };
Message msg = {0};
Reponse rep;
unsigned long reussis = 0, rates = 0;

void setup() {
  Serial.begin(115200);
  bool b = radio.begin();
  Serial.print(F("begin = ")); Serial.println(b);
  Serial.print(F("puce detectee = ")); Serial.println(radio.isChipConnected());
  radio.setPALevel(RF24_PA_LOW);
  radio.setDataRate(RF24_250KBPS);
  radio.enableAckPayload();
  radio.setRetries(5, 10);
  radio.openWritingPipe(ADRESSE);
  radio.stopListening();
}

void loop() {
  msg.numero++;
  bool ok = radio.write(&msg, sizeof(msg));
  if (ok) {
    reussis++;
    Serial.print(F("Envoi ")); Serial.print(msg.numero); Serial.print(F(" : OK"));
    if (radio.available()) {
      radio.read(&rep, sizeof(rep));
      Serial.print(F("  | B a bien recu le numero ")); Serial.print(rep.recu);
    }
  } else {
    rates++;
    Serial.print(F("Envoi ")); Serial.print(msg.numero); Serial.print(F(" : PAS DE REPONSE"));
  }
  Serial.print(F("   (reussis ")); Serial.print(reussis);
  Serial.print(F(", rates ")); Serial.print(rates); Serial.println(F(")"));
  delay(1000);
}

 

Celui de l'esclave "récepteur":

 
// ===== ARDUINO B : RECEPTEUR =====
// Ecoute l'Arduino A, affiche ce qu'il recoit et renvoie un accuse de reception.
// Moniteur serie : 115200 bauds.
// Branchement nRF24L01 : GND, VCC (voir note), CE=D9, CSN=D10, SCK=D13, MOSI=D11, MISO=D12
#include <SPI.h>
#include <RF24.h>

RF24 radio(9, 10);                  // CE, CSN
const byte ADRESSE[6] = "TEST1";    // identique a A

struct Message { uint32_t numero; };
struct Reponse { uint32_t recu; };
Message msg;
Reponse rep = {0};
unsigned long total = 0;

void setup() {
  Serial.begin(115200);
  bool b = radio.begin();
  Serial.print(F("begin = ")); Serial.println(b);
  Serial.print(F("puce detectee = ")); Serial.println(radio.isChipConnected());
  radio.setPALevel(RF24_PA_LOW);
  radio.setDataRate(RF24_250KBPS);
  radio.enableAckPayload();
  radio.openReadingPipe(1, ADRESSE);
  radio.writeAckPayload(1, &rep, sizeof(rep));
  radio.startListening();
  Serial.println(F("B ecoute..."));
}

void loop() {
  if (radio.available()) {
    radio.read(&msg, sizeof(msg));
    total++;
    rep.recu = msg.numero;
    radio.writeAckPayload(1, &rep, sizeof(rep));   // sera renvoye avec l'accuse du prochain message
    Serial.print(F("Recu de A : numero ")); Serial.print(msg.numero);
    Serial.print(F("   (total recus ")); Serial.print(total); Serial.println(F(")"));
  }
}


Code html pour la connexion en Bluetooth (mode BLE):

<!doctype html>

<html lang="fr">

<head>

<meta charset="utf-8">

<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">

<title>Trike solaire</title>

<style>

  :root {

    --fond:#0f1720; --carte:#1a2531; --texte:#e8eef4; --doux:#8fa1b3;

    --vert:#2ea043; --bleu:#2f81f7; --orange:#d9822b; --rouge:#da3633; --gris:#3a4756;

  }

  * { box-sizing:border-box; -webkit-tap-highlight-color:transparent; }

  body { margin:0; background:var(--fond); color:var(--texte); font-family:system-ui,sans-serif;

         padding:16px; max-width:480px; margin-inline:auto; user-select:none; -webkit-user-select:none; }

  h1 { font-size:18px; margin:0 0 12px; font-weight:600; }

  .carte { background:var(--carte); border-radius:14px; padding:14px; margin-bottom:12px; }

  #connexion { width:100%; padding:14px; font-size:16px; border:0; border-radius:12px;

               background:var(--bleu); color:#fff; font-weight:600; }

  #etat { margin-top:8px; font-size:14px; color:var(--doux); min-height:20px; }

  .mode { text-align:center; font-size:26px; font-weight:700; padding:10px; border-radius:10px;

          background:var(--gris); letter-spacing:1px; }

  .mode.AUTO { background:var(--vert); } .mode.MANUEL { background:var(--orange); }

  .mode.ROUTE, .mode.HORIZ { background:var(--bleu); } .mode.NUIT { background:#4b3f8a; }

  .rangee { display:flex; justify-content:space-between; font-size:14px; color:var(--doux); margin:10px 0 4px; }

  .rangee b { color:var(--texte); }

  .barre { position:relative; height:16px; background:var(--gris); border-radius:8px; overflow:hidden; }

  .barre i { position:absolute; left:0; top:0; bottom:0; width:0; background:var(--bleu); transition:width .3s; }

  .barre u { position:absolute; top:0; bottom:0; width:3px; background:#fff; opacity:.8; }

  .perdue { color:var(--rouge) !important; }

  .grille { display:grid; grid-template-columns:1fr 1fr; gap:10px; }

  button.b { padding:18px 8px; font-size:16px; font-weight:700; border:0; border-radius:12px;

             color:#fff; background:var(--gris); touch-action:none; }

  button.b:active { filter:brightness(1.3); }

  .b.auto { background:var(--vert); } .b.manuel { background:var(--orange); }

  .b.bleu { background:var(--bleu); }

  .b.monter, .b.descendre { padding:30px 8px; font-size:20px; background:var(--gris); }

  .b.stop { grid-column:1 / -1; background:var(--rouge); padding:24px 8px; font-size:22px; }

  button:disabled { opacity:.4; }

  small { color:var(--doux); display:block; margin-top:8px; font-size:12px; line-height:1.4; }

</style>

</head>

<body>

<h1>☀️ Trike solaire</h1>


<div class="carte">

  <button id="connexion">Se connecter au trike</button>

  <div id="etat">Non connecté.</div>

</div>


<div class="carte">

  <div id="mode" class="mode">—</div>


  <div class="rangee"><span>Trike (inclinaison)</span><b id="t-val">—</b></div>

  <div class="barre"><i id="t-bar"></i><u id="t-plat"></u></div>


  <div class="rangee"><span>Remorque</span><b id="r-val">—</b></div>

  <div class="barre"><i id="r-bar"></i><u id="r-plat"></u></div>


  <div class="rangee"><span>Écart lumière</span><b id="ecart">—</b></div>

</div>


<div class="carte">

  <div class="grille">

    <button class="b auto"   data-cmd="A">AUTO</button>

    <button class="b manuel" data-cmd="M">MANUEL</button>

    <button class="b bleu"   data-cmd="R">ROUTE</button>

    <button class="b bleu"   data-cmd="H">À PLAT</button>

    <button class="b monter"   id="monter">▲ MONTER</button>

    <button class="b descendre" id="descendre">▼ DESCENDRE</button>

    <button class="b stop"   data-cmd="X">STOP</button>

  </div>

  <small>MONTER / DESCENDRE : maintenez le doigt appuyé. Le mouvement s'arrête quand vous lâchez.</small>

</div>


<script>

// ---------- Réglages ----------

const COURSE_TOTALE = 20000;   // ms pour la course complète (150 mm)

const POS_PLAT      = 9500;    // position "à plat" (doit correspondre à POSITION_HORIZONTALE du maître)

const SERVICE = 0xFFE0, CARACT = 0xFFE1;   // HM-10


let appareil = null, car = null, tampon = '', mode = '', repeter = null;

const $ = id => document.getElementById(id);

const boutons = () => document.querySelectorAll('button.b');


function etat(t) { $('etat').textContent = t; }

function activer(oui) { boutons().forEach(b => b.disabled = !oui); }

activer(false);

$('t-plat').style.left = (POS_PLAT / COURSE_TOTALE * 100) + '%';

$('r-plat').style.left = (POS_PLAT / COURSE_TOTALE * 100) + '%';


// ---------- Envoi d'une commande (un seul caractère) ----------

async function envoyer(c) {

  if (!car) return;

  try { await car.writeValueWithoutResponse(new TextEncoder().encode(c)); }

  catch (e) { try { await car.writeValue(new TextEncoder().encode(c)); } catch (e2) { etat('Envoi impossible : ' + e2.message); } }

}


// ---------- Réception : une ligne par seconde ----------

// Exemple : "AUTO P1:9500 P2:9300 E:12 R:OK"

function lire(ligne) {

  const m = ligne.match(/^(\S+)\s+P1:(-?\d+)\s+P2:(-?\d+)\s+E:(-?\d+)\s+R:(\w+)/);

  if (!m) return;

  mode = m[1];

  const p1 = +m[2], p2 = +m[3], ecart = m[4], radio = m[5];

  const pct = p => Math.max(0, Math.min(100, Math.round(p / COURSE_TOTALE * 100)));


  const el = $('mode'); el.textContent = mode; el.className = 'mode ' + mode;


  $('t-val').textContent = pct(p1) + ' %  (' + Math.round(p1 / COURSE_TOTALE * 150) + ' mm)';

  $('t-bar').style.width = pct(p1) + '%';


  const perdue = radio !== 'OK' || p2 < 0;

  $('r-val').textContent = perdue ? 'perdue' : pct(p2) + ' %  (' + Math.round(p2 / COURSE_TOTALE * 150) + ' mm)';

  $('r-val').classList.toggle('perdue', perdue);

  $('r-bar').style.width = perdue ? '0%' : pct(p2) + '%';


  $('ecart').textContent = ecart;

}


function reception(ev) {

  tampon += new TextDecoder().decode(ev.target.value);

  let i;

  while ((i = tampon.indexOf('\n')) >= 0) {

    lire(tampon.slice(0, i).trim());

    tampon = tampon.slice(i + 1);

  }

  if (tampon.length > 200) tampon = '';

}


// ---------- Connexion Bluetooth ----------

async function connecter() {

  if (!navigator.bluetooth) {

    etat("Bluetooth indisponible ici. Ouvrez cette page avec Chrome sur Android.");

    return;

  }

  try {

    etat('Recherche du module…');

    appareil = await navigator.bluetooth.requestDevice({

      filters: [{ services: [SERVICE] }, { namePrefix: 'HMSoft' }],

      optionalServices: [SERVICE]

    });

    appareil.addEventListener('gattserverdisconnected', deconnecte);

    etat('Connexion…');

    const serveur = await appareil.gatt.connect();

    const service = await serveur.getPrimaryService(SERVICE);

    car = await service.getCharacteristic(CARACT);

    await car.startNotifications();

    car.addEventListener('characteristicvaluechanged', reception);

    activer(true);

    $('connexion').textContent = 'Connecté ✓';

    etat('Connecté à ' + (appareil.name || 'HM-10') + '.');

    envoyer('?');

  } catch (e) {

    etat('Échec : ' + e.message);

    activer(false);

  }

}

function deconnecte() {

  car = null; activer(false); arreterRepetition();

  $('connexion').textContent = 'Se reconnecter';

  etat('Déconnecté.');

}

$('connexion').addEventListener('click', () => {

  if (appareil && appareil.gatt.connected) appareil.gatt.disconnect(); else connecter();

});


// ---------- Boutons simples ----------

document.querySelectorAll('button[data-cmd]').forEach(b =>

  b.addEventListener('click', () => envoyer(b.dataset.cmd)));


// ---------- MONTER / DESCENDRE : maintenir appuyé ----------

function arreterRepetition() { if (repeter) { clearInterval(repeter); repeter = null; } }


async function demarrerMouvement(cmd) {

  if (mode === 'AUTO' || mode === 'ROUTE' || mode === 'HORIZ' || mode === 'NUIT') {

    await envoyer('M');                      // passe en manuel d'abord

    await new Promise(r => setTimeout(r, 150));

  }

  envoyer(cmd);

  arreterRepetition();

  repeter = setInterval(() => envoyer(cmd), 400);   // répète tant que le doigt reste appuyé

}

function finMouvement() {

  if (!repeter) return;

  arreterRepetition();

  envoyer('S');                              // relâché : arrêt du mouvement

}

function maintien(id, cmd) {

  const b = $(id);

  b.addEventListener('pointerdown', e => { e.preventDefault(); b.setPointerCapture(e.pointerId); demarrerMouvement(cmd); });

  ['pointerup', 'pointercancel', 'lostpointercapture'].forEach(t => b.addEventListener(t, finMouvement));

  b.addEventListener('contextmenu', e => e.preventDefault());

}

maintien('monter', 'U');

maintien('descendre', 'D');


// STOP général : aussi arrêter toute répétition

document.querySelector('.stop').addEventListener('click', arreterRepetition);

</script>

</body>

</html>

jeudi 23 janvier 2025

Volumio, écran tactile 3.5 MPI3508 sur RPI 3B+

 

 

 https://community.volumio.com/t/volumio-with-3-5-tft-touch-screen-gpio-rpi-3b/11265/323

 

Assurez vous d'avoir une installation "fraiche" de Volumio.

  1. Connecter vous à Volumio en ssh (https://developers.volumio.com/SSH%20Connection)

  2. Installer le plugin "Touch display" mais ne le rendez pas effectif tout de suite.

  3. Executez:

volumio kernelsource (installation des sources du noyau ce qui prend un peu de temps) 

sudo apt-get -y install xserver-xorg-video-fbturbo

sudo apt-get -y install xserver-xorg-input-evdev

sudo cp /usr/share/X11/xorg.conf.d/10-evdev.conf /usr/share/X11/xorg.conf.d/45-evdev.conf

sudo curl -L https://github.com/goodtft/LCD-show/blob/master/usr/99-calibration.conf-3508-0 -o /etc/X11/xorg.conf.d/99-calibration.conf

Ci-dessous le contenu de ce qui sera collé dans 99-calibration.conf

cat >> /etc/X11/xorg.conf.d/99-calibration.conf <<EOL
Section "InputClass"
        Identifier      "calibration"
        MatchProduct    "ADS7846 Touchscreen"
        Option  "Calibration"   "3945 233 3939 183"
        Option  "SwapAxes"      "1"
EndSection

 

sudo curl -L https://github.com/goodtft/LCD-show/blob/master/usr/99-fbturbo.conf-HDMI -o /usr/share/X11/xorg.conf.d/99-fbturbo.conf

Ci-dessous le contenu de ce qui sera collé dans 99-fbturbo.conf

cat >> /usr/share/X11/xorg.conf.d/99-fbturbo.conf <<EOL
# This is a minimal sample config file, which can be copied to
# /etc/X11/xorg.conf in order to make the Xorg server pick up
# and load xf86-video-fbturbo driver installed in the system.
#
# When troubleshooting, check /var/log/Xorg.0.log for the debugging
# output and error messages.
# Run "man fbturbo" to get additional information about the extra
# configuration options for tuning the driver.

Section "Device"
        Identifier      "Allwinner A10/A13/A20 FBDEV"
        Driver          "fbturbo"
        Option          "fbdev" "/dev/fb0"

        Option          "SwapbuffersWait" "true"
EndSection

On modifie le fichier userconfig.txt ce qui permet au système de prendre en compte les différents paramètres de l'écran et de la partie tactile

cat >> /boot/userconfig.txt <<EOL

dtparam=spi=on
config_hdmi_boost=7
hdmi_group=2
hdmi_mode=87
hdmi_drive=2
hdmi_cvt 480 320 60 6 0 0 0
dtoverlay=ads7846
dtparam=cs=1
dtparam=penirq=25
dtparam=penirq_pull=2
dtparam=speed=50000
dtparam=swapxy=0
dtparam=pmax=255
dtparam=xohms=150
dtparam=xmin=200
dtparam=xmax=3900
dtparam=ymin=200
dtparam=ymax=3900
EOL
On redémarre 
  1. Reboot.

Si tout se passe bien, vous rendez effectif le plugin "Touchdisplay" et l'écran devrai afficher une page.

C'est terminé. 

Merci à la communauté Volumio pour son aide.

https://community.volumio.com/t/raspberry-pi3-hdmi-touch-screen/6729

 

 


 


sudo nano app-013f2d5c5e.css
press [CTRL]+W to search and type in "volumeManager{margin-top"
this should show the line:
	[code]

@media (max-width:767px){#trackManagerWrapper{left:-30px;right:-30px}}#trackManager,.volumeManager{margin-top:110px}
[/code]
change margin-top:110px to margin-top:55px

You can also search for:
		webkit-scrollbar{width:10px;height:10px}:
and change the width to a larger number to make the scroll bar more finger friendly (40px should be fine)[/spoiler]