Plateau tournant pour photogrammétrie et intervallomètre déclencheur d'appareil photo numérique

Bonjour à toute la communauté.

Je voudrais présenter ici un nouveau projet (presque) terminé. Je dis presque, car il manque des finitions, mais pour l’essentiel tout est là et fonctionne.

Il s’agit d’un appareil permettant de faire tourner un plateau d’un angle donné puis de déclencher un appareil photo numérique.

Le montage peut également être utilisé en simple intervallomètre sans plateau tournant, ou pour faire tourner l’appareil photo lui-même afin de faire un panoramique.

L’objet du plateau tournant, c’est de pouvoir prendre une série de photos d’un objet avec un décalage fixe de quelques degrés de manière automatique, dans le but de reconstituer un objet 3D à partir de ces photos. Cette technique s’appelle la photogrammétrie, et elle permet d’obtenir des résultats bluffants. Pour la reconstruction 3D j’utilise soit 3DF ZEFYR (Free avec une limitation à 50 vues), ou alors Meshroom qui est un logiciel libre sans limitation. Il faut néanmoins disposer d’une carte graphique avec un processeur NVidia et beaucoup de mémoire graphique (4 à 8Go).

La partie mécanique consiste en une base Arca Swiss installée sur un trépied équipé d’une rotule, sur cette base est fixée un moteur pas à pas (NEMA17) qui actionne un axe via un jeu de deux poulies crantées, amenant un rapport de démultiplication x3. L’axe est lui-même équipé d’une base Arca Swiss en partie haute afin d’y fixer le plateau ou un appareil photo.

Les commandes de mise au point et déclenchement sont effectuées via un câble de télécommande relié à des optocoupleurs.

Le système supporte les commandes suivantes :

?

Raz :

Focus : [Durée] {secondes}

Rideau : [Durée] {secondes}

TourneD : [Ang] : [Vit] : [AngMax] {degrés} {L/M/R} {degrés}

TourneG : [Ang] : [Vit] : [AngMax] {degrés} {L/M/R} {degrés}

Interval: [M/A] : [Durée] {secondes} : [Nombre intervals]

MA : [M/A]

Rapport :

Reboot :

Les valeurs par défaut sont :

Pas de rotation

Durée commande Focus = 3 secondes

Durée commande Déclenchement = 2 secondes

Vitesse de rotation = Moyenne

Sens de rotation = Droite

Intervallomètre inactif, Durée interval = 10 secondes

Nombre intervals = illimité

Etat marche = Arrêt

Le matériel utilisé :

L’appareil photo que j’utilise est un Nikon D750. Je ne sais pas si cela est identique pour tous les modèles, mais sur le D750 il faut activer la commande de mise au point (Focus) en même temps que la commande de déclenchement (Rideau) pour obtenir le déclenchement. Je donne cette précision car j’avais vu sur un Forum qu’un utilisateur n’arrivait pas à déclencher à partir d’un microcontrôleur, sans doute parce qu’il n’activait pas les deux sorties simultanément.

Le moteur pas à pas est un NEMA17, 4fils, 200 pas par tour,

Le microcontrôleur est un Arduino Nano Every. Attention, le programme ne peut pas être chargé dans un Arduino Uno car la taille de la mémoire dynamique est insuffisante.

Pour piloter l’ensemble j’ai installé un module Bluetooth, ce qui me permet d’envoyer des commandes depuis mon téléphone portable. Du coups, je ne sais pas si j’installerai les bouton-poussoir que j’avais prévu.

Le module de pilotage du moteur pas à pas est DRV 8825.

N’ayant pas trouvé facilement de connecteur Jack stéréo de 2,5mm pour circuit imprimé, j’ai opté pour un connecteur de 3,5mm et un petit câble adaptateur.

J’alimente le tout à partir d’une batterie 12V Li Ion.

Voici des photos de la carte et du montage mécanique. Le câblage de la carte n’est pas terrible car j’ai réutilisé un précédent montage et du coup c’est moins bien que si j’étais reparti de zéro.

Je mets aussi le programme et le schéma. 5si tout ne tien pas dans ce post, j’en ferai un second à la suite.



/* Programme Tete-Photo.ino
syph60 02/2025
Ce programme assure le fonctionnement d’un support pivotant et déclencheur pour appareil photo

*/

#include <Wire.h>
#include <stdio.h> 
#include <stdarg.h>
# 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 CBMA = 5; // N° entrée du bouton poussoir Marche/Arrêt
const int CBFoc = 6; // N° entrée du bouton poussoir Focus
const int CDecl = 7; // N° entrée du bouton poussoir Déclenchement
const int COfoc = 12; // N° sortie optocoupleur commande Focus
const int COdecl = 8; // N° sortie optocoupleur commande déclencheur
const int VD13 = 13; // N° entrée builtin diode
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
int Vsteps = 19200; // Variable nombre de pas pour 360 degrés
int Vstepdeg = 53; // Nombre de pas pour tourner d'un angle de 1,004464 degrés
int VmsDelay[4] = {0,2048,512,24}; // Variable durée impulsion moteur rotation Lente, Moyenne, Rapide
int VResSerial = 0; // Resultat fonction F_Commande
uint8_t VTfoc = 3; // Durée commande Focus, 0 = pas de focus
uint16_t VTdecl = 2; // Durée commande Déclenchement, 0 = pas de déclenchement
uint16_t VTang = 0; // Angle de rotation plateau, 0 = pas de rotation
uint8_t VVrot = 2; // Vitesse rotation plateau (0 pas de rotation,1 Lente,2 Moyenne,3 Rapide)
uint16_t Vtotrot = 0; // Cumul angle rotation
uint16_t VAngmax = 360; // Angle de rotation maxi
bool VSens = true; // Sens de rotation : true = droite, false = gauche
bool VInt = false; // Fonctionnement en mode intervallomètre false = Non, True = Oui
uint8_t VTint = 10; // Durée interval par défaut
uint16_t VNBint= 0; // Nombre d'intervals à exécuter, 0 indique pas de limites
bool VMA = false; // Etat marche/Arrêt
bool Vresaction = false; // Résultat action de commande
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

/* Fonctions :
Setup()
Loop()
F_Gole(int Pangle) Tourne à gauche d'un angle donné
F_Gori(int Pangle) Tourne à droite d'un angle donné
int F_Commande() Retourne le code de commande entré sur la liaison série
int F_Trouvestr(String PChn, String PChx, int PDeb) Recherche la chaine PChx dans PChn à partir de la position PDeb
String F_Trimall(String PChn) Supprimes tous les espaces dans PChn
bool F_in(int Vms, int VNbe, ...) Vérifie si la valeur Vms est un élément de la liste VNbe...
bool F_Bouton1() Renvoi l'état filtré du bouton poussoir Marche/Arrêt
bool F_Bouton2() Renvoi l'état filtré du bouton poussoir Focus
bool F_Bouton3() Renvoi l'état filtré du bouton poussoir Déclenchement
bool F_Timer(unsigned long Ptemps) attente d'un délais en ms, sans interruption du programme
float F_Ubat() Lecture de la tension d'alimentation VIN (soit, la batterie)
int F_Seravailable() Remplace la fonction Serial.available et prend soit l'entrée du  terminal soit celle de Bluetooth
void F_Serprintln(String PTxt) Remplace la fonction println et oriente la sortie soit vers le terminal soit vers Bluetooth
void F_Serprintln(String PTxt) Remplace la fonction println et oriente la sortie soit vers le terminal soit vers Bluetooth
String F_SerreadString() Remplace la fonction Serial.readString et prend soit l'entrée du  terminal soit celle de Bluetooth
void F_reboot() Fonction reboot redémarre le contrôleur
int F_Chrcount(String PChn, char PCar) Permet de compter le nombre d'occurences d'un caractère dans une chaine
String F_Strfield(String PChn, char PCar, int PDeb, int PNbe) Retourne la chaine délimitée par PDeb pour N occurences

*/

void setup() {
  Serial.begin(9600);
  VBt.begin(9600); // initialisation Bluetooth
  VBt.println("");
  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(CEstate, INPUT); // Entrée état module Bluetooth
  pinMode(COfoc, OUTPUT); // Sortie optocoupleur commande focus
  pinMode(COdecl, OUTPUT); // Sortie optocoupleur commande déclencheur
  pinMode(CBMA, INPUT_PULLUP); // Entrée bouton poussoir Marche/Arrêt
  pinMode(CBFoc, INPUT_PULLUP); // Entrée bouton poussoir Focus
  pinMode(CDecl, INPUT_PULLUP); //Entrée bouton poussoir Déclenchement
  delay(500);
  F_Aide();
} // Fin fonction setup

void loop()
{
  VResSerial = F_Commande();
  Vresaction = F_Action();

  if (VResSerial == 9) // Commande Reboot
  {
     F_reboot();  
  }  
} // Fin fonction loop



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);
  for(int x = 0; x < (P_angle * Vstepdeg); x++)
  {
    digitalWrite(CstepPin, HIGH);
    delayMicroseconds(VmsDelay[VVrot]);
    digitalWrite(CstepPin, LOW);
    delayMicroseconds(VmsDelay[VVrot]);
  }
  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

    for(int x = 0; x < (P_angle*Vstepdeg); x++)
  {
    digitalWrite(CstepPin, HIGH);
    delayMicroseconds(VmsDelay[VVrot]);
    digitalWrite(CstepPin, LOW);
    delayMicroseconds(VmsDelay[VVrot]);
  }
  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_Commande()
{
  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
      VInbuff.toUpperCase();
      VIdx = F_Trouvestr(VInbuff, "RAZ:", 0); // Vérification syntaxique indiquant une commande d'initialisation
      if (VIdx != -1)
      {
        VTang = 5; // Angle de rotation par défaut
        VTfoc = 3; // Durée par défaut commande focus = 3 secondes
        VTdecl = 2; // Durée par défaut commande déclenchement = 2 s
        VVrot = 2; // Vitesse de rotation moyenne
        VSens = true; // Sens de rotation à droite
        VInt = false; // Fonctionnement en mode intervallomètre désactivé
        VTint = 10; // Durée par défaut intervallomètre = 10 secondes
        VNBint = 0; // Nombre d'intervals = illimité
        VMA = false; // Etat Marche/Arrêt à l'arrêt
        VRet = 1;
        F_Serprintln("Fait commande Raz");
      }
      VIdx = F_Trouvestr(VInbuff, "RAPPORT:", 0); // Vérification syntaxique indiquant une commande de rapport
      if (VIdx != -1)
      {
        F_Serprintln("Réglages");
        F_Serprintln("--------");
        if (VTang > 0) {F_Serprintln("Angle de rotation = " + String(VTang));F_Serprintln("Angle maxi = " + String(VAngmax));} else {F_Serprintln("Pas de rotation");}
        if (VTfoc > 0) {F_Serprintln("Durée commande Focus = " + String(VTfoc));} else {F_Serprintln("Pas de Focus");}
        if (VTdecl >0) {F_Serprintln("Durée commande Déclenchement = " + String(VTdecl));} else {F_Serprintln("Pas de Déclenchement");} 
        switch ((int) VVrot)
        {
          case 0: {F_Serprintln("Vitesse de rotation = Pas de rotation"); break;}
          case 1: {F_Serprintln("Vitesse de rotation = Lente"); break;}
          case 2: {F_Serprintln("Vitesse de rotation = Moyenne"); break;}
          case 3: {F_Serprintln("Vitesse de rotation = Rapide"); break;}
        }
        if (VSens) {F_Serprintln("Sens de rotation = Droite");} else {F_Serprintln("Sens de rotation = Gauche");}
        if (VInt == false)  {F_Serprint("Intervallomètre inactif");} else {F_Serprint("Intervallomètre actif");}
        F_Serprintln(", Durée interval = " + String(VTint));
        if (VNBint == 0) {F_Serprintln("Nombre intervals = illimité");} else {F_Serprintln("Nombre intervals = " + String(VNBint));}
        if (VMA == false) {F_Serprintln("Etat marche = Arrêt");} else {F_Serprintln("Etat marche = Marche");}
        VRet = 2;
      }
      VIdx = F_Trouvestr(VInbuff, "FOCUS:", 0); // Vérification syntaxique indiquant une commande de Focus
      if (VIdx != -1)
      {
        if (F_Chrcount(VInbuff, ':') > 0)
        {
          VStr = F_Strfield(VInbuff, ':', 2,1);
          if(VStr != "") {VTfoc = (uint8_t) VStr.toInt();} else {VTfoc = 3;}
          if (VTfoc <1 || VTfoc > 10) VTfoc = 3; // Valeur par défaut si hors limites
        }
        VRet = 3;
        F_Serprintln("Fait commande Focus : " + String(VTfoc));
      }
      VIdx = F_Trouvestr(VInbuff, "RIDEAU:", 0); // Vérification syntaxique indiquant une commande de Focus
      if (VIdx != -1)
      {
        if (F_Chrcount(VInbuff, ':') > 0)
        {
          VStr = F_Strfield(VInbuff, ':', 2,1);
          if(VStr != "") {VTdecl = (uint16_t) VStr.toInt();} else {VTdecl = 2;}
          if (VTdecl <1 || VTdecl > 3600) VTdecl = 2; // Valeur par défaut si hors limites
        }
        VRet = 4;
        F_Serprintln("Fait commande Rideau : " + String(VTdecl));
      }
      VIdx = F_Trouvestr(VInbuff, "TOURNED:", 0); // Vérification syntaxique indiquant une commande de rotation à droite
      if (VIdx != -1)
      {
        if (F_Chrcount(VInbuff, ':') > 0)
        {
          VStr = F_Strfield(VInbuff, ':', 2,1);
          if(VStr != "") {VTang = (uint16_t) VStr.toInt();} else {VTang = 5;}
        }
        {
          VStr = F_Strfield(VInbuff, ':', 3,1);
          switch (F_Toasc(VStr))
          {
            case 76: { VVrot = 1; break;}
            case 77: { VVrot = 2; break;}
            case 82: { VVrot = 3; break;}
            default: {VVrot = 0; break;}
          }
          VStr = F_Strfield(VInbuff, ':', 4,1);
          if (VStr != "") VAngmax = (uint16_t) VStr.toInt(); // Angle de rotation maximum
        }
        VRet = 5;
        VSens = true;
        F_Serprintln("Fait commande TourneD : " + String(VTang) + ", Vitesse : " + String(VVrot) + ", Sens : " + VSens + ", Angle maxi : " + String(VAngmax));
      }
      VIdx = F_Trouvestr(VInbuff, "TOURNEG:", 0); // Vérification syntaxique indiquant une commande de rotation à droite
      if (VIdx != -1)
      {
        if (F_Chrcount(VInbuff, ':') > 0)
        {
          VStr = F_Strfield(VInbuff, ':', 2,1);
          if(VStr != "") {VTang = (uint16_t) VStr.toInt();} else {VTang = 5;}
        }
        {
          VStr = F_Strfield(VInbuff, ':', 3,1);
          switch (F_Toasc(VStr))
          {
            case 76: { VVrot = 1; break;}
            case 77: { VVrot = 2; break;}
            case 82: { VVrot = 3; break;}
            default: {VVrot = 0; break;}

          }
          VStr = F_Strfield(VInbuff, ':', 4,1);
          if (VStr != "") VAngmax = (uint16_t) VStr.toInt(); // Angle de rotation maximum          
        }
        VRet = 6;
        VSens = false;
        F_Serprintln("Fait commande TourneG : " + String(VTang) + ", Vitesse : " + String(VVrot) + ", Sens : " + VSens + ", Angle maxi : " + String(VAngmax));
      }
      VIdx = F_Trouvestr(VInbuff, "MA:", 0); // Vérification syntaxique indiquant une commande Marche/Arrêt
      if (VIdx != -1)
      {
        if (F_Chrcount(VInbuff, ':') > 0)
        {
          VStr = F_Strfield(VInbuff, ':', 2,1);
          if(VStr.startsWith("M")) 
          {
            VMA = true;
            F_Serprintln("Fait commande Marche");
            if (VTang > 0) {Vtotrot = 0;}
          }
          if(VStr.startsWith("A")) 
          {
            VMA = false;
            F_Serprintln("Fait commande Arrêt");
            digitalWrite(COfoc, LOW); // Désactivation sortie Focus
            digitalWrite(COdecl, LOW); // Désactivation sortie déclencheur
            digitalWrite(CEnPin, HIGH); // On éteint le moteur
          }
        }
        VRet = 7;
      }                                       
      VIdx = F_Trouvestr(VInbuff, "INTERVAL:", 0); // Vérification syntaxique indiquant le mode interval
      if (VIdx != -1)
      {
        F_Serprint("Fait commande Intervallomètre : ");
        if (F_Chrcount(VInbuff, ':') > 0)
        {
          VStr = F_Strfield(VInbuff, ':', 2,1);
          if (VStr.startsWith("M"))
          {
            VInt = true;
            F_Serprint("Marche");
          }
          if (VStr.startsWith("A"))
          {
            VInt = false;
            F_Serprint("Arrêt");
          }          
          VStr = F_Strfield(VInbuff, ':', 3,1);
          if(VStr != "") {VTint = (uint8_t) VStr.toInt();} else {VTint = 10;}
          F_Serprint(", Délai = " + String(VTint) + "s");
          VStr = F_Strfield(VInbuff, ':', 4, 1);
          if(VStr != "") {VNBint = (uint16_t) VStr.toInt();} else {VNBint = 0;}
        }
        VRet = 8;
        F_Serprintln(", Nombre intervals = " + String(VNBint));
      }
      VIdx = F_Trouvestr(VInbuff, "?", 0); // Vérification syntaxique indiquant une commande Help
      if (VIdx != -1)
      {
        F_Aide();
      }      
      VIdx = F_Trouvestr(VInbuff, "REBOOT:", 0); // Vérification syntaxique indiquant une commande Reboot
      if (VIdx != -1)
      {
        VRet = 9;
      }                 
    }
  }
  return VRet;
} // Fin fonction F_Commande

/* 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



/* 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

/* Routines utilisées pour gérer les bouton poussoirs, retourne true ou false selon que les BP sont
appuyés ou relâchés
    */
bool F_Bouton1()
{
  int VEtatcur = LOW;
  bool VRetour = false;
  static int VNblec = 10; // Nombre de cycles répétitifs, utilisé pour le filtrage des rebonds
  static int Vbout0 = HIGH;
  static int Vbout1 = HIGH;

  Vbout1 = digitalRead(CBMA); // 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) {VRetour = true;} else {VRetour = false;} // Inversion du sens d'action du bouton (à cause de INPUT_PULLUP)
  return(VRetour);
} // Fin fonction F_Bouton1

bool F_Bouton2()
{
  int VEtatcur = LOW;
  bool VRetour = false;
  static int VNblec = 10; // Nombre de cycles répétitifs, utilisé pour le filtrage des rebonds
  static int Vbout0 = HIGH;
  static int Vbout1 = HIGH;

  Vbout1 = digitalRead(CBFoc); // 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) {VRetour = true;} else {VRetour = false;} // Inversion du sens d'action du bouton (à cause de INPUT_PULLUP)
  return(VRetour);
} // Fin fonction F_Bouton2

bool F_Bouton3()
{
  int VEtatcur = LOW;
  bool VRetour = false;
  static int VNblec = 10; // Nombre de cycles répétitifs, utilisé pour le filtrage des rebonds
  static int Vbout0 = HIGH;
  static int Vbout1 = HIGH;

  Vbout1 = digitalRead(CDecl); // 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) {VRetour = true;} else {VRetour = false;} // Inversion du sens d'action du bouton (à cause de INPUT_PULLUP)
  return(VRetour);
} // Fin fonction F_Bouton2

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

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

// Permet de compter le nombre d'occurences d'un caractère dans une chaine
int F_Chrcount(String PChn, char PCar)
{
  int Vresu = 0;
  int VLong = PChn.length();
  for (int i =0; i < VLong; i++)
  {
    if (PChn[i] == PCar) {Vresu++;}
  }
  return Vresu;
} // Fin F_Chrcount

/* La fonction F_StrField permet d'extraire une partie de chaine en
 fonction de caractères de délimitation.
 La syntaxe est F_Strfield(Chaine,car,deb,nbr)

   Chaine  = chaine dans laquelle on effectue la recherche
   car     = caractère de délimitation des éléments recherchés
   deb     = Occurence de départ
   nbr     = nombre d'occurences


   Exemples :

       F_Strfield("ABC","|",1,1) = ABC
       F_Strfield("ABC|DEF|GHI","|",1,1) = ABC
       F_Strfield("ABC|DEF|GHI","|",2,1) = DEF
       F_Strfield("ABC|DEF|GHI","|",1,2) = ABC|DEF
       F_Strfield("ABC|DEF|GHI","|",2,2) = DEF|GHI
       F_Strfield("ABC|DEF|GHI","|",2,5) = DEF|GHI
*/

String F_Strfield(String PChn, char PCar, int PDeb, int PNbe)
{
  String VResu = "";
  int VStart, Vidx, Vposition;
  int Vdeb = 0;
  int Vfin = 0;
  String VChn2 = "";
  if (PChn.length() == 0  || PDeb < 1 || PNbe < 1)
  {
    return VResu;
  }
  else
  {
   Vidx = 0;
   VStart = 0;
   Vposition = -1;
    while(Vidx < (PDeb -1))
    {  
      Vdeb = Vposition +1;
      Vposition = PChn.indexOf(PCar, VStart);
      if (Vposition == -1)
      {
        return VResu;
      }
      else
      {
        Vidx+=1;
        VStart = Vposition +1;
      }
    }
    VChn2 =PChn.substring(VStart);
    //if (PDeb == 1) {Vdeb = 0;} else {Vdeb = Vposition + 1;}
    Vidx = 0;
    VStart = 0;
    while(Vidx < PNbe)
    {
      Vposition = VChn2.indexOf(PCar, VStart);
      if (Vposition != -1)
      {
        Vidx +=1;
        //VStart = Vposition +1;
        //Vfin = Vposition;
        if (Vidx == PNbe) {return VChn2.substring(0,Vposition);} else {VStart = Vposition +1;}
      }
      else
      {
        return VChn2;
      }  
    }
    return PChn.substring(Vdeb, Vfin);
  }
} // Fin fonction F_Strfield

int F_Toasc(String PChn) // Retourne l'entier représentant la valeur ASCII du premier caratère de PChn
{
  int Vresu = 0;
  if (PChn != "") Vresu = (int) PChn[0];
  return Vresu;
}

bool F_Action() // Exécution des commandes
{
  bool Vresu;
  static unsigned long Vtmppre = millis();
  static unsigned long Vtmpcur = millis();
  static unsigned long Vdelai = 0;
  Vresu = false;
  Vtmpcur = millis();
  if (Vdelai > 0 && (Vtmpcur - Vtmppre) > Vdelai) // Temps écoulé
  {
    Vdelai = 0;
    Vtmppre = Vtmpcur;
  }
  if (VInt == true && VMA == true && Vdelai == 0) // Mode interval et Marche actif et interval non en cours
  {
    F_Serprintln("Intervallomètre : Déclenchement");
    digitalWrite(COfoc, HIGH); // Activation sortie Focus
    Vresu = true;
    if(VTfoc > 0) // Temps focus non nul
    {
      delay(VTfoc * 1000); // Attente du délai pour réglage mise au point
    }
    if(VTdecl > 0) // On enchaine sur le déclenchement
    {
      digitalWrite(COdecl, HIGH); // Activation sortie déclencheur
      delay(VTdecl * 1000); // Attente du temps d'opération du déclencheur
    }
    digitalWrite(COfoc, LOW); // Désactivation sortie Focus
    digitalWrite(COdecl, LOW); // Désactivation sortie déclencheur
    Vdelai = VTint * 1000; // Temps interval passé en millisecondes
    Vtmpcur = millis();
  }
  else if (VInt == false && VMA == true && Vdelai == 0) // Mode interval inactif et Marche et interval non en cours
  {
    if (VTang > 0) {F_Serprintln("Rotation : Déclenchement/Rotation");} else {F_Serprintln("Déclenchement unique");};
    if (VTfoc > 0) // Temps focus non nul
    {
      digitalWrite(COfoc, HIGH); // Activation sortie Focus
      delay(VTfoc * 1000); // Attente du délai pour réglage mise au point
    }
    if(VTdecl > 0) // On enchaine sur le déclenchement
    {
      digitalWrite(COdecl, HIGH); // Activation sortie déclencheur
      delay(VTdecl * 1000); // Attente du temps d'opération du déclencheur
    }
    digitalWrite(COfoc, LOW); // Désactivation sortie Focus
    digitalWrite(COdecl, LOW); // Désactivation sortie déclencheur
    if (VTang > 0 && VVrot > 0) // Rotation active
    {
      if (VSens == true) {F_Gori(VTang);} else {F_Gole(VTang);} // commande rotation
      Vtotrot += VTang;
      F_Serprintln("Rotation totale : " + String(Vtotrot)); // Affichage rotation totale
      if (Vtotrot >= VAngmax) 
      {
        VMA = false; // Arrêt commande
        Vtotrot = 0;
        F_Serprintln("Pret");
      }
    }
    Vdelai = 1000; // Attente de 1 seconde pour stabilisation
    Vtmpcur = millis();
    if (VTang == 0)  // Si pas de rotation on arrête l'exécution
    { 
      VMA = false;
      F_Serprintln("Pret");
    }
  }
  return Vresu;
} // Fin fonction F_Action

void F_Aide()
{
  F_Serprintln("   Commandes disponibles :");
  F_Serprintln("   -----------------------");
  F_Serprintln("?");
  F_Serprintln("Raz :");
  F_Serprintln("Focus   : [Durée] {secondes}");
  F_Serprintln("Rideau  : [Durée] {secondes}");
  F_Serprintln("TourneD : [Ang] : [Vit] : [AngMax] {degrés} {L/M/R} {degrés}");
  F_Serprintln("TourneG : [Ang] : [Vit] : [AngMax] {degrés} {L/M/R} {degrés}");
  F_Serprintln("Interval: [M/A] : [Durée] {secondes} : [Nombre intervals]");
  F_Serprintln("MA      : [M/A]");
  F_Serprintln("Rapport :");
  F_Serprintln("Reboot  :");
    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é");}
  VUbat = F_Ubat();
  F_Serprintln("Tension batterie = " + String(VUbat) + "V");
} // Fin fonction F_Aide

Il y aura des améliorations à faire, j'en vois déjà une qui consiste à ajouter une commande pause pour définir le temps nécessaire à la stabilisation après rotation du plateau ou de l'appareil photo...

Merci du partage.

Vous pourriez prévoir de ne pas déclencher la mise au point. Souvent quand on fait ce genre de montage, on travaille en mode manuel (exposition, profondeur de champ, mise au point, ...) pour éviter tout risque.

Vous pourriez aussi prévoir de déclencher des flash (une boîte avec diffuseurs autour du plateau) lors de la prise de vue pour un éclairage stable

Bonjour, merci de ces remarques. En effet, j'avais bien prévu de désactiver l'autofocus et le déclenchement, mais j'ai rajouté une ligne pour vérifier les limites de la valeur entrée et j'ai exclu le zéro (par erreur). Il faut juste modifier les lignes 199 et 211, et remplacer : if (VTfoc <1 || VTfoc > 10), par if (VTfoc <0 || VTfoc > 10). Après en entrant la commande focus : 0, ou rideau : 0 on désactive l'autofocus et/ou le déclenchement. Pour mes essais, j'ai juste désactivé l'autofocus sur l'appareil.
En ce qui concerne le déclenchement du flash, je n'y avais pas pensé. Cela dit, j'utilise le système de déclenchement multiflash Nikon, et donc mes flashs sont commandés par l'appareil photo. J'ai déjà fait une série de photo pour de la reconstruction 3D avec un flash dans une softbox, cela donne de bons résultats. Personnellement je préfère travailler avec un éclairage fixe, bien que ce ne soit pas toujours facile, car pour la photogrammétrie, il faut une grande profondeur de champ et peu de bruit.

OK oui c'est une solution aussi

Beau boulot!

Peut-être retoucher la mise en page pour rendre la lecture plus agréable, en particulier la liste des commandes et des valeurs par défaut (voir le lien ci-dessous)

J'ai apporté quelques modifications au système :
Changement du moteur pour un modèle plus puissant afin de supporter la rotation
de l'appareil photo avec un objectif un peu lourd (cela coinçait avant).

  • Sur le schéma j'ai ajouté la borne Gnd Logic que j'avais oublié de représenter.

  • J'ai ajouté une fonction Pause pour permettre à l'appareil photo de refroidir entre deux déclenchements, lors d'une grande série de prises de vues.

  • J'ai mis l'électronique dans une boite, et fait une protection pour le mécanisme de protection.

Il y a deux points que je voudrais signaler, car je me suis fait avoir...

Les moteurs pas à pas standards (made in China) ne sont pas étanches, et donc il faut absolument éviter les milieux comportant de la limaille de fer, sinon elle rentre dans le moteur et le bloque. Il n'y a plus qu'à tout démonter et nettoyer à l'air comprimé.
Pour le second moteur, j'ai ajouté une rondelle en plastique au niveau de la sortie de l'axe moteur, percée juste au diamètre avec un emporte pièces. Cela augmente l'étanchéité à la poussière.

Le Driver de moteur que j'ai utilisé est fourni avec un fascicule de mise en oeuvre. Seulement la figure qui montre la position des bornes du circuit est mal orientée (180°) et du coup, il faut se fier à la sérigraphie sur le circuit imprimé, sinon le circuit par en fumée.

Enfin, je n'ai toujours pas installé les boutons poussoirs que j'avais prévus au départ, et ça ne pose pas de problème, tout est pilotable depuis Androïd.

Je rajoute une mise à jour du schéma et du programme, ainsi qu'une photo de l'ensemble.
Schm tete-photo.pdf (189,8 Ko)

/* Programme Tete-Photo.ino
syph60 02/2025
V2, 03/2025
Ce programme assure le fonctionnement d’un support pivotant et déclencheur pour appareil photo

*/

#include <Wire.h>
#include <stdio.h> 
#include <stdarg.h>
# 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 CBMA = 5; // N° entrée du bouton poussoir Marche/Arrêt
const int CBFoc = 6; // N° entrée du bouton poussoir Focus
const int CDecl = 7; // N° entrée du bouton poussoir Déclenchement
const int COfoc = 12; // N° sortie optocoupleur commande Focus
const int COdecl = 8; // N° sortie optocoupleur commande déclencheur
const int VD13 = 13; // N° entrée builtin diode
const int CEbat = A3 ; // Entrée analogique A3 pour tension batterie
const float CKu = 0.014956; // 5*(7850 + 4656) / (4656 * 1024), Coefficient pour lecture tension batterie
const int Crampe[] = {2712,2086,1605,1234,949,730,562,432,332,255,196,151,116,89,69};
const String CLibV[] = {"Aucune", "Lente", "Moyenne", "Rapide"}; // Libellé affiché pour la vitesse de rotation
// Fin section constantes

// Variables
int Vsteps = 19200; // Variable nombre de pas pour 360 degrés
int Vstepdeg = 53; // Nombre de pas pour tourner d'un angle de 1,004464 degrés
int VmsDelay[4] = {0,2048,512,53}; // Variable durée impulsion moteur rotation Lente, Moyenne, Rapide
int VResSerial = 0; // Resultat fonction F_Commande
uint8_t VTfoc = 3; // Durée commande Focus, 0 = pas de focus
uint16_t VTdecl = 2; // Durée commande Déclenchement, 0 = pas de déclenchement
uint16_t VTang = 0; // Angle de rotation plateau, 0 = pas de rotation
uint8_t VVrot = 2; // Vitesse rotation plateau (0 pas de rotation,1 Lente,2 Moyenne,3 Rapide)
uint16_t Vtotrot = 0; // Cumul angle rotation
uint16_t VAngmax = 360; // Angle de rotation maxi
bool VSens = true; // Sens de rotation : true = droite, false = gauche
bool VInt = false; // Fonctionnement en mode intervallomètre false = Non, True = Oui
uint8_t VTint = 10; // Durée interval par défaut
uint16_t VNBint = 0; // Nombre d'intervals à exécuter, 0 indique pas de limites
uint16_t VIntE = 0; // Nombre d'intervals exécutés
uint8_t VPause = 0; // Pause entre rotation et déclenchement en secondes
bool VMA = false; // Etat marche/Arrêt
bool Vresaction = false; // Résultat action de commande
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

/* Fonctions :
Setup()
Loop()
F_Gole(int Pangle) Tourne à gauche d'un angle donné
F_Gori(int Pangle) Tourne à droite d'un angle donné
int F_Commande() Retourne le code de commande entré sur la liaison série
int F_Trouvestr(String PChn, String PChx, int PDeb) Recherche la chaine PChx dans PChn à partir de la position PDeb
String F_Trimall(String PChn) Supprimes tous les espaces dans PChn
bool F_in(int Vms, int VNbe, ...) Vérifie si la valeur Vms est un élément de la liste VNbe...
bool F_Bouton1() Renvoi l'état filtré du bouton poussoir Marche/Arrêt
bool F_Bouton2() Renvoi l'état filtré du bouton poussoir Focus
bool F_Bouton3() Renvoi l'état filtré du bouton poussoir Déclenchement
bool F_Timer(unsigned long Ptemps) attente d'un délais en ms, sans interruption du programme
float F_Ubat() Lecture de la tension d'alimentation VIN (soit, la batterie)
int F_Seravailable() Remplace la fonction Serial.available et prend soit l'entrée du  terminal soit celle de Bluetooth
void F_Serprintln(String PTxt) Remplace la fonction println et oriente la sortie soit vers le terminal soit vers Bluetooth
void F_Serprintln(String PTxt) Remplace la fonction println et oriente la sortie soit vers le terminal soit vers Bluetooth
String F_SerreadString() Remplace la fonction Serial.readString et prend soit l'entrée du  terminal soit celle de Bluetooth
void F_reboot() Fonction reboot redémarre le contrôleur
int F_Chrcount(String PChn, char PCar) Permet de compter le nombre d'occurences d'un caractère dans une chaine
String F_Strfield(String PChn, char PCar, int PDeb, int PNbe) Retourne la chaine délimitée par PDeb pour N occurences

*/

void setup() {
  Serial.begin(9600);
  VBt.begin(9600); // initialisation Bluetooth
  VBt.println("");
  pinMode(CstepPin, OUTPUT); // Sortie STEP
  pinMode(CdirPin, OUTPUT); // Sortie sens de rotation
  pinMode(CEnPin, OUTPUT); // Sortie Enable
  digitalWrite(CdirPin, LOW); // sélection sens de rotation
  digitalWrite(CstepPin, LOW); // On éteint le moteur
  digitalWrite(CEnPin, LOW); // On éteint le moteur
  pinMode(CEstate, INPUT); // Entrée état module Bluetooth
  pinMode(COfoc, OUTPUT); // Sortie optocoupleur commande focus
  pinMode(COdecl, OUTPUT); // Sortie optocoupleur commande déclencheur
  pinMode(CBMA, INPUT_PULLUP); // Entrée bouton poussoir Marche/Arrêt
  pinMode(CBFoc, INPUT_PULLUP); // Entrée bouton poussoir Focus
  pinMode(CDecl, INPUT_PULLUP); //Entrée bouton poussoir Déclenchement
  delay(500);
  F_Gole(2); // Initialisation driver moteur
  F_Gori(2);
  F_Aide();
} // Fin fonction setup

void loop()
{
  VResSerial = F_Commande();
  Vresaction = F_Action();

  if (VResSerial == 9) // Commande Reboot
  {
     F_reboot();  
  }  
} // Fin fonction loop



void F_Gole(int P_angle)
{ // Commande du panneau vers la gauche d'un angle donné en paramètre
  int Vdel = 0; // Délai impulsion
  int Vmaxa =0; // Angle maxi
  digitalWrite(CEnPin, LOW);
  digitalWrite(CdirPin, HIGH);
  Vmaxa = P_angle * Vstepdeg;
  for(int x = 0; x < Vmaxa; x++)
  {
    if (VVrot == 3 && x < 15) { Vdel = Crampe[x];} else if( VVrot == 3 && x > (Vmaxa -15)) 
        {Vdel = Crampe[Vmaxa - (x+1)];} else {Vdel = VmsDelay[VVrot];} // On applique une rampe au mode rapide
    digitalWrite(CstepPin, HIGH);
    delayMicroseconds(Vdel);
    digitalWrite(CstepPin, LOW);
    delayMicroseconds(Vdel);
  }
  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
  int Vdel = 0; // Délai impulsion
  int Vmaxa =0; // Angle maxi
  digitalWrite(CEnPin, LOW); // activation moteur
  digitalWrite(CdirPin, LOW); // sélection sens de rotation
  Vmaxa = P_angle * Vstepdeg;
    for(int x = 0; x < Vmaxa; x++)
  {
    if (VVrot == 3 && x < 15) { Vdel = Crampe[x];} else if( VVrot == 3 && x > (Vmaxa -15)) 
        {Vdel = Crampe[Vmaxa - (x+1)];} else {Vdel = VmsDelay[VVrot];} // On applique une rampe au mode rapide
    digitalWrite(CstepPin, HIGH);
    delayMicroseconds(Vdel);
    digitalWrite(CstepPin, LOW);
    delayMicroseconds(Vdel);
  }
  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_Commande()
{
  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
      VInbuff.toUpperCase();
      VIdx = F_Trouvestr(VInbuff, "RAZ:", 0); // Vérification syntaxique indiquant une commande d'initialisation
      if (VIdx != -1)
      {
        VTang = 5; // Angle de rotation par défaut
        VTfoc = 3; // Durée par défaut commande focus = 3 secondes
        VTdecl = 2; // Durée par défaut commande déclenchement = 2 s
        VVrot = 2; // Vitesse de rotation moyenne
        VSens = true; // Sens de rotation à droite
        VInt = false; // Fonctionnement en mode intervallomètre désactivé
        VTint = 10; // Durée par défaut intervallomètre = 10 secondes
        VNBint = 0; // Nombre d'intervals = illimité
        VMA = false; // Etat Marche/Arrêt à l'arrêt
        VRet = 1;
        F_Serprintln("Fait commande Raz");
      }
      VIdx = F_Trouvestr(VInbuff, "RAPPORT:", 0); // Vérification syntaxique indiquant une commande de rapport
      if (VIdx != -1)
      {
        F_Serprintln("Réglages");
        F_Serprintln("--------");
        if (VTang > 0) {F_Serprintln("Angle de rotation = " + String(VTang));F_Serprintln("Angle maxi = " + String(VAngmax));} else {F_Serprintln("Pas de rotation");}
        if (VTfoc > 0) {F_Serprintln("Durée commande Focus = " + String(VTfoc));} else {F_Serprintln("Pas de Focus");}
        if (VTdecl >0) {F_Serprintln("Durée commande Déclenchement = " + String(VTdecl));} else {F_Serprintln("Pas de Déclenchement");} 
        switch ((int) VVrot)
        {
          case 0: {F_Serprintln("Vitesse de rotation = Pas de rotation"); break;}
          case 1: {F_Serprintln("Vitesse de rotation = Lente"); break;}
          case 2: {F_Serprintln("Vitesse de rotation = Moyenne"); break;}
          case 3: {F_Serprintln("Vitesse de rotation = Rapide"); break;}
        }
        if (VPause >0) {F_Serprintln("Pause avant déclenchement = " + String(VPause));} else {F_Serprintln("Pas de pause");}
        if (VSens) {F_Serprintln("Sens de rotation = Droite");} else {F_Serprintln("Sens de rotation = Gauche");}
        if (VInt == false)  {F_Serprint("Intervallomètre inactif");} else {F_Serprint("Intervallomètre actif");}
        F_Serprintln(", Durée interval = " + String(VTint));
        if (VNBint == 0) {F_Serprintln("Nombre intervals = illimité");} else {F_Serprintln("Nombre intervals = " + String(VNBint));}
        if (VMA == false) {F_Serprintln("Etat marche = Arrêt");} else {F_Serprintln("Etat marche = Marche");}
        F_Serprintln("Tension batterie = " + String(F_Ubat()) + "V");
        VRet = 2;
      }
      VIdx = F_Trouvestr(VInbuff, "FOCUS:", 0); // Vérification syntaxique indiquant une commande de Focus
      if (VIdx != -1)
      {
        if (F_Chrcount(VInbuff, ':') > 0)
        {
          VStr = F_Strfield(VInbuff, ':', 2,1);
          if(VStr != "") {VTfoc = (uint8_t) VStr.toInt();} else {VTfoc = 3;}
          if (VTfoc <0 || VTfoc > 10) VTfoc = 3; // Valeur par défaut si hors limites
        }
        VRet = 3;
        F_Serprintln("Fait commande Focus : " + String(VTfoc));
      }
      VIdx = F_Trouvestr(VInbuff, "RIDEAU:", 0); // Vérification syntaxique indiquant une commande de Focus
      if (VIdx != -1)
      {
        if (F_Chrcount(VInbuff, ':') > 0)
        {
          VStr = F_Strfield(VInbuff, ':', 2,1);
          if(VStr != "") {VTdecl = (uint16_t) VStr.toInt();} else {VTdecl = 2;}
          if (VTdecl <0 || VTdecl > 3600) VTdecl = 2; // Valeur par défaut si hors limites
        }
        VRet = 4;
        F_Serprintln("Fait commande Rideau : " + String(VTdecl));
      }
      VIdx = F_Trouvestr(VInbuff, "PAUSE:", 0); // Vérification syntaxique indiquant une commande de Focus
      if (VIdx != -1)
      {
        if (F_Chrcount(VInbuff, ':') > 0)
        {
          VStr = F_Strfield(VInbuff, ':', 2,1);
          if(VStr != "") {VPause = (uint16_t) VStr.toInt();} else {VPause = 0;}
          if (VPause <0 || VPause > 300) VPause = 0; // Valeur par défaut si hors limites
        }
        VRet = 4;
        F_Serprintln("Fait commande Pause : " + String(VPause));
      }
      VIdx = F_Trouvestr(VInbuff, "TOURNED:", 0); // Vérification syntaxique indiquant une commande de rotation à droite
      if (VIdx != -1)
      {
        if (F_Chrcount(VInbuff, ':') > 0)
        {
          VStr = F_Strfield(VInbuff, ':', 2,1);
          if(VStr != "") {VTang = (uint16_t) VStr.toInt();} else {VTang = 5;}
        }
        {
          VStr = F_Strfield(VInbuff, ':', 3,1);
          switch (F_Toasc(VStr))
          {
            case 76: { VVrot = 1; break;}
            case 77: { VVrot = 2; break;}
            case 82: { VVrot = 3; break;}
            default: {VVrot = 0; break;}
          }
          VStr = F_Strfield(VInbuff, ':', 4,1);
          if (VStr != "") VAngmax = (uint16_t) VStr.toInt(); // Angle de rotation maximum
        }
        VRet = 5;
        VSens = true;
        F_Serprintln("Fait commande TourneD : " + String(VTang) + ", Vitesse : " + CLibV[VVrot] + ", Sens : " + VSens + ", Angle maxi : " + String(VAngmax));
      }
      VIdx = F_Trouvestr(VInbuff, "TOURNEG:", 0); // Vérification syntaxique indiquant une commande de rotation à droite
      if (VIdx != -1)
      {
        if (F_Chrcount(VInbuff, ':') > 0)
        {
          VStr = F_Strfield(VInbuff, ':', 2,1);
          if(VStr != "") {VTang = (uint16_t) VStr.toInt();} else {VTang = 5;}
        }
        {
          VStr = F_Strfield(VInbuff, ':', 3,1);
          switch (F_Toasc(VStr))
          {
            case 76: { VVrot = 1; break;}
            case 77: { VVrot = 2; break;}
            case 82: { VVrot = 3; break;}
            default: {VVrot = 0; break;}

          }
          VStr = F_Strfield(VInbuff, ':', 4,1);
          if (VStr != "") VAngmax = (uint16_t) VStr.toInt(); // Angle de rotation maximum          
        }
        VRet = 6;
        VSens = false;
        F_Serprintln("Fait commande TourneG : " + String(VTang) + ", Vitesse : " + CLibV[VVrot] + ", Sens : " + VSens + ", Angle maxi : " + String(VAngmax));
      }
      VIdx = F_Trouvestr(VInbuff, "MA:", 0); // Vérification syntaxique indiquant une commande Marche/Arrêt
      if (VIdx != -1)
      {
        if (F_Chrcount(VInbuff, ':') > 0)
        {
          VStr = F_Strfield(VInbuff, ':', 2,1);
          if(VStr.startsWith("M")) 
          {
            VMA = true;
            F_Serprintln("Fait commande Marche");
            VIntE = 0;
            if (VTang > 0) {Vtotrot = 0;}
          }
          if(VStr.startsWith("A")) 
          {
            VMA = false;
            F_Serprintln("Fait commande Arrêt");
            digitalWrite(COfoc, LOW); // Désactivation sortie Focus
            digitalWrite(COdecl, LOW); // Désactivation sortie déclencheur
            digitalWrite(CEnPin, HIGH); // On éteint le moteur
          }
        }
        VRet = 7;
      }                                       
      VIdx = F_Trouvestr(VInbuff, "INTERVAL:", 0); // Vérification syntaxique indiquant le mode interval
      if (VIdx != -1)
      {
        F_Serprint("Fait commande Intervallomètre : ");
        if (F_Chrcount(VInbuff, ':') > 0)
        {
          VStr = F_Strfield(VInbuff, ':', 2,1);
          if (VStr.startsWith("M"))
          {
            VInt = true;
            F_Serprint("Marche");
          }
          if (VStr.startsWith("A"))
          {
            VInt = false;
            F_Serprint("Arrêt");
          }          
          VStr = F_Strfield(VInbuff, ':', 3,1);
          if(VStr != "") {VTint = (uint8_t) VStr.toInt();} else {VTint = 10;}
          F_Serprint(", Délai = " + String(VTint) + "s");
          VStr = F_Strfield(VInbuff, ':', 4, 1);
          if(VStr != "") {VNBint = (uint16_t) VStr.toInt();} else {VNBint = 0;}
          VIntE = 0;
        }
        VRet = 8;
        F_Serprintln(", Nombre intervals = " + String(VNBint));
      }
      VIdx = F_Trouvestr(VInbuff, "?", 0); // Vérification syntaxique indiquant une commande Help
      if (VIdx != -1)
      {
        F_Aide();
      }      
      VIdx = F_Trouvestr(VInbuff, "REBOOT:", 0); // Vérification syntaxique indiquant une commande Reboot
      if (VIdx != -1)
      {
        VRet = 9;
      }                 
    }
  }
  return VRet;
} // Fin fonction F_Commande

/* 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



/* 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

/* Routines utilisées pour gérer les bouton poussoirs, retourne true ou false selon que les BP sont
appuyés ou relâchés
    */
bool F_Bouton1()
{
  int VEtatcur = LOW;
  bool VRetour = false;
  static int VNblec = 10; // Nombre de cycles répétitifs, utilisé pour le filtrage des rebonds
  static int Vbout0 = HIGH;
  static int Vbout1 = HIGH;

  Vbout1 = digitalRead(CBMA); // 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) {VRetour = true;} else {VRetour = false;} // Inversion du sens d'action du bouton (à cause de INPUT_PULLUP)
  return(VRetour);
} // Fin fonction F_Bouton1

bool F_Bouton2()
{
  int VEtatcur = LOW;
  bool VRetour = false;
  static int VNblec = 10; // Nombre de cycles répétitifs, utilisé pour le filtrage des rebonds
  static int Vbout0 = HIGH;
  static int Vbout1 = HIGH;

  Vbout1 = digitalRead(CBFoc); // 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) {VRetour = true;} else {VRetour = false;} // Inversion du sens d'action du bouton (à cause de INPUT_PULLUP)
  return(VRetour);
} // Fin fonction F_Bouton2

bool F_Bouton3()
{
  int VEtatcur = LOW;
  bool VRetour = false;
  static int VNblec = 10; // Nombre de cycles répétitifs, utilisé pour le filtrage des rebonds
  static int Vbout0 = HIGH;
  static int Vbout1 = HIGH;

  Vbout1 = digitalRead(CDecl); // 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) {VRetour = true;} else {VRetour = false;} // Inversion du sens d'action du bouton (à cause de INPUT_PULLUP)
  return(VRetour);
} // Fin fonction F_Bouton2

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

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

// Permet de compter le nombre d'occurences d'un caractère dans une chaine
int F_Chrcount(String PChn, char PCar)
{
  int Vresu = 0;
  int VLong = PChn.length();
  for (int i =0; i < VLong; i++)
  {
    if (PChn[i] == PCar) {Vresu++;}
  }
  return Vresu;
} // Fin F_Chrcount

/* La fonction F_StrField permet d'extraire une partie de chaine en
 fonction de caractères de délimitation.
 La syntaxe est F_Strfield(Chaine,car,deb,nbr)

   Chaine  = chaine dans laquelle on effectue la recherche
   car     = caractère de délimitation des éléments recherchés
   deb     = Occurence de départ
   nbr     = nombre d'occurences


   Exemples :

       F_Strfield("ABC","|",1,1) = ABC
       F_Strfield("ABC|DEF|GHI","|",1,1) = ABC
       F_Strfield("ABC|DEF|GHI","|",2,1) = DEF
       F_Strfield("ABC|DEF|GHI","|",1,2) = ABC|DEF
       F_Strfield("ABC|DEF|GHI","|",2,2) = DEF|GHI
       F_Strfield("ABC|DEF|GHI","|",2,5) = DEF|GHI
*/

String F_Strfield(String PChn, char PCar, int PDeb, int PNbe)
{
  String VResu = "";
  int VStart, Vidx, Vposition;
  int Vdeb = 0;
  int Vfin = 0;
  String VChn2 = "";
  if (PChn.length() == 0  || PDeb < 1 || PNbe < 1)
  {
    return VResu;
  }
  else
  {
   Vidx = 0;
   VStart = 0;
   Vposition = -1;
    while(Vidx < (PDeb -1))
    {  
      Vdeb = Vposition +1;
      Vposition = PChn.indexOf(PCar, VStart);
      if (Vposition == -1)
      {
        return VResu;
      }
      else
      {
        Vidx+=1;
        VStart = Vposition +1;
      }
    }
    VChn2 =PChn.substring(VStart);
    //if (PDeb == 1) {Vdeb = 0;} else {Vdeb = Vposition + 1;}
    Vidx = 0;
    VStart = 0;
    while(Vidx < PNbe)
    {
      Vposition = VChn2.indexOf(PCar, VStart);
      if (Vposition != -1)
      {
        Vidx +=1;
        //VStart = Vposition +1;
        //Vfin = Vposition;
        if (Vidx == PNbe) {return VChn2.substring(0,Vposition);} else {VStart = Vposition +1;}
      }
      else
      {
        return VChn2;
      }  
    }
    return PChn.substring(Vdeb, Vfin);
  }
} // Fin fonction F_Strfield

int F_Toasc(String PChn) // Retourne l'entier représentant la valeur ASCII du premier caratère de PChn
{
  int Vresu = 0;
  if (PChn != "") Vresu = (int) PChn[0];
  return Vresu;
}

bool F_Action() // Exécution des commandes
{
  bool Vresu;
  static unsigned long Vtmppre = millis();
  static unsigned long Vtmpcur = millis();
  static unsigned long Vdelai = 0;
  Vresu = false;
  Vtmpcur = millis();
  if (Vdelai > 0 && (Vtmpcur - Vtmppre) > Vdelai) // Temps écoulé
  {
    Vdelai = 0;
    Vtmppre = Vtmpcur;
  }
  if (VInt == true && VMA == true && Vdelai == 0) // Mode interval et Marche actif et interval non en cours
  {
    F_Serprintln("Intervallomètre : Déclenchement, interval " + String(VIntE));
    digitalWrite(COfoc, HIGH); // Activation sortie Focus
    Vresu = true;
    if(VTfoc > 0) // Temps focus non nul
    {
      delay(VTfoc * 1000); // Attente du délai pour réglage mise au point
    }
    if(VTdecl > 0) // On enchaine sur le déclenchement
    {
      digitalWrite(COdecl, HIGH); // Activation sortie déclencheur
      delay(VTdecl * 1000); // Attente du temps d'opération du déclencheur
    }
    digitalWrite(COfoc, LOW); // Désactivation sortie Focus
    digitalWrite(COdecl, LOW); // Désactivation sortie déclencheur
    Vdelai = VTint * 1000; // Temps interval passé en millisecondes
    Vtmppre = Vtmpcur;
    VIntE += 1; // Incrémentation interval courant
    if (VNBint > 0 && VIntE > VNBint) 
    {
      VMA = false;
      VIntE = 0;
      F_Serprintln("Pret");
    }
  }
  else if (VInt == false && VMA == true && Vdelai == 0) // Mode interval inactif et Marche et interval non en cours
  {
    if (VTang > 0) {F_Serprintln("Rotation : Déclenchement/Rotation");} else {F_Serprintln("Déclenchement unique");};
    if (VTfoc > 0) // Temps focus non nul
    {
      digitalWrite(COfoc, HIGH); // Activation sortie Focus
      delay(VTfoc * 1000); // Attente du délai pour réglage mise au point
    }
    if(VTdecl > 0) // On enchaine sur le déclenchement
    {
      digitalWrite(COfoc, HIGH); // Activation sortie Focus
      digitalWrite(COdecl, HIGH); // Activation sortie déclencheur
      delay(VTdecl * 1000); // Attente du temps d'opération du déclencheur
    }
    digitalWrite(COfoc, LOW); // Désactivation sortie Focus
    digitalWrite(COdecl, LOW); // Désactivation sortie déclencheur
    if (VTang > 0 && VVrot > 0) // Rotation active
    {
      if (VSens == true) {F_Gori(VTang);} else {F_Gole(VTang);} // commande rotation
      Vtotrot += VTang;
      F_Serprintln("Rotation totale : " + String(Vtotrot)); // Affichage rotation totale
      if (Vtotrot >= VAngmax) 
      {
        VMA = false; // Arrêt commande
        Vtotrot = 0;
        F_Serprintln("Pret");
      }
    }
    if (VPause == 0) {Vdelai = 1000;} else {Vdelai = (unsigned long) VPause * 1000;} // Attente de 1 seconde pour stabilisation
    Vtmppre = Vtmpcur;
    if (VTang == 0)  // Si pas de rotation on arrête l'exécution
    { 
      VMA = false;
      F_Serprintln("Pret");
    }
  }
  return Vresu;
} // Fin fonction F_Action

void F_Aide()
{
  F_Serprintln("   Commandes disponibles :");
  F_Serprintln("   -----------------------");
  F_Serprintln("?");
  F_Serprintln("Raz :");
  F_Serprintln("Focus : [Durée] {secondes :  0-9}");
  F_Serprintln("Rideau : [Durée] {secondes : 0-3600}");
  F_Serprintln("Pause : [Durée] {secondes : 0-300}");
  F_Serprintln("TourneD : [Ang] : [Vit] : [AngMax] {degrés} {L/M/R} {degrés}");
  F_Serprintln("TourneG : [Ang] : [Vit] : [AngMax] {degrés} {L/M/R} {degrés}");
  F_Serprintln("Interval : [M/A] : [Durée] {secondes} : [Nombre intervals]");
  F_Serprintln("MA : [M/A]");
  F_Serprintln("Rapport :");
  F_Serprintln("Reboot :");
    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é");}
  VUbat = F_Ubat();
  F_Serprintln("Tension batterie = " + String(F_Ubat()) + "V");
} // Fin fonction F_Aide

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.