Automatisme suiveur pour panneau solaire

Bonjour, comme je l'ai indiqué dans mon post de présentation je vais décrire une réalisation que je viens de terminer. Il y aura sans doute des modifications, mais seulement après une période d'observation pour voir si tout va bien et si le système reste fiable sur la durée.
Le système est constitué de :
-Un chassis soudé en fer L de 30x30
-Deux paliers à bille supportant l'axe de rotation au dessus duquel est fixé le panneau solaire.
-Un moteur pas à pas Nema23 de 2,8A avec un jeu de poulies crantées et courroie ad hoc.
-Une tige filetée de 12mm pour réaliser l'axe (c'est plus facile pour positionner les différents éléments.
-Un capteur de position optoélectronique.
-Une carte Arduino Nano Every.
-Une carte horloge RTC DS3231.
-Un driver de moteur pas à pas TB6600.
-Un afficheur LCD 4x20 avec une carte d'interface I2C.
-Un module Bluetooth HC05
-Une housse pour barbecue afin de protéger le mécanisme des intempéries.

Le fonctionnement est le suivant :
Lors de la mise en marche, on fait une séquence d'initialisation (balayage droite/gauche) pour
retrouver la position du détecteur optique et définir l'angle 90° (le capteur pointe sur le SUD).
Ensuite, en fonction de l'heure le panneau est positionné à l'angle correspondant (15°/heure).
L'actualisation est faite toutes les quinze minutes.
L'interface Bluetooth permet de passer plusieurs commandes au contrôleur : La mise à l'heure de l'horloge RTC, le positionnement du panneau à un angle précis, obtenir un rapport d'état des paramètres, et relancer le contrôleur (reboot) en cas de besoin.
Le système fonctionne entre 6H et 22H, après on positionne le panneau à l'EST pour le lendemain matin. L'inclinaison du panneau n'est pas motorisée mais peut être ajustée pour tenir compte de la hauteur du soleil selon la saison.
Photo du montage :


à suivre...

ça vous oblige à positionner un point précis du montage vers le sud, non ? L'orientation pourrait être un paramètre comme les autres ainsi que la position GPS pour calculer la position du soleil et le lever / coucher de soleil.

Je suppose qu'à terme, vous voudrez piloter aussi votre panneau photovoltaïque en hauteur et avoir un peu plus de précision en azimut, quoique ce ne soit pas vraiment nécessaire.

Je vous propose une petite librairie qui vous donne les angles d'azimut et de hauteur en fonction de l'heure et de votre position géographique :
ChP_Ephem_Soleil.zip (3,1 Ko)

Cordialement.

Pierre.

Bonsoir, je vais répondre aux deux messages précédents.
Positionner le montage par rapport au Sud n'est pas une grande contrainte dans mon cas. Mais effectivement, tenir compte de la date pour déclencher la marche et l'arrêt du mouvement serai une amélioration que je ferai peut-être plus tard.
Ensuite, non je n'envisage pas de piloter le panneau pour suivre la hauteur du soleil. Je me conterai éventuellement d'en modifier l'inclinaison en cours d'année.
Afin de clarifier ma démarche, j'utilise ce montage pour un panneau solaire de 50W, autant dire que pour quelques euros de plus j'aurais aussi bien pu installer un panneau de 100W fixe, et cela m'aurait donné le même résultat. Mais bien sûr, le plaisir de réaliser ce montage n'est pas comparable. Enfin, je crois que plus un système est simple, et plus il a de chances de fonctionner, d'être maintenable et fiable, alors j'essaye de ne pas faire trop compliqué, bien que ma tendance personnelle m'y pousse trop souvent.
Merci beaucoup pour vos réponses, et pour le fichier qui me sera utile sans aucun doute.
A bientôt,
Sylvain

oui c'est la partie fun même si ça crée des ennuis de maintenance ensuite :slight_smile:

Perso j'ai mis plus de panneaux que nécessaire et ils sont fixes - ça me laisse du temps pour d'autres projets où la garantie de fonctionnement n'est pas importante.

Il y a aussi le fait que le rendement finale du panneau solaire n'est pas forcément positif.

Par contre le plaisir de faire ce jolie projet n'a pas d'équivalent :slight_smile:

En ce qui me concerne, c'est pour mettre au fond du jardin afin d'avoir de la lumière.
Voici la suite, le schéma et les plans mécaniques


et le code

/* Programme Pansol1
syph60 02/2025
Ce programme assure le fonctionnement d’un support pivotant pour panneau solaire.

Le panneau est fixé sur un support pivoté avec une roue dentée mue par une courroie et un moteur pas à pas. 
Le moteur pas à pas est commandé par le programme et utilise une carte d’interface dédiée.
Le driver du moteur pas à pas est câblé de manière à ce qu'une impulsion génère 1/32eme de tour, et 
l'ensemble poulies-courroie assure une démultiplication x2.8. Il faut donc 17920 impulsion pour un tour complet,
et 50 impulsions pour tourner le panneau de un degré.
La synchonisation avec la rotation du soleil est pilotée via une carte horloge RTC interfacée en I2C.
Enfin, le montage est doté d’un afficheur LCD 20x4 avec une carte d’interface I2C. Cet afficheur permet  
d’indiquer la date et l’heure, et certains paramètres du système.
Un bouton poussoir permet d’effectuer la mise à l’heure du système (solution de secours). Sinon, la carte est équipée d'un
module bluetooth HC05 qui permet via un terminal bluetooth depuis un smartphone, d'envoyer quelque commandes :
Date : JJ/MM/AAAA - HH:MM:SS -> Effectue la mise à l'heure de l'horloge RTC"
Init :                       -> Lance la séquence d'initialisation
GoAng : Valeur               -> Positionne le panneau à l'angle indiqué
Rapport :                    -> Affiche un rapport d'état du système
Reboot :                     -> Redemarre le contrôleur
Au démarrage du système on effectue un balayage droite-gauche pour retrouver le point central au moyen d'un fin de course optique
positionné vers le SUD (90°) ensuite le panneau est positionné en fonction de la position du soleil correspondant à l'heure du moment.
Le suivi du soleil est actualisé toutes les quinze minute entre 6H00 et 22H00. En dehors de cette plage le panneau est positionné
à l'Est (gauche).
*/

#include <Wire.h>
#include <LiquidCrystal_I2C.h> // Bibliothèque contrôle afficheur LCD via liaison I2C
#include <stdio.h> 
#include <stdarg.h>
#include <RTClib.h> // Librairie horloge temps réèl
# include <SoftwareSerial.h> // Librairie pour liaison série


// Constantes
const uint8_t CstepPin = 2; // Définition sortie commande STEP
const uint8_t CdirPin = 3; // Définition sortie commande DIR
const uint8_t CEnPin = 4; // Définition sortie Enable
const uint8_t CEstate = 9; // Entrée D9 pour état module Bluetooth
const uint8_t CErx = 11; // Entrée D11 réception Bluetooth coté controleur
const uint8_t CEtx = 10; // Entrée D10 émission Bluetooth coté controleur
const int Cangmin = 0; // Angle mini (le plus à gauche) avec le Sud = 90°
const int Cangmax = 225; // Angle maxi (le plus à droite)
const int CCpto = 5; // Définition entrée FDC optique
const int CPBut = 6; // N° entrée du bouton poussoir mise à l'heure
const int VD13 = 13; // N° entrée builtin diode
const int Ctpexit = 5000; // Valeur tempo de sortie du mode réglage (5s)
const int Ctpuls = 1500; // Durée impulsion pour temps long
const int CCliRythm = 300; // Rythme du clignotement affichage mise à l'heure
const unsigned long CTOlcd = 300000; // Time out pour éteindre le LCD = 5 minutes
const int CEbat = A3 ; // Entrée analogique A3 pour tension batterie
const float CKu = 0.012389666; // 5*(7850 + 4656) / (4656 * 1024), Coefficient pour lecture tension batterie
// Fin section constantes

// Variables
LiquidCrystal_I2C lcd = LiquidCrystal_I2C(0x27, 20, 4); // Declaration écran LCD
int Vsteps = 17920; // Variable nombre de pas pour 180 degrés (ex 19200)
int Vstepdeg = 50; // Nombre de pas pour tourner d'un angle de 1,004464 degrés
int VmsDelay = 500; // Variable durée impulsion moteur
int Vangcur = 90; // Valeur angle courant d'apès les déplacements du moteur
uint16_t VHrs = 0;          // Heure courante
uint16_t VMin = 0;          // Minutes courantes
uint16_t VSec = 0;          // Secondes courantes
uint16_t VMois = 1;         // Mois courant
uint16_t VJour = 1;         // Jour courant
uint16_t VAnnee = 2025;       // Année courante
unsigned long VExit =0; // Timeout pour sortie du mode réglage
char VBuff[20];         // Buffer de texte de la ligne affichée (date + heure)
int VBuffidx = 0;       // Index buffer
int VResuBP = 0;        // Retour fonction test du bouton poussoir
bool VReglage = false;  // Fonction réglage date et heure active
byte VStpReg = 0;       // Step réglage 1 = Jour, 2 = mois, 3 = année, 4 = heure...
byte Vprem = true;      // Flag utilisé pour détecter le premier passage dans une boucle
byte V4print = false;   // Flag utilisé pour déclencher l'impression du buffer texte
byte VCliRythm = false; // Flag utilisé pour le clignotement affichage réglage date/heure
byte VcurX = 0; // Position curseur sur la ligne
byte VcurY = 0; // Numéro de ligne
int Vparam = 0;
byte Vtemps = false;
RTC_DS3231 Vrtc;
DateTime VHrcour;
int VStatut = 0; // Variable statut général 0 = Ok, 1 = défaut
bool VFinjour = false; // Variable utilisée pour le retour à gauche après 22H
bool VStlcd = true; // Etat allumé lcd
int VResSerial = 0; // Resultat fonction F_HeureSerie
int VAng = 0; // Angle pour commande moteur à partir d'un ordre passé par la liaison série
float VUbat = 0.0; // Tension batterie
SoftwareSerial VBt(CErx, CEtx); // Bluetooth Emission, Réception coté controleur
int VEtatBT; // Valeur entrée état module Bluetooth
// Fin section variables

void setup() {
  Serial.begin(9600);
  VBt.begin(9600); // initialisation Bluetooth
  VBt.println("");
  lcd.init(); // Initialisation afficheur
  lcd.clear(); // Effacement afficheur
  lcd.backlight(); // Allumage Afficheur
  pinMode(CstepPin, OUTPUT); // Sortie STEP
  pinMode(CdirPin, OUTPUT); // Sortie sens de rotation
  pinMode(CEnPin, OUTPUT); // Sortie Enable
  digitalWrite(CEnPin, LOW); // On éteint le moteur
  pinMode(CCpto, INPUT); // Entrée FDC optique
  pinMode(CEstate, INPUT); // Entrée état module Bluetooth
  Vrtc.begin(); // Démarrage horloge temps réèl
  delay(500);
  F_Serprintln("*  Fonction setup  *");
  F_Serprintln("   Commandes disponibles :");
  F_Serprintln("Date : JJ/MM/AAAA - HH:MM:SS -> Effectue la mise à l'heure de l'horloge RTC");
  F_Serprintln("Init :                       -> Lance la séquence d'initialisation");
  F_Serprintln("GoAng : Valeur               -> Positionne le panneau à l'angle indiqué");
  F_Serprintln("Rapport :                    -> Affiche un rapport d'état du système");
  F_Serprintln("Reboot :                     -> Redemarre le contrôleur");
  VEtatBT = digitalRead(CEstate); // lecture état module Bluetooth (haut = connecté, bas = non connecté)
  F_Serprint("Etat Bluetooth = ");
  if (VEtatBT == HIGH) {F_Serprintln("connecté");} else {F_Serprintln("non connecté");}
  VHrcour = Vrtc.now(); // lecture date et heure courante
  VAnnee = VHrcour.year();   // initialisation avec la date courante
  VJour = VHrcour.day();
  VMois = VHrcour.month();
  VHrs = VHrcour.hour();
  VMin = VHrcour.minute();
  VSec = VHrcour.second();
  F_Serprint("Date : ");
  F_Serprint(String(VAnnee));
  F_Serprint("/");
  F_Serprint(String(VMois));
  F_Serprint("/");
  F_Serprint(String(VJour));
  F_Serprint("  Heure : ");
  F_Serprint(String(VHrs));
  F_Serprint(":");
  F_Serprint(String(VMin));
  F_Serprint(":");
  F_Serprintln(String(VSec));
  lcd.setCursor(0,1); // Positionnement curseur en haut
  lcd.print("Angle   : ");
  lcd.setCursor(0,2);
  lcd.print("Statut  : ");
  lcd.setCursor(14,2);
  VUbat = F_Ubat();
  F_Serprintln("Tension batterie = " + String(VUbat) + "V");
  lcd.print(VUbat,2);
  lcd.setCursor(0,3);
  lcd.print("Temperature : ");
  lcd.print(Vrtc.getTemperature());
  F_Faitligne();
  lcd.setCursor(0,0);
  lcd.print(VBuff);
  F_init();
  F_Position();
  F_RefreshLCD();
  F_Serprintln("Statut : " + String(VStatut));
} // Fin fonction setup

void loop()
{
  F_Timer2(900); // (délais 15 minutes entre deux lectures)
  if (Vtemps)
  {
    F_Serprint("Vtemps");
    Vtemps = false;
    F_Position(); // Positionnement panneau
  }
  F_gestheure();
  VResSerial = F_HeureSerie();
  if (VResSerial == 1) // Mise à l'heure
  {
    Vrtc.adjust(DateTime(VAnnee, VMois, VJour, VHrs, VMin, VSec)); // Mise a l'heure saisie sur term USB ou Bluetooth
    F_Serprintln("Nouvelle date : " + String(VJour) + "/" + String(VMois) +"/" + String(VAnnee) + " - " + String(VHrs)+ ":" + String(VMin) + ":" + String(VSec) + " - Statut = " + String(VStatut));
    F_Position(); // Repositionnement panneau selon nouvelle heure
  }
  if (VResSerial == 2) // Commande balayage d'initialisation
  {
    F_Serprintln("Commande Init :");
    F_init();
  }
  if (VResSerial == 3) // Commande Go Left, positionnement panneau à un angle donné
  {
    F_Serprintln("Commande GoAng Left :");
    F_Gole(VAng);   
  }
  if (VResSerial == 4) // Commande Go Right, positionnement panneau à un angle donné
  {
    F_Serprintln("Commande GoAng Right :");
    F_Gori(VAng);   
  }
  if (VResSerial == 5)  // Impression du rapport sur terminal USB ou Bluetooth
  {
    F_Serprintln("Rapport :");
    F_Serprintln("Heure :" + String(VHrs) + ":" + String(VMin) + ":" + String(VSec) + " ,  angle courant : " + String(Vangcur));
    F_Serprintln("Tension batterie = " + String(VUbat,2) + "V");
    F_Serprint("Statut : ");
    if (VStatut == 0) {F_Serprint("OK ");} else {F_Serprint("NOK");}
    F_Serprintln("  Température = " + String(Vrtc.getTemperature(),2));
  }
  if (VResSerial == 6) // Commande Reboot
  {
     F_reboot();  
  }  
  F_TOlcd(); // Check si pas de timeout pour l'afficheur LCD
  if(VReglage) {F_lcdmajreg();}
} // Fin fonction loop


void F_Position() // Routine utilisée pour positionner le panneau en fonction de l'heure courante
{
  unsigned long VTjour; // nombre de secondes écoulées depuis minuit
  int VNangle; // Nouvelle position
  if (VStatut == 0) // On s'assure que le panneau à bien été positionné pendant la séquence init
  {
    VHrcour = Vrtc.now(); // lecture date et heure courante
    VHrs = VHrcour.hour();
    VMin = VHrcour.minute();
    VSec = VHrcour.second();
    VTjour = ((unsigned long) VHrs * 3600) + ((unsigned long) VMin * 60) + (unsigned long) VSec; // Calcul nombre de secondes depuis minuit
    if (VTjour > 21600 && VTjour < 79200) // On ne positionne le panneau qu'entre 6H et 22H
    {
      if (VFinjour == true) // premier mouvement à 6H
      {
        VFinjour = false;
        F_Serprintln("En piste, il est 6H");
        F_init;
      }
      VNangle = int(VTjour * 0.004167) - 90; // Le nouvel angle est calculé relativement à la position zéro (midi) = 90°
      F_Serprintln("Heure :" + String(VHrs) + ":" + String(VMin) + ":" + String(VSec) + " , nouvel angle : " + String(VNangle) + "   angle courant : " + String(Vangcur));     
      if (VNangle != Vangcur)
      {
        if (VNangle > Vangcur)
        {
          F_Gori(VNangle - Vangcur);
          F_Serprintln("Direction droite, nouvel angle : " + String(Vangcur));
          delay(1000); // On attend une seconde, puis
          //digitalWrite(CEnPin, HIGH); // on éteint le moteur
        } else
        {
          F_Gole(Vangcur - VNangle);
          F_Serprintln("Direction gauche, nouvel angle : " + String(Vangcur));
          delay(1000); // On attend une seconde, puis
          //digitalWrite(CEnPin, HIGH); // on éteint le moteur          
        }
      }
    }
    if (VTjour > 79200 && VFinjour == false) // Après 22H
    {
      VFinjour = true; // On renvoie le panneau à gauche
      F_Gole(Vangcur-1);
      F_Serprintln("Retour au bercail, il est 22H");
    }
  }
} // Fin fonction F_Position


void F_Gole(int P_angle){ // Commande du panneau vers la gauche d'un angle donné en paramètre
  digitalWrite(CEnPin, LOW);
  digitalWrite(CdirPin, HIGH);
  if((Vangcur - P_angle) > Cangmin)
  {
    Vangcur -= P_angle;
    for(int x = 0; x < (Vstepdeg * P_angle); x++)
    {
      digitalWrite(CstepPin, HIGH);
      delayMicroseconds(VmsDelay);
      digitalWrite(CstepPin, LOW);
      delayMicroseconds(VmsDelay);
    }
  }
  //digitalWrite(CEnPin, HIGH); // On éteint le moteur
} // Fin fonction F_Gole

void F_Gori(int P_angle){ // Commande du panneau vers la droite d'un angle donné en paramètre
  digitalWrite(CEnPin, LOW); // activation moteur
  digitalWrite(CdirPin, LOW); // sélection sens de rotation
  if((Vangcur + P_angle) < Cangmax)
  {
    Vangcur += P_angle; // mise à jour de la valeur Angle courant
    for(int x = 0; x < (Vstepdeg * P_angle); x++)
    {
      digitalWrite(CstepPin, HIGH);
      delayMicroseconds(VmsDelay);
      digitalWrite(CstepPin, LOW);
      delayMicroseconds(VmsDelay);
    }
  }
  //digitalWrite(CEnPin, HIGH); // On éteint le moteur
} // Fin fonction F_Gori

/* Cette fonction lit les données dispo sur la liaison série devant avoir la forme :
Date : JJ/MM/AAAA - HH:MM:SS
Les espaces n'ont pas d'importance, ni les caratères séparateurs utilisés, par contre les tous les séparateurs doivent
être présent, et tous les champs doivent avoir 2 caratères sauf l'année qui en a 4
Si des champs sont omis, leur valeur est fixée à zéro */

int F_HeureSerie()
{
  String VInbuff; // Buffer entrée liaison série
  String VStr; // Chaine temporaire de travail
  int VIdx; // Utilisé pour déterminerla position des caratères
  int VNbcar = 0; // Nombre de caractères à lire sur la liaison série
  int VRet = 0; // Variable de retour
  VRet = 0;
  VIdx = -1;
  if (Serial)
  {
    if ( (VNbcar = F_Seravailable()) > 0) // Caractères disponibles sur liaison série
    {
      VInbuff = F_Trimall(F_SerreadString()); // Supression des espaces
      VIdx = F_Trouvestr(VInbuff, "Date:", 0); // Vérification syntaxique indiquant une commande de mise à l'heure
      if (VIdx != -1)
      {
        VStr = VInbuff.substring(VIdx+5, VIdx + 7); // Récupération des paramètres de réglage en format numérique
        VJour = VStr.toInt();
        VStr = VInbuff.substring(VIdx+8, VIdx + 10);
        VMois = VStr.toInt();
        VStr = VInbuff.substring(VIdx+11, VIdx + 15);
        VAnnee = VStr.toInt();
        VStr = VInbuff.substring(VIdx+16, VIdx + 18);
        VHrs = VStr.toInt();
        VStr = VInbuff.substring(VIdx+19, VIdx + 21);
        VMin = VStr.toInt();
        VStr = VInbuff.substring(VIdx+22, VIdx + 24);
        VSec = VStr.toInt();
        VRet = 1;                       
      }
      VIdx = F_Trouvestr(VInbuff, "Init:", 0); // Vérification syntaxique indiquant une commande d'initialisation
      if (VIdx != -1)
      {
        VRet = 2;
      }
      VIdx = F_Trouvestr(VInbuff, "GoAng:", 0); // Vérification syntaxique indiquant une commande d'initialisation
      if (VIdx != -1)
      VAng = VInbuff.substring(VIdx+6, VIdx + 8).toInt(); // Récupération des paramètres de réglage en format numérique
      {
        if (VAng > 0 && VAng != Vangcur)
        {
          if (VAng > Vangcur)
          {
            VAng = VAng - Vangcur;
            VRet = 3; // Exécuter commande Gole
          } else
          {
            VAng = Vangcur - VAng;
            VRet = 4; // Exécuter commande Gori
          }
        }          
      }
      VIdx = F_Trouvestr(VInbuff, "Rapport:", 0); // Vérification syntaxique indiquant une commande d'impression du rapport
      if (VIdx != -1)
      {
        VRet = 5;
      }
      VIdx = F_Trouvestr(VInbuff, "Reboot:", 0); // Vérification syntaxique indiquant une commande Reboot
      if (VIdx != -1)
      {
        VRet = 6;
      }                 
    }
  }
  return VRet;
} // Fin fonction F_HeureSerie

/* Cette fonction recherche la première occurence d'une chaine A, dans une chaine B
en commençant à la position X */

int F_Trouvestr(String PChn, String PChx, int PDeb)
{
  int VPos1 = 0;
  int VPos2 = 0;
  int VLgn = 0;
  int VLgx = 0;
  int VResu = 0;
  String VSubch;
  VResu = -1;
  VLgn = PChn.length(); // Longueur chaine origine
  VLgx = PChx.length(); // Longueur chaine recherchée
  VPos1 = PDeb;
  do
  {
    VPos2 = VPos1 + VLgx;
    if (VLgn > 0 && VLgx > 0 && VPos2 < VLgn)
    {
      VSubch = PChn.substring(VPos1, VPos2);
      if (VSubch != PChx)
      {
        VPos1++;
      } else {VResu = VPos1;}
    }
  } while (VResu == -1 && (VPos1+VLgx) < VLgn);
  return VResu;
} // Fin fonction F_Trouvestr

/* Cette fonction retourne la chaine passée en paramètre en supprimant tous les espaces */

String F_Trimall(String PChn)
{
  String VNewstr = "";
  for (int i = 0; i < PChn.length(); i++)
  {
    if (PChn[i] != ' ') {VNewstr += PChn[i];}
  }
  return VNewstr;
} // Fin fonction F_Trimall


void F_init() /* Au démarrage on fait un balayage complet à droite puis un retour
                 à gauche en recherchant la position centrale avec le Fin De Course optique.*/ 
{
  int VCpto = HIGH; // Variable lecture FDC optique , entrée pullup
  VCpto = digitalRead(CCpto);
  while(Vangcur < (Cangmax-1) && VCpto == HIGH) // On amène le panneau complètement à droite ou FDC optique trouvé
  {
    F_Gori(1);
    VCpto = digitalRead(CCpto);   
  }
  if (VCpto == HIGH)
  {
    while(Vangcur > (Cangmin+1) && VCpto == HIGH) // On amène le panneau à gauche jusqu'au FDC optique
    {
      F_Gole(1);
      VCpto = digitalRead(CCpto);     
    }
  }
  delay(1000); // On attend une seconde, puis
  //digitalWrite(CEnPin, HIGH); // on éteint le moteur
  if (VCpto == HIGH) {VStatut = 1;} else // Erreur, on a pas détecté le FDC optique, sinon l'angle courant est égal à 90°
  {
    Vangcur = 90;
    VStatut = 0; //Statut OK
  }
} // Fin fonction F_init


// Fonction pour effacer les caratères restant sur une ligne de l'écran LCD
void F_ClrEOL(int P_posx, int P_posy, int P_valeur)
{
  int V_lgstr = 0;
  int V_nb = 0;
  String V_val = String(P_valeur);
  if(P_posx >= 0 && P_posx < 20 && P_posy >= 0 && P_posy < 5)
  {
    V_lgstr = V_val.length();
    V_nb = 20 - P_posx - V_lgstr;
    if(V_nb > 0)
    {
      lcd.setCursor(P_posx + V_lgstr, P_posy);
      for(int i = 1; i <= V_nb; i++) {lcd.print(" ");}
    }
  }
} // Fin fonction ClrEOL

void F_RefreshLCD() // Mise à jour afficheur LCD
{
  lcd.setCursor(10,1);
  lcd.print(Vangcur);
  F_ClrEOL(10, 1, Vangcur);
  lcd.setCursor(10,2);
  if (VStatut == 0) {lcd.print("OK ");} else {lcd.print("NOK");}
  VUbat = F_Ubat(); // Lecture tension batterie
  lcd.setCursor(14,2);
  lcd.print("      ");
  lcd.setCursor(14,2);
  lcd.print(VUbat,2);
  lcd.print("V");
  lcd.setCursor(14,3);
  lcd.print("      ");
  lcd.setCursor(14,3);
  lcd.print(Vrtc.getTemperature());
} // Fin fonction F_RefreshLCD

/* La fonction F_MajTime lit l'horloge RTC et
effectue la mise à jour de la date et de l'heure à chaque seconde écoulée*/

void F_MajTime()
{
  static unsigned long VTempscour = millis();
  static unsigned long VTempsprec = VTempscour;  
  VHrcour = Vrtc.now(); // lecture date et heure courante
  VAnnee = VHrcour.year();   // initialisation avec la date courante
  VJour = VHrcour.day();
  VMois = VHrcour.month();
  VHrs = VHrcour.hour();
  VMin = VHrcour.minute();
  VSec = VHrcour.second();
  VTempscour = millis(); // Valeur courante compteur interne en millisecondes
  if(VTempscour - VTempsprec > CCliRythm) {VCliRythm = true;} else {VCliRythm = false;}
  if(VTempscour - VTempsprec >= 1000) // Si une seconde écoulée
  {
    VTempsprec = VTempscour;

    F_Faitligne(); // Préparation de l'affichage

    lcd.setCursor(0,0);
    lcd.print(VBuff);
  }
} // fin F_MajTime*/


/* Fonction F_in, cette fonction teste si l'argument Vms existe
 dans la liste d'arguements variable passée à la fonction, VNbe
 indique le nombre d'arguments passés. va_list, va_start, va_arg et va_end
 sont des macros définies dans stdarg.h
 Cette fonction fonctionne uniquement avec le type integer.
 La fonction renvoie un booléen si l'occurence est trouvée, mais on aurait pu
 aussi bien retourner le numéro de l'élément dans la liste des arguments*/

bool F_in(int Vms, int VNbe, ...)
{
  bool trouve = false;
  va_list(L_ms); // initialisation de la variable liste
  int Velem;
  va_start(L_ms, VNbe);

  do
  {
    Velem = va_arg(L_ms, int); // Retourne le prochain élément de la liste
    if(Velem == Vms){
      trouve = true;
      break;
    }
    VNbe -=1;
  } while(VNbe);
  va_end(L_ms);
  return(trouve);
} // fin F_in

/* Routine utilisée pour gérer un bouton poussoir, les fonctions sont :
    Bouton appuyé puis relâché moins de une seconde (appui court) renvoie 1
    Bouton appuyé puis relâché plus d'une seconde (appui long) renvoie 2
    Bouton resté appuyé et non relâché après une seconde, renvoie 3
    Autres situations renvoie 0
    */
int F_Pbut(int PInput)
{
  static int VEtatprec = LOW;
  int VEtatcur = LOW;
  static bool VFront = false; // Flag permettant de détecter un front montant ou descendant
  static bool VLong = false; // Flag indiquant un appui long
  int VRetour = 0;
  static unsigned long VCurti = millis(); // Valeur courante du compteur interne de l'Arduino
  static unsigned long VPrevti = VCurti; // Valeur précédente du compteur interne de l'Arduino
  static bool VFirst = true; // Flag permettant de détecter le premier appel à la fonction
  static int VNblec = 10; // Nombre de cycles répétitifs, utilisé pour le filtrage des rebonds
  int Vbout0 = HIGH;
  int Vbout1 = HIGH;

  if(VFirst) // Premier passage dans la fonction
  {
    VFirst = false;
    pinMode(PInput, INPUT_PULLUP); // Initialisation entrée bouton poussoir
  }
  Vbout1 = digitalRead(PInput); // Acquisition entrée du bouton
  if (Vbout1 == Vbout0) {VNblec -=1;} else // Filtrage rebonds
  {
    VNblec = 10;
    Vbout0 = Vbout1;
  }
  if (VNblec <= 0)
  {
    VNblec = 0;
    VEtatcur = Vbout1; // Validation état bouton après filtrage des rebonds
  }
  // if (VEtatcur == LOW) {digitalWrite(VD13, HIGH);} else {digitalWrite(VD13, LOW);} // Allumage builtin diode
  VCurti = millis(); // Actualisation temps courant
  if(VEtatcur == LOW) {VEtatcur = HIGH;} else {VEtatcur = LOW;} // Inversion du sens d'action du bouton (à cause de INPUT_PULLUP)
  if(VEtatcur != VEtatprec) // Changement d'état du bouton poussoir
  {
    VEtatprec = VEtatcur;
    VFront = true;
    if (VStlcd == false) // Si le LCD est éteint on le rallume
    {
      lcd.backlight();
      lcd.display();
      VStlcd = true;
    }
    VPrevti = VCurti;    
    if (VEtatcur == LOW) // Front descendant
    {
      if(VLong) {VRetour = 2;} else {VRetour = 1;}
      VFront = false;
      VLong = false;
    }
  } else if (VFront) // Pas de changement d'état mais front actif
  {
    if(VCurti - VPrevti > Ctpuls) // Bouton resté appuyé depuis plus d'une seconde
    {
      VLong = true;
      VRetour = 3;
    } 
    else
    {
      VRetour = 0;
    }
  }
  else {VRetour = 0;}
  return(VRetour);
} // Fin fonction F_Pbut

void F_Faitligne() // Contruction de la première ligne affichée sur l'écran LCD, Date et Heure
{
    VBuffidx = 0;
    VBuffidx += sprintf(VBuff + VBuffidx, "%02d", VJour);
    VBuffidx += sprintf(VBuff + VBuffidx, "%s", "/");
    VBuffidx += sprintf(VBuff + VBuffidx, "%02d", VMois);
    VBuffidx += sprintf(VBuff + VBuffidx, "%s", "/");
    VBuffidx += sprintf(VBuff + VBuffidx, "%2d", VAnnee);
    VBuffidx += sprintf(VBuff + VBuffidx, "  ");
    VBuffidx += sprintf(VBuff + VBuffidx, "%02d", VHrs);
    VBuffidx += sprintf(VBuff + VBuffidx, "%s", ":");
    VBuffidx += sprintf(VBuff + VBuffidx, "%02d", VMin);
    VBuffidx += sprintf(VBuff + VBuffidx, "%s", ":");
    VBuffidx += sprintf(VBuff + VBuffidx, "%02d", VSec);  
} // Fin fonction F_Faitligne

/* Cette fonction assure un mode de secours pour la mise à l'heure de l'horloge à l'aide du bouton
poussoir. Un appuis long sur le bouton fait entrer dans le mode réglage et affiche les jours en
clignotant sur l'écrans lcd. Un appuis court incrémente la valeur traitée de 1. Un nouvel appuis long
fait passer à la valeur suivante (mois, année, heures, minutes et secondes). Le mode réglage est quitté
si pas d'action sur le bouton pendant 3 secondes, et la nouvelle heure&date sont inscrites dans l'horloge
RTC. NB : l'action sur le bouton poussoir rallume l'écran LCD si celui-ci était éteint suite à un timeout.*/

void F_gestheure() 
{
  if (VReglage == false) {F_MajTime();} // On ne fait pas la mise à jour de l'heure pendant le réglage
  VResuBP = F_Pbut(CPBut); // Acquisition état du bouton poussoir
  if(VResuBP == 2 & VReglage == false)  // Activation du mode réglage si action longue sur le BP
  {
    VReglage = true;
    VStpReg =0;
    F_Serprintln("Réglage activé");
    F_Faitligne();
    F_Serprintln(VBuff);
    V4print = false;
    VExit = millis(); // Initialisation timeout pour sortie du mode réglage
    VcurX = 0;
    VcurY = 0;
    Vparam = VJour;
  }
  if(VResuBP == 0 & VReglage == true & VStpReg == 0) // On avance d'un cran quand le BP est relâché
  {
    VStpReg +=1;
    F_Serprintln("Réglage jours");
  }
  if(VResuBP == 1 & VReglage == true & VStpReg > 0 & Vprem == true) // Traitement impulsion courte sur le BP
  {
    Vprem = false;
    VExit = millis(); // Raz timeout
    switch (VStpReg)
    {
      case 1: // Jours
        VJour +=1;
        V4print = true;
        if(VJour == 31 & F_in(VMois,4,4, 6, 9, 11)) // cas du 30 pour les mois à 30 jour
        {
          VJour = 1;
        }
        else if(VJour == 32 & F_in(VMois,7,1, 3, 5, 7,8, 10,12)) // cas du 31 pour les mois à 31 jours
        {
          VJour = 1;
          if(VMois == 13)
          {
            VJour = 1;
          }
        }
        else if(VJour == 30 & VMois == 2 & VAnnee % 4 == 0) // cas de février pour les années bissextiles
        {
          VJour = 1;
        }
        else if(VJour == 29 & VMois == 2 & VAnnee % 4 > 0) // cas de février pour les années non bissextiles
        {
          VJour = 1;
        }
        Vparam = VJour;             
      break;
      case 2: // Mois
        VMois += 1;
        V4print = true;
        if(VMois == 13) {VMois = 1;}
        Vparam = VMois;
      break;
      case 3: // Annee
        VAnnee +=1;
        V4print = true;
        Vparam = VAnnee;        
      break;
      case 4: // Heures
        VHrs +=1;
        V4print = true;        
        if(VHrs == 24) {VHrs = 0;}
        Vparam = VHrs;
      break;
      case 5: // Minutes
        VMin += 1;
        V4print = true;        
        if(VMin == 60) {VMin = 0;}
        Vparam = VMin;
      break;
      case 6: // Secondes
        VSec += 1;
        V4print = true;        
        if(VSec == 60) { VSec = 0;}
        Vparam = VSec;
      break;
    }
  }
  if(VResuBP == 2 & VReglage == true & VStpReg > 0 & Vprem == true) // Traitement impulsion longue sur le BP
  {
    VStpReg +=1;
    VExit = millis(); // raz timeout
    if(VStpReg > 6) 
    {
      VStpReg =1;
    }
    switch ( VStpReg) // Mise à jour affichage
    {
      case 1:
      F_Serprintln("Réglage jours");
      VcurX = 0;
      Vparam = VJour;
      break;
      case 2:
      F_Serprintln("Réglage mois");
      VcurX =3;
      Vparam = VMois;
      break;
      case 3:
      F_Serprintln("Réglage années");
      VcurX = 6;
      Vparam = VAnnee;
      break;
      case 4:
      F_Serprintln("Réglage heures");
      VcurX = 12;
      Vparam = VHrs;
      break;
      case 5:
      F_Serprintln("Réglage minutes");
      VcurX = 15;
      Vparam = VMin;
      break;
      case 6:
      F_Serprintln("Réglage secondes");
      VcurX = 18;
      Vparam = VSec;
      break;                              
    }
    Vprem = false;
  }
  if(VResuBP == 0 & VReglage == true & VStpReg > 0 & Vprem == false) {Vprem = true;}
  if (V4print) // Affichage de la ligne en cas de changement d'une valeur
  {
    V4print = false;
  }
  if ((millis() - VExit > Ctpexit) & VReglage == true) // Sortie mode réglage après time out
  {
    VReglage = false;
    F_Serprintln("Sortie du mode réglage");
    Vrtc.adjust(DateTime(VAnnee, VMois, VJour, VHrs, VMin, VSec));
  }
} // Fin fonction V_Gestheure

// Fonction F_lcdmajreg, utilisée pour l'affichage en mode clignotant de la valeur en cours de réglage (mise à l'heure)
void F_lcdmajreg()
{
  static unsigned long VTpcur = 0;
  static unsigned long VTprec = 0;

  VTpcur = millis();
  if(VTpcur - VTprec > CCliRythm) 
  {
    VTprec = VTpcur;
    if (VCliRythm == false) {VCliRythm = true;} else {VCliRythm = false;}
    F_Serprint(String(VCliRythm));
  }
  lcd.setCursor(VcurX, VcurY);
  if (VCliRythm == false) {lcd.print("  ");} else {lcd.print(Vparam);}
} // Fin fonction F_lcdmajreg

// Fonction F_Timer attente d'un délais en ms, sans interruption du programme
void F_Timer(unsigned long Ptemps)
{
  static unsigned long VTimeP = millis();
  static unsigned long VTimeC;
  VTimeC = millis();
  if (VTimeC - VTimeP > Ptemps)
  {
    Vtemps = true;
    VTimeP = VTimeC;
  }
} // Fin fonction F_Timer

// Fonction F_Timer2 attente d'un délais en s, sans interruption du programme
void F_Timer2(unsigned long Ptemps)
{
  unsigned long VTjour; // nombre de secondes écoulées depuis minuit
  VHrcour = Vrtc.now(); // lecture date et heure courante
  VHrs = VHrcour.hour();
  VMin = VHrcour.minute();
  VSec = VHrcour.second();
  VTjour = (VHrs * 3600) + (VMin * 60) + VSec; // Calcul nombre de secondes depuis minuit
  static unsigned long VTimeP = VTjour;
  static unsigned long VTimeC;
  VTimeC = VTjour;
  if (VTjour == 0) { VTimeP =0;} // Passage de minuit
  if (VTimeC - VTimeP >= Ptemps)
  {
    Vtemps = true;
    VTimeP = VTimeC;
  }
} // Fin fonction F_Timer2

void F_TOlcd() // Fonction time out pour éteindre l'écran LCD
{
  static unsigned long VTp = millis(); // Initialisation temps courant et temps actuel
  static unsigned long VTc = VTp;
  VTc = millis();
  if (VTc - VTp > CTOlcd) // Le time out CTOlcd est dépassé
  {
    if (VStlcd == true)
    {
      lcd.noDisplay();
      lcd.noBacklight();
      VStlcd = false;
      VTp = VTc;  
    } 
  }
} // Fin fonction F_TOlcd

float F_Ubat() // Lecture de la tension d'alimentation VIN (soit, la batterie)
{
  int VA3;
  VA3 = analogRead(CEbat);
  return float(VA3) * CKu; // La valeur de CKu dépend des valeurs exactes du pont diviseur sur l'entrée CEbat
} // Fin fonction F_Ubat

void F_Serprint(String PTxt) // Remplace la fonction print et oriente la sortie soit ves le terminal soit vers Bluetooth
{
  VEtatBT = digitalRead(CEstate); // lecture état module Bluetooth (haut = connecté, bas = non connecté)
  if (VEtatBT == HIGH)
  {
    VBt.print(PTxt);
  } else
  {
    Serial.print(PTxt);
  }
} // Fin fonction F_Serprint

void F_Serprintln(String PTxt) // Remplace la fonction println et oriente la sortie soit vers le terminal soit vers Bluetooth
{
  VEtatBT = digitalRead(CEstate); // lecture état module Bluetooth (haut = connecté, bas = non connecté)
  if (VEtatBT == HIGH)
  {
    VBt.println(PTxt);
  } else
  {
    Serial.println(PTxt);
  }
} // Fin fonction F_Serprintln

int F_Seravailable() // Remplace la fonction Serial.available et prend soit l'entrée du  terminal soit celle de Bluetooth
{
  int VResu = 0;
  VEtatBT = digitalRead(CEstate); // lecture état module Bluetooth (haut = connecté, bas = non connecté)
  if (VEtatBT == HIGH)
  {
    VResu = VBt.available();
  } else
  {
    VResu = Serial.available();
  }
  return VResu;
} // Fin fonction F_Seravailable

String F_SerreadString() // Remplace la fonction Serial.readString et prend soit l'entrée du  terminal soit celle de Bluetooth
{
  String VResu = "";
  VEtatBT = digitalRead(CEstate); // lecture état module Bluetooth (haut = connecté, bas = non connecté)
  if (VEtatBT == HIGH)
  {
    VResu += VBt.readString();
  } else
  {
    VResu += Serial.readString();
  }
  return VResu;
} // Fin fonction F_SerreadString

void F_reboot() // Fonction reboot redémarre le contrôleur
{
  
  F_Serprintln("Reboot...");
  asm volatile ("jmp 0");
} // Fin fonction F_Reboot



merci du partage !

Tout à fait.

Dans le domaine, je suis juste un voyeur (en tout bien tout honneur :grinning:).

Le "voyeur" à "vu" l'agrandissement et l'amélioration d'une "ferme solaire".

Les premiers équipements étaient fixes en azimut et en hauteur.
Dans la cadre d'une optimisation, ces équipements ont été modifiés pour introduire un réglage manuel en hauteur.
Aucun réglage en azimut.

Dernièrement le nombre de panneaux a été doublé, il n'y a toujours pas de réglage en azimut, mais le réglage en hauteur a été automatisé.

"Voyeur" de base, je me dis que si les spécialistes des fermes solaires ne font toujours pas de réglage en azimut , c'est qu'ils trouvent que le réglage en azimut n'apporte pas suffisamment de gain de puissance par rapport au surcoût de fabrication et aussi de fonctionnement.

Ça tombe parce que c'est que je pense : le max de puissance, c'est quand le soleil est haut sur l'horizon, c'est à dire à midi solaire qui corespond l'hiver à 13 h à Paris, et environ 12h30 à Strasbourg et 13h30 à Brest.
Le matin et le soir, la lumière rasante est atténuée par la grande épaisseur de la couche de l'atmosphère qu'elle doit traverser.
Pourquoi avoir des frais de fonctionnement quand la lumière est faible ?
Autant n'optimiser que pour la position correspondant à la puissance est maximale.

J'ai lu des articles très intéressants sur la lumière diffuse.
Il y a deux types de rayonnement : le rayonnement direct et le rayonnement diffus.
Le rayonnement diffus trouve son origine soit dans des réflexions, soit dans un ciel nuageux, brumeux.
Point surprenant, la puissance de la luminosité diffuse peut être du même ordre de grandeur que le rayonnement direct.
Cela réduit l'intérêt du réglage en hauteur, qui dans l'exemple de la ferme solaire reste interressant, mais réduit quasiment à zéro l'intérêt du réglage en azimut.

Dernier point : Quelle précision pour le réglage adapté au rayonnement direct.
Le problème vu mathématiquement :
La puissance captée est proportionnelle à la surface du capteur effectivement "vu par le soleil".

  • Si la surface du capteur n'est pas perpendiculaire aux rayons du soleil la surface "effective" sera plus faible que la surface "mécanique".
  • Dans le cas où on ne fait un réglage que sur un axe (hauteur ou azimut c'est pareil) la surface "effective" est égale à la projection de la surface "mécanique" sur un plan perpendiculaire aux rayons du soleil.
    C'est-à-dire si alpha est l'angle d'erreur : S_effective = S_mécanique * cos(alpha).

Si je ne me suis pas planté :
Une erreur de 10° apporterait une perte d'efficacité de :
cos(10) = 0,9848
Soit une perte d'efficacité de 1,52 %
L'asservissement de position consommant de l'énergie, on peut améliorer le rendement en optimisant le délai entre deux repositionnements.

Au début j'avais envisagé de piloter le panneau sur deux axes, mais cela devenait trop compliqué. Je me suis donc contenté de la solution qui me paraissait la plus accessible compte tenu de mes moyens et compétences (notamment pour la soudure MMA, c'est dur dur). Enfin, j'ai mis le système en service depuis trois jours ça a l'air d'aller, mais il faut voir sur la durée ce que cela va donner. De plus je sais qu'il y aura un problème par vent fort et que le panneau déviera de sa position. Là encore, ajouter un système de frein est concevable mais assez compliqué pour un amateur.
Je rajoute deux photos que je n'avais pas pu charger du fait de la limitation pour les nouveaux inscrits...