T.T.2.J Version 2
Après plusieurs mois d’utilisation j’ai constaté quelques points gênants dans l’utilisation de ma télécommande de train de jardin.
Le premier et le plus contraignant étant d’avoir un menu et d’utiliser un encodeur rotatif pour naviguer dedans et choisir la locomotive. Même si cette solution est élégante sur le papier, en pratique elle ne permet pas de passer rapidement du pilotage d’une locomotive à une autre. Les télécommandes Deltang étaient visiblement munies d’un sélecteur rotatif à 10 positions permettant de choisir rapidement la loco.
J’ai donc décidé de faire évoluer ma télécommande dans ce sens. Pour cela j’ai enlevé l’encodeur et l’ai remplacé par un sélecteur rotatif 12 positions.
Le deuxième point contraignant est le manque de lisibilité de l’écran LCD sous le soleil. J’en ai profité pour éliminer le menu et l’écran ne sert maintenant plus qu’a afficher des informations dont on peut se passer pour la conduite du train, c’est désormais un écran OLED
Le dernier point est le manque de stabilité de la connexion 2.4Ghz qui plante par moment. J’ai utilisé l’IA pour réaliser le nouveau code de ma télécommande V2 et il semble que le fait d’avoir un sélecteur rotatif et surtout, je pense, un code plus propre, aide fortement à la stabilité de la connexion. Après 4 séances d’essais je n’ai pas constaté de coupure de liaison, même en passant d’une locomotive à l’autre.
J’ai en profité pour réduire considérablement la taille de la télécommande, pour cela j’ai utilisé un Adruino Uno à la place du Mega, et surtout un écran Oled 1.3″.
Nous verrons à l’usage si il faudra passer à une version 3 à base de module ESP32 qui communiquent en Wifi.
Utilisation d’un sélecteur rotatif 12 positions avec 1 seule entrée analogique sur l’arduino :
N’ayant pas la possibilité d’utiliser 12 entrées pour les 12 positions du sélecteur j’ai cherché une solution à ce problème. J’ai posé la question à une IA : Le Chat de la société Mistral et la réponse a été simple : il faut utiliser des diviseurs de tension et une entrée analogique comme cela pour chaque position une tension différent sera appliquée à l’entrée A0 de l’arduino. La position sera donc détectée par l’arduino. L’IA m’a aussi calculé la valeur de ces diviseurs de tension :
Pour calculer les tensions de sortie qui seront mesurées par l’Arduino avec les résistances standardisées, il faut utiliser la formule du pont diviseur de tension. La tension de sortie ( V{out} ) est donnée par :
Où :
- Vout est la tension de sortie mesurée aux bornes de la résistance commune Rc.
- Vin est la tension d’entrée appliquée au diviseur de tension (5V pour l’Arduino Uno).
- Ri est la résistance connectée à une position spécifique du commutateur.
- Rc est la résistance commune connectée à la masse :10kΩ.
Position Résistance Ri (kΩ) Tension de sortie (V) 1 470.0 0.10 2 82.0 0.54 3 39.0 1.02 4 22.0 1.47 5 15.0 1.92 6 12.0 2.27 7 8.2 2.75 8 5.6 3.21 9 3.9 3.60 10 2.2 4.03 11 1.2 4.46 12 0.22 4.89 Ces tensions de sortie sont calculées en utilisant les résistances standardisées et devraient aider à identifier chaque position du commutateur avec l’Arduino.
J’ai ensuite demandé à l’IA de me générer le code correspondant pour que chacune des 12 positions du sélecteur corresponde une adresse qui sera utilisée pour définir le canal de communication du module nRF24L01 et donc les 12 récepteurs (locomotives) avec lesquels la télécommande pourra communiquer. J’ai aussi demandé d’associer à ces 12 adresses, 12 noms de locomotives qui seront affichés sur l’écran OLED.
Le point milieu du sélecteur rotatif est connecté au +5V, les 12 résistances à chacunes des bornes extérieures du sélecteur. Toutes les résistances sont reliées entre elles puis au travers de la résistance RC connectées à la masse. Le point milieu entre les résistances Ri et RC est connecté à la borne A0 de l’Arduino :
Gestion de l’écran :
Pour gérer l’écran Oled je me suis aidé des 2 sites ci-dessous :
https://electroniqueamateur.blogspot.com/2019/01/ecran-oled-sh1106-i2c-et-arduino.html
https://passionelectronique.fr/ecran-oled-i2c-arduino
J’ai créé le logo avec la locomotive ainsi qu’une petite icone de lampe allumée qui sont intégrés dans le code.

Boutons poussoirs, potentiomètre et communication radio :
J’ai conservé un bouton STOP, et un bouton pour allumer/éteindre les phares des locomotives. J’ai aussi gardé un potentiomètre pour piloter le moteur des locomotives.
Afin de ne pas devoir changer les codes des récepteurs je suis parti sur la même bibliothèque de transmission radio pour le module nRF24L01 avec les mêmes adresses et les mêmes variables envoyées pour le potentiomètre, l’arrêt d’urgence et la lumière. Se référer aux précédents articles pour plus d’explications.
Il y a aussi 2 boutons en attente pour une future sonorisation des locomotives.
Alimentation :
La télécommande est toujours alimentée par 2 batteries 18650 via un interrupteur ON/OFF. J’ai aussi prévu un pont diviseur afin de pouvoir mesurer la tension de la batterie mais pour le moment cette fonction n’est pas encore implantée car elle fait planter le programme.
La connexion entre la partie inférieure du boitier ou il y a les batteries, et la partie supérieure ou est toute l’électronique, est réalisée avec des petites broches :
Boitier en impression 3D :
Je me suis servi de 2 designs disponibles sur Thingiverse pour le support de l’écran Oled et pour le support de l’Arduino Uno. J’ai intégré ces designs dans ma télécommande que j’ai dessinée sur Tinkercad et ensuite imprimée en PLA.

Lien vers les fichiers STL pour imprimer la télécommande :
https://www.thingiverse.com/thing:7146003
Voici une photo pour comparer le nouveau boitier (à droite) avec l’ancien :
Le nouveau boitier tient beaucoup mieux en main et est plus ergonomique. Les commandes surtout le changement de locomotive sont beaucoup plus réactives.
Voici une petite démo :
https://www.youtube.com/watch?v=dZq-kOeDvJ4&list=PLXsWxZBZicnRipiJa6pr8HRC8UqP038c8&index=14
Code complet de la télécommande :
#include <SPI.h>
#include <NRFLite.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH1106.h>
// Définir display
Adafruit_SH1106 display(-1);
// Définir les pins analogiques utilisés
const int analogPin = A0; // Pin pour le sélecteur rotatif
const int potPin = A1; // Pin pour le potentiomètre
// Définir les pins pour les boutons poussoirs
const int buttonARUPin = 2; // Pin pour le bouton ARU
const int buttonLightPin = 3; // Pin pour le bouton LIGHT
// Variables pour les états des boutons
int ARU = 0;
int LIGHT_Value = 0;
// Tableau des tensions de référence pour chaque position
float referenceVoltages[] = { 0.10, 0.54, 1.02, 1.47, 1.92, 2.27, 2.75, 3.21, 3.60, 4.03, 4.46, 4.89 };
// Noms des locomotives
const char* locomotiveNames[] = { "RUSTY", "GEMINI", "LIGHTNING", "THUNDER", "STORM", "BLAZE", "COMET", "VULCAN", "ZEPHYR", "AURORA", "TITAN", "NEBULA" };
// Identifiants numériques pour les adresses
const uint8_t radioIds[] = { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22 };
// Identifiant de l'adresse du récepteur
uint8_t DESTINATION_RADIO_ID;
// Configuration du module radio émetteur
const static uint8_t RADIO_ID = 1;
const static uint8_t PIN_RADIO_CE = 9; // Mise à jour du pin CE
const static uint8_t PIN_RADIO_CSN = 10; // Mise à jour du pin CSN
NRFLite _radio;
// Structure pour regrouper les valeurs à envoyer
struct RadioPacket {
int POT_Value;
int ARU;
int LIGHT_Value;
};
//déclaration de la variable associée à la structure
RadioPacket _radioData;
// Dessin du logo d'accueil
const unsigned char logo[] PROGMEM = {
// 'LOGO, 64x64px
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0x80, 0x7f, 0xff, 0xfe, 0x00,
0x00, 0x07, 0xff, 0x80, 0x7f, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0x80, 0x7f, 0xff, 0xfe, 0x00,
0x00, 0x07, 0xff, 0x80, 0x7f, 0xff, 0xfe, 0x00, 0x00, 0x07, 0xff, 0x80, 0x7f, 0xff, 0xfe, 0x00,
0x00, 0x07, 0xff, 0x00, 0x7f, 0xff, 0xfe, 0x00, 0x00, 0x01, 0xfe, 0x00, 0x7f, 0xff, 0xfe, 0x00,
0x00, 0x00, 0xfc, 0x00, 0x7f, 0xff, 0xfe, 0x00, 0x00, 0x03, 0xfe, 0x1f, 0x07, 0xff, 0xf8, 0x00,
0x00, 0x03, 0xfe, 0x1f, 0x07, 0xff, 0xf8, 0x00, 0x00, 0x03, 0xfe, 0x1f, 0x07, 0xff, 0xf8, 0x00,
0x00, 0x03, 0xfe, 0x1f, 0x07, 0xff, 0xf8, 0x00, 0x00, 0x03, 0xfe, 0x1f, 0x07, 0xff, 0xf8, 0x00,
0x00, 0x03, 0xfe, 0x1f, 0x07, 0xff, 0xf8, 0x00, 0x00, 0x03, 0xfe, 0x1f, 0x07, 0xff, 0xf8, 0x00,
0x00, 0x03, 0xfe, 0x1f, 0x07, 0xff, 0xf8, 0x00, 0x00, 0x03, 0xff, 0x1f, 0x07, 0xff, 0xf8, 0x00,
0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00,
0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00,
0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00,
0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00,
0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00,
0x00, 0x1f, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x00, 0x00, 0x0f, 0xff, 0xff, 0xff, 0xff, 0xfe, 0x00,
0x00, 0x0f, 0xe7, 0xff, 0xff, 0xf9, 0xfe, 0x00, 0x00, 0x0f, 0x01, 0xff, 0xff, 0xc0, 0x3f, 0x00,
0x00, 0x1e, 0x38, 0x7f, 0xff, 0x8e, 0x1f, 0x00, 0x00, 0x3c, 0xfe, 0x3f, 0xff, 0x3f, 0x8f, 0x00,
0x00, 0x39, 0xff, 0x3f, 0xfe, 0x7f, 0xc8, 0x00, 0x00, 0x79, 0xff, 0x9f, 0xfe, 0x7f, 0xe0, 0x00,
0x00, 0xfb, 0xff, 0x9f, 0xfe, 0x7f, 0xe0, 0x00, 0x00, 0x03, 0xff, 0x80, 0x00, 0xff, 0xe0, 0x00,
0x00, 0x03, 0xff, 0x80, 0x00, 0x7f, 0xe0, 0x00, 0x00, 0x01, 0xff, 0x80, 0x00, 0x7f, 0xe0, 0x00,
0x00, 0x01, 0xff, 0x00, 0x00, 0x7f, 0xc0, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x3f, 0xc0, 0x00,
0x00, 0x00, 0x7c, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// Dessin de l'icone light
const unsigned char light[] PROGMEM = {
// 'light, 20x20px
0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x40, 0x00, 0x10, 0x40, 0x80, 0x08, 0x41, 0x00, 0x04,
0x02, 0x00, 0x00, 0xf0, 0x00, 0x01, 0xf8, 0x00, 0x03, 0xfc, 0x00, 0xfb, 0xfc, 0x00, 0x03, 0xfd,
0xf0, 0x03, 0xfc, 0x00, 0x01, 0xf8, 0x00, 0x00, 0xf0, 0x00, 0x04, 0x02, 0x00, 0x08, 0x21, 0x00,
0x10, 0x20, 0x80, 0x00, 0x20, 0x00, 0x00, 0x20, 0x00, 0x00, 0x20, 0x00
};
void setup() {
Serial.begin(9600);
//Initialiser l'écran
display.begin(SH1106_SWITCHCAPVCC, 0x3C); // Assurez-vous que l'adresse est correcte pour votre écran
display.clearDisplay();
display.setRotation(2); // 0 = pas de rotation, 1 = 90°, 2 = 180°, 3 = 270°
// Afficher le logo au centre pendant 2,5 secondes
afficherLogo();
delay(2500);
// Afficher l'écran de transition pendant 2,5 secondes
afficherEcranTransition();
delay(2500);
// Effacer l'écran avant de passer à la boucle principale
display.clearDisplay();
display.display();
// Initialiser les pins des boutons en entrée avec résistance pull-up
pinMode(buttonARUPin, INPUT_PULLUP);
pinMode(buttonLightPin, INPUT_PULLUP);
// Initialiser le module radio
if (!_radio.init(RADIO_ID, PIN_RADIO_CE, PIN_RADIO_CSN)) {
Serial.println("Impossible d'initialiser le module radio.");
while (1)
; // Arrête l'exécution si l'initialisation échoue
}
DESTINATION_RADIO_ID = radioIds[0]; // Initialisation par défaut
}
// Fonction pour afficher le logo au centre de l'écran
void afficherLogo() {
int16_t x = (display.width() - 64) / 2; // Centrer horizontalement
int16_t y = (display.height() - 64) / 2; // Centrer verticalement
display.drawBitmap(x, y, logo, 64, 64, WHITE);
display.display();
}
// Fonction pour afficher l'écran de transition
void afficherEcranTransition() {
display.clearDisplay();
// Afficher "T.T.2.J" centré en taille 2
display.setTextSize(2);
display.setTextColor(WHITE);
int16_t x = (display.width() - (6 * 12)) / 2; // 6 caractères * 12 pixels par caractère (approximation)
int16_t y = 16; // Position verticale approximative pour centrer
display.setCursor(x, y);
display.print("T.T.2.J");
// Afficher "V2.0" en bas à droite en taille 1
display.setTextSize(1);
x = display.width() - (4 * 6); // 4 caractères * 6 pixels par caractère
y = display.height() - 8; // 8 pixels de marge en bas
display.setCursor(x, y);
display.print("V2.0");
display.display();
}
void loop() {
// ******** GESTION DES ENTREES DE COMMANDES ********
// Lire la valeur du sélecteur rotatif
int sensorValue = analogRead(analogPin);
float voltage = sensorValue * (5.0 / 1023.0);
int position = getSwitchPosition(voltage);
// Lire la valeur du potentiomètre
int POT_Value = analogRead(potPin);
// Lire et mettre à jour l'état des boutons
if (digitalRead(buttonARUPin) == LOW) {
delay(50); // Délai pour éviter les rebonds
ARU = !ARU; // Inverser l'état
while (digitalRead(buttonARUPin) == LOW)
; // Attendre que le bouton soit relâché
}
if (digitalRead(buttonLightPin) == LOW) {
delay(50); // Délai pour éviter les rebonds
LIGHT_Value = !LIGHT_Value; // Inverser l'état
while (digitalRead(buttonLightPin) == LOW)
; // Attendre que le bouton soit relâché
}
// ******** ACTIONS *******
if (position != -1) {
DESTINATION_RADIO_ID = radioIds[position - 1]; // Mettre à jour DESTINATION_RADIO_ID
// ******** GESTION DE L'AFFICHAGE ********
display.clearDisplay();
// Afficher le nom de la locomotive en gros et centré
display.setTextSize(2);
display.setTextColor(WHITE);
display.setCursor(20, 0);
display.print(locomotiveNames[position - 1]);
// Afficher DESTINATION RADIO ID
display.setTextSize(1);
display.setTextColor(WHITE);
display.setCursor(0, 25);
display.print("Radio ID: ");
display.print(DESTINATION_RADIO_ID);
// Afficher la valeur brute du potentiomètre
display.setTextSize(1);
display.setTextColor(WHITE);
display.setCursor(0, 35);
display.print("POT Value: ");
display.print(POT_Value);
// Affichage de l'icône light si LIGHT_Value == 1
if (LIGHT_Value == 1) {
display.drawBitmap(0, display.height() - 20, light, 20, 20, WHITE);
}
// Affichage de "STOP"
if (ARU == 1) {
display.setTextSize(1);
display.setTextColor(WHITE);
int16_t x = 30;
int16_t y = display.height() - 8;
display.setCursor(x, y);
display.print("STOP");
}
display.display();
delay(10);
}
// Afficher les valeurs dans le moniteur série
//Serial.print("Potentiometer Value: ");
//Serial.println(POT_Value);
//Serial.print("Destination Radio ID: ");
//Serial.println(DESTINATION_RADIO_ID);
// ******** GESTION DE LA TRANSMISSION RADIO ********
// Créer une instance de la structure pour regrouper les valeurs à envoyer
_radioData.POT_Value = POT_Value;
_radioData.ARU = ARU;
_radioData.LIGHT_Value = LIGHT_Value;
// Transmettre les valeurs par radio
if (_radio.send(DESTINATION_RADIO_ID, &_radioData, sizeof(_radioData))) // on envoit la structure precedement rempli a l'adresse selectionnee
{
Serial.println(" ...Succes");
} else {
Serial.println(" ...Echec");
}
delay(100);
}
// Fonction pour déterminer la position du sélecteur rotatif
int getSwitchPosition(float voltage) {
for (int i = 0; i < 12; i++) {
if (abs(voltage - referenceVoltages[i]) < 0.1) {
return i + 1;
}
}
return -1;
}