Voici un petit code d'exemple à tester qui inclut un bibliothèque minimaliste FTP.
ça gère le temps par NTP, le maintient de la connexion au WiFi en cas de déconnexion et ça propose de faire un log journalier et un log cyclique (tous les delta T).
Dans le code vous aurez à remplir cette partie là
// Le WiFi
const char WIFI_NETWORK[] = "************"; // le SSID
const char WIFI_PWD[] = "*********"; // le mot de passe
// le FTP
const char ftpServer[] = "******"; // le serveur ftp comme ftp.cluster1.hosting.ovh.net
const uint16_t ftpPort = 21; // le port standard FTP
const char ftpUser[] = "******"; // le login sur le serveur FTP
const char ftpPwd[] = "******"; // le mot de passe du serveur FTP
const char ftpDirectory[] = "/FTP_LOG"; // le chemin du répertoire depuis la racine du serveur FTP où sauver les logs
avec les infos de votre environnement
et pour voir comment construire un nom de fichier avec la date, regardez les fonctions ecrireLogJournalier() (le nom est sous la forme AAAAMMJJ.txt et la fonction ecrireLogCyclique() où là le nom est sous la forme AAAAMMJJ_HHMMSS.txt
ça devrait vous donner des idées.
Le code principal (le .ino)
/*
╔═════════════════════════════════════════════════════════════════════════════════╗
║ ║
║ code is placed under the MIT license ║
║ Copyright (c) 2026 J-M-L ║
║ For the Arduino Forum : https://forum.arduino.cc/u/j-m-l ║
║ ║
║ Permission is hereby granted, free of charge, to any person obtaining a copy ║
║ of this software and associated documentation files (the "Software"), to deal ║
║ in the Software without restriction, including without limitation the rights ║
║ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ║
║ copies of the Software, and to permit persons to whom the Software is ║
║ furnished to do so, subject to the following conditions: ║
║ ║
║ The above copyright notice and this permission notice shall be included in ║
║ all copies or substantial portions of the Software. ║
║ ║
║ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ║
║ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ║
║ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ║
║ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ║
║ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ║
║ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN ║
║ THE SOFTWARE. ║
╚═════════════════════════════════════════════════════════════════════════════════╝
*/
#include <WiFi.h>
#include <WiFiClient.h>
#include <esp_sntp.h>
#include <time.h>
#include "ClientFTPMinimaliste.h"
// Le WiFi
const char WIFI_NETWORK[] = "************"; // le SSID
const char WIFI_PWD[] = "*********"; // le mot de passe
// le FTP
const char ftpServer[] = "******"; // le serveur ftp comme ftp.cluster1.hosting.ovh.net
const uint16_t ftpPort = 21; // le port standard FTP
const char ftpUser[] = "******"; // le login sur le serveur FTP
const char ftpPwd[] = "******"; // le mot de passe du serveur FTP
const char ftpDirectory[] = "/FTP_LOG"; // le chemin du répertoire depuis la racine du serveur FTP où sauver les logs
// Log journalier
const bool journalisation = true; // fait on un log par jour ?
// Heure cible pour un log ici tous les jours à 12h30:00
const uint8_t heureCibleLog = 12;
const uint8_t minuteCibleLog = 30;
const uint8_t secondeCibleLog = 0;
// Log cyclique (écritures periodiques dans un log)
const bool cyclique = true; // fait on un log cyclique ?
const uint32_t periodeLog = 15000ul; // toutes les 15 secondes
WiFiClient tcpClient;
WiFiClient dataClient;
ClientFTPMinimaliste ftpClient(tcpClient, dataClient);
const char *ntpServer = "fr.pool.ntp.org";
bool wifiOK = false;
bool synchroNtpEffectuee = false;
time_t derniereSynchroNTP;
void ntpSyncCallback(struct timeval *tv) {
derniereSynchroNTP = time(NULL);
synchroNtpEffectuee = true;
Serial.print("NTP synced at: ");
struct tm *pTime = localtime(&derniereSynchroNTP);
Serial.println(pTime, "%d/%m/%Y %H:%M:%S");
}
void WiFiStationConnection(WiFiEvent_t, WiFiEventInfo_t) {
Serial.println("Wi-Fi connected");
}
void WiFiStationObtentionIP(WiFiEvent_t, WiFiEventInfo_t) {
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
wifiOK = true;
}
void WiFiStationDeconnection(WiFiEvent_t, WiFiEventInfo_t info) {
wifiOK = false;
Serial.print("Wi-Fi disconnected. Reason: ");
Serial.println(info.wifi_sta_disconnected.reason);
WiFi.begin(WIFI_NETWORK, WIFI_PWD);
}
bool afficherTemps(bool affichageForce = false) {
static unsigned long dernierAffichage = 0;
if (!synchroNtpEffectuee) return false;
if (affichageForce || millis() - dernierAffichage >= 1000) {
time_t now = time(NULL);
struct tm *pTime = localtime(&now);
Serial.println(pTime, "%d/%m/%Y %H:%M:%S");
dernierAffichage = millis();
}
return true;
}
bool logJournalier() {
if (!journalisation) return false;
if (!wifiOK || !synchroNtpEffectuee) return false;
time_t now = time(NULL);
struct tm *pTime = localtime(&now);
static bool dejaEcritAujourdHui = false;
static int dernierJour = -1;
// Réinitialisation si changement de jour
if (pTime->tm_mday != dernierJour) {
dejaEcritAujourdHui = false;
dernierJour = pTime->tm_mday;
}
uint32_t secondesDepuisMinuit = pTime->tm_hour * 3600ul + pTime->tm_min * 60ul + pTime->tm_sec;
uint32_t cibleSecondes = heureCibleLog * 3600ul + minuteCibleLog * 60ul + secondeCibleLog;
if (secondesDepuisMinuit >= cibleSecondes && !dejaEcritAujourdHui) {
dejaEcritAujourdHui = true;
return true;
}
return false;
}
bool logRegulier() {
if (!cyclique) return false;
if (!wifiOK || !synchroNtpEffectuee) return false;
static uint32_t derniereEcriture = -periodeLog;
if (millis() - derniereEcriture >= periodeLog) {
derniereEcriture = millis();
return true;
}
return false;
}
void ecrireFichierFTP(const char *nomFichier, const char *texte, size_t taille) {
if (!wifiOK || !synchroNtpEffectuee) {
Serial.println("Erreur : WiFi ou NTP non disponible, log impossible");
return;
}
if (!ftpClient.ouvrir(ftpServer, ftpPort, ftpUser, ftpPwd)) {
Serial.printf("Erreur : impossible de se connecter au serveur FTP %s:%d\n", ftpServer, ftpPort);
return;
}
Serial.printf("Changer de répertoire : ");
if (!ftpClient.changerRepertoire(ftpDirectory)) {
Serial.printf("Erreur : impossible de changer pour le répertoire %s\n", ftpDirectory);
ftpClient.fermer();
return;
} else {
Serial.println("OK");
}
Serial.printf("Écriture du fichier %s : ", nomFichier);
if (!ftpClient.ecrireFichier(nomFichier, texte, taille)) {
Serial.println("Erreur : écriture FTP échouée");
ftpClient.fermer();
return;
} else {
Serial.println("OK");
Serial.printf("Contenu écrit : %s\n", texte);
}
ftpClient.fermer();
}
void ecrireLogJournalier() {
char ftpFilename[] = "AAAAMMJJ.txt" ; // juste pour réserver le bon nombre de caractères
char buffer[128];
time_t now = time(NULL);
struct tm *pTime = localtime(&now);
snprintf(buffer, sizeof buffer, "Log journalier - Nous sommes le %02d/%02d/%04d et l'heure est %02d:%02d:%02d\r\n",
pTime->tm_mday, pTime->tm_mon + 1, pTime->tm_year + 1900,
pTime->tm_hour, pTime->tm_min, pTime->tm_sec);
snprintf(ftpFilename, sizeof ftpFilename, "%04d%02d%02d.txt", pTime->tm_year + 1900, pTime->tm_mon + 1, pTime->tm_mday);
ecrireFichierFTP(ftpFilename, buffer, strlen(buffer));
}
void ecrireLogCyclique() {
char ftpFilename[] = "AAAAMMJJ_HHMMSS.txt"; // juste pour réserver le bon nombre de caractères
char buffer[128];
time_t now = time(NULL);
struct tm *pTime = localtime(&now);
snprintf(buffer, sizeof buffer, "Log cyclique - Nous sommes le %02d/%02d/%04d et l'heure est %02d:%02d:%02d\r\n",
pTime->tm_mday, pTime->tm_mon + 1, pTime->tm_year + 1900,
pTime->tm_hour, pTime->tm_min, pTime->tm_sec);
snprintf(ftpFilename, sizeof ftpFilename, "%04d%02d%02d_%02d%02d%02d.txt",
pTime->tm_year + 1900, pTime->tm_mon + 1, pTime->tm_mday,
pTime->tm_hour, pTime->tm_min, pTime->tm_sec);
ecrireFichierFTP(ftpFilename, buffer, strlen(buffer));
}
void log() {
if (logJournalier()) ecrireLogJournalier(); // si c'est le moment du log journalier, écrire le log journalier
if (logRegulier()) ecrireLogCyclique(); // si c'est le moment du log cyclique, écrire le log cyclique
}
void setup() {
Serial.begin(115200);
Serial.println("Demo with Wi-Fi, NTP, and FTP");
WiFi.onEvent(WiFiStationConnection, ARDUINO_EVENT_WIFI_STA_CONNECTED);
WiFi.onEvent(WiFiStationObtentionIP, ARDUINO_EVENT_WIFI_STA_GOT_IP);
WiFi.onEvent(WiFiStationDeconnection, ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
configTzTime("CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00", ntpServer);
sntp_set_time_sync_notification_cb(ntpSyncCallback);
WiFi.begin(WIFI_NETWORK, WIFI_PWD);
}
void loop() {
afficherTemps(); // à commenter si vous ne voulez pas voir les secondes défiler
log();
}
ensuite dans le même répertoire du sketch vous mettez ces deux fichiers
ClientFTPMinimaliste.h
/*
╔═════════════════════════════════════════════════════════════════════════════════╗
║ ║
║ code is placed under the MIT license ║
║ Copyright (c) 2026 J-M-L ║
║ For the Arduino Forum : https://forum.arduino.cc/u/j-m-l ║
║ ║
║ Permission is hereby granted, free of charge, to any person obtaining a copy ║
║ of this software and associated documentation files (the "Software"), to deal ║
║ in the Software without restriction, including without limitation the rights ║
║ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ║
║ copies of the Software, and to permit persons to whom the Software is ║
║ furnished to do so, subject to the following conditions: ║
║ ║
║ The above copyright notice and this permission notice shall be included in ║
║ all copies or substantial portions of the Software. ║
║ ║
║ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ║
║ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ║
║ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ║
║ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ║
║ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ║
║ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN ║
║ THE SOFTWARE. ║
╚═════════════════════════════════════════════════════════════════════════════════╝
*/
#ifndef ClientFTPMinimaliste_H
#define ClientFTPMinimaliste_H
#define DEBUG_FTP 0 // mettre à 0 pour enlever les traces, 1 pour activer les traces
#if DEBUG_FTP
#define D_SerialBegin(...) Serial.begin(__VA_ARGS__)
#define D_print(...) Serial.print(__VA_ARGS__)
#define D_write(...) Serial.write(__VA_ARGS__)
#define D_println(...) Serial.println(__VA_ARGS__)
#define D_printf(...) Serial.printf(__VA_ARGS__)
#else
#define D_SerialBegin(...)
#define D_print(...)
#define D_write(...)
#define D_println(...)
#define D_printf(...)
#endif
#include <Client.h>
#include <Arduino.h>
#define DELAI_TIMEOUT_FTP 20000
#define TAILLE_BUFFER_FTP 128
#define TAILLE_BLOC_FTP 512
class ClientFTPMinimaliste {
private:
Client& controle;
Client& donnees;
uint16_t code;
char tamponReponse[TAILLE_BUFFER_FTP];
uint8_t tamponEcriture[TAILLE_BLOC_FTP];
uint16_t executerCommande(const char* commande, const char* parametre = "", char* reponse = nullptr);
uint16_t lireReponse(char* reponse = nullptr);
bool ouvrirModePassif();
bool fermerModePassif();
void envoyer(const uint8_t* tampon, size_t taille);
public:
ClientFTPMinimaliste(Client& controle, Client& donnees);
bool ouvrir(const char* serveur, const uint16_t port, const char* utilisateur, const char* motDePasse);
void fermer();
bool creerRepertoire(const char* repertoire);
bool changerRepertoire(const char* repertoire);
bool ecrireFichier(const char* nomFichier, const uint8_t* tampon, size_t taille);
bool ecrireFichier(const char* nomFichier, const char* tampon, size_t taille);
bool supprimerFichier(const char* nomFichier);
};
#endif
et l'implémentation associée
ClientFTPMinimaliste.cpp
/*
╔═════════════════════════════════════════════════════════════════════════════════╗
║ ║
║ code is placed under the MIT license ║
║ Copyright (c) 2026 J-M-L ║
║ For the Arduino Forum : https://forum.arduino.cc/u/j-m-l ║
║ ║
║ Permission is hereby granted, free of charge, to any person obtaining a copy ║
║ of this software and associated documentation files (the "Software"), to deal ║
║ in the Software without restriction, including without limitation the rights ║
║ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell ║
║ copies of the Software, and to permit persons to whom the Software is ║
║ furnished to do so, subject to the following conditions: ║
║ ║
║ The above copyright notice and this permission notice shall be included in ║
║ all copies or substantial portions of the Software. ║
║ ║
║ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ║
║ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ║
║ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ║
║ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ║
║ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, ║
║ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN ║
║ THE SOFTWARE. ║
╚═════════════════════════════════════════════════════════════════════════════════╝
*/
#include "ClientFTPMinimaliste.h"
#include <Arduino.h>
ClientFTPMinimaliste::ClientFTPMinimaliste(Client& controle, Client& donnees)
: controle(controle), donnees(donnees) {}
bool ClientFTPMinimaliste::ouvrir(const char* serveur, const uint16_t port, const char* utilisateur, const char* motDePasse) {
if (!controle.connect(serveur, port)) return false;
if (lireReponse() != 220) return false;
if (executerCommande("USER ", utilisateur) != 331) return false;
if (executerCommande("PASS ", motDePasse) != 230) return false;
executerCommande("TYPE ", "I"); // mode binaire
return true;
}
void ClientFTPMinimaliste::fermer() {
executerCommande("QUIT");
controle.stop();
}
bool ClientFTPMinimaliste::creerRepertoire(const char* repertoire) {
return executerCommande("MKD ", repertoire) == 257;
}
bool ClientFTPMinimaliste::changerRepertoire(const char* repertoire) {
return executerCommande("CWD ", repertoire) == 250;
}
bool ClientFTPMinimaliste::supprimerFichier(const char* nomFichier) {
return executerCommande("DELE ", nomFichier) == 250;
}
bool ClientFTPMinimaliste::ecrireFichier(const char* nomFichier, const uint8_t* tampon, size_t taille) {
if (!ouvrirModePassif()) return false;
if (executerCommande("STOR ", nomFichier) == 150) envoyer(tampon, taille);
return fermerModePassif();
}
bool ClientFTPMinimaliste::ecrireFichier(const char* nomFichier, const char* tampon, size_t taille) {
return ecrireFichier(nomFichier, reinterpret_cast<const uint8_t*>(tampon), taille);
}
uint16_t ClientFTPMinimaliste::executerCommande(const char* commande, const char* parametre, char* reponse) {
controle.print(commande);
controle.println(parametre);
return lireReponse(reponse);
}
uint16_t ClientFTPMinimaliste::lireReponse(char* reponse) {
uint32_t timeout = millis() + DELAI_TIMEOUT_FTP;
uint8_t i = 0;
while (!controle.available() && millis() < timeout) delay(5);
while (controle.available()) {
char c = controle.read();
if (i < TAILLE_BUFFER_FTP - 1) {
tamponReponse[i++] = c;
tamponReponse[i] = 0;
}
}
if (reponse) strcpy(reponse, &tamponReponse[4]);
sscanf(tamponReponse, "%hu", &code);
return code;
}
bool ClientFTPMinimaliste::ouvrirModePassif() {
char reponse[128];
uint16_t code = executerCommande("PASV", "", reponse);
D_printf("Réponse PASV : %u %s\n", code, reponse);
if (code != 227) {
D_println("Erreur : serveur refuse PASV");
return false;
}
strtok(reponse, "(,");
uint8_t d[6];
for (uint8_t i = 0; i < 6; i++) {
char* t = strtok(nullptr, "(,");
if (!t) {
D_println("Erreur : impossible d'extraire l'adresse IP/port PASV");
return false;
}
d[i] = atoi(t);
}
IPAddress ip(d[0], d[1], d[2], d[3]);
uint16_t port = (d[4] << 8) | d[5];
D_printf("Connexion données passive : %u.%u.%u.%u:%u\n", d[0], d[1], d[2], d[3], port);
bool ok = donnees.connect(ip, port);
D_printf("Connexion données : %s\n", ok ? "OK" : "Erreur");
return ok;
}
bool ClientFTPMinimaliste::fermerModePassif() {
donnees.stop();
return lireReponse() == 226;
}
void ClientFTPMinimaliste::envoyer(const uint8_t* tampon, size_t taille) {
size_t n = 0;
for (size_t i = 0; i < taille; i++) {
tamponEcriture[n++] = tampon[i];
if (n == TAILLE_BLOC_FTP) {
donnees.write(tamponEcriture, n);
n = 0;
}
}
if (n > 0) donnees.write(tamponEcriture, n);
}
vous avez dans le code principal la possibilité de régler l'heure d'écriture du log journalier et le temps entre 2 logs successifs (cyclique) avec chacun un bool qui dit si on le fait ou pas.
// Log journalier
const bool journalisation = true; // fait on un log par jour ?
// Heure cible pour un log ici tous les jours à 12h30:00
const uint8_t heureCibleLog = 12;
const uint8_t minuteCibleLog = 30;
const uint8_t secondeCibleLog = 0;
// Log cyclique (écritures periodiques dans un log)
const bool cyclique = true; // fait on un log cyclique ?
const uint32_t periodeLog = 15000ul; // toutes les 15 secondes
ça devrait fonctionner 