Mes trains passent au numérique ;


avec RaspberryPi et TinyGo !

Un peu d'histoire


  • Train(s) acheté au début des années 1980 ;
  • ... Puis il a passé 25 ans au grenier...
  • ... Avant d'être remis en route en 2005.

  • On l'a fait (un peu) évoluer depuis 2011 :
    • Taille x3 (gare de triage, extension de la table) ;
    • Nouveaux bâtiments, nouvelles structures ;
    • Mise à niveau électrique (l'ancien setup avait 40 ans, #FormationIncendie)

  • => 3 générations y ont joué !



Mais, c'est quoi un train électrique ?

  • C'est un jouet apparu il a 2 siècles environ ;
  • Qui se démocratise dans les années 1950 (standardisation, coûts, etc) ;
  • Nous, on est en format HO (réplique 1:87) ;
  • C'est toujours produit ! (mais les prix font mal).
  • Il en existe plein de modèles : TGV, TER, draisines, il y en a pour tout les goûts !


  • 14V continu dans un rail ;
  • 1 lampe et un moteur branché en dérivation ;
  • Et... Bah c'est tout.




En 2011, on agrandit le terrain de jeu

  • Et on commence à voir les limites de ce système
    • On pilote nos trains en analogique, avec un variateur -14 => +14 V
    • Donc tout nos trains vont dans le même sens, à la même vitesse
    • Même sur notre grande table, 3 trains en simultané, à gérer, c'est rude
    • Du coup, on a 18 locomotives mais on peut en gérer 3 max :-(
    • Solution "ça marche" : on coupe certaines zones du circuit pour éteindre les locos

(Par contre, faut pas regarder dedans)

On regarde si il existe un système

  • Et oui, il existe le DCC !
  • En très court et en très bref, on passe des informations par le courant, comme du CPL ;
  • On peut ainsi gérer plusieurs trains (entre 2 et 8 selon le matériel) à vitesses et directions différentes !


Et là, on se dit : c'est génial, il nous faut ça !

Oui, MAIS...

  • C'est comme un produit Apple, faut tout changer !
  • Il faut un transformateur particulier ;
  • Il faut changer les moteurs des trains ;
  • Et rajouter des cartes dedans.
  • Coût de l'opération ? Environ 2900€


Au final, nos trains en analogique, c'est pas si mal hein !

En plus, bien entendu, c'est 100% propriétaire, donc impossible d'aller bidouiller derrière.

On abandonne l'idée, les années passent...

... Jusqu'en 2024.


"TinyGo, petit mais costaud ! 💪" (Thierry Chantier @titimoby)

On vole emprunte un train, et on démonte

  • Il y a pile la place pour mettre un Raspberry Pi Pico !
  • 21 * 51 mm
  • 264kB RAM ; 2 coeurs @133MHz ; 2MB de mémoire flash
  • On en commande 5 en version "W" (avec WiFi/Bluetooth inclus)


Notre victime (BB9201)

14V => 5V

  • Le rail est alimenté en 14V, le Pi prend 5V max => étincelles, chaleur, fumée.
  • => On achète des ponts abaisseurs de tension

Contrôler les moteurs

  • Pour piloter les moteurs, on ajoute un contrôleur PWM
  • => On achète des Adafruit DRV8871

On branche tout ça


  • 14V continu
  • 5V continu
  • Data (contrôle PWM)

Et ça marche...

  • ... Jusqu'à ce qu'on inverse le sens du train sans faire exprès.
  • Le 14V entre dans le mauvais sens du convertisseur, crame la carte et le RPi
  • On va rajouter une diode hein 😬


Stabilité

  • On teste et on se rend compte que le réseau électrique n'est pas stable
  • Beaucoup de pièces font passer l'électricité : rail, roues, etc
  • => Plein de micro-coupures qu'on ne voit pas sur un moteur...
  • ... Mais le Pi lui, il redémarre en boucle !
  • On ajoute un condensateur pour lisser le courant.


Et là, ça fonctionne !

Par contre, ça rentre pas

On minifie un peu

  • Impression d'un PCB "shield" pour le Pi

4 semaines plus tard...

Et ça démarre !


  • On pousse un bout de code simple sur le Pi
  • (il fait tourner le moteur à fond et allume la lampe...)
  • Le moteur tourne, la lampe s'allume, GG.


Passons au code


TinyGo

TinyGo

  • Adaptation de Go pour de l'embarqué
  • On retrouve une grande majorité des features & concepts de Go
  • MAIS TinyGo != Go => Certaines librairies ne fonctionneront pas
  • Cycles de vie distinct entre Go et TinyGo

Bon, notre Pi, il gère quoi ?

  • La lumière ;
  • Le moteur ;
  • La communication avec le reste du monde ;
  • Et c'est déjà pas mal.

Good ol' webserver

  • On part sur un classique client/serveur
  • Simple, peu coûteux en ressources
  • Le Pico expose une API
  • Un serveur "central" fait le lien entre un frontend et les trains
  • C'est pas parfait mais c'est une V1... 😉


Dans le train

  • 5 endpoints :
    • Allumer/éteindre la lumière ;
    • Modifier la vitesse & le sens ;
    • Récupérer le statut du train ;
    • Ping !

Dans le train


//--- Constants
var DirectionPWM = machine.PWM1
const DirectionPin = machine.GPIO18
// ...

DirectionPWM.Set(DirectionChannel, 0);
var speed = 50;
SpeedPWM.Set(SpeedChannel, SpeedPWM.Top() / 100 * speed)
            

Dans le train - Watchdog!

  • Quand le train perd l'alimentation > 5s, il devient "zombie"
  • => Besoin de le full-reboot : On découvre les watchdogs !
  • Un dispositif "anti-deadlock" qui permet de reboot si soucis
  • Basé sur une horloge annexe dans le processeur
  • Si on envoie pas de signal pendant 8s, reboot !

Dans le train - Watchdog!

func main() {
	machine.Watchdog.Start()
	go sanity()
}

func sanity() {
	for {
		time.Sleep(time.Second * time.Duration(4))

		if err := checkAlive(); err == nil {
			machine.Watchdog.Update()
			Logger.Info("Sanity check passed")
		}
	}
}

Dans le train

  • On contacte le serveur "central" pour lui dire qu'on est là
  • On initialise tout les capteurs (moteur, lumière, etc)
  • On démarre le watchdog
  • Et on attend les ordres

Le serveur central

  • Pas grand chose à dire, c'est une API REST basique
  • Écrit en Go, et ça tourne sur un Raspberry Pi 3 qui trainait

L'interface graphique


Et ça marche !

Pas trop vite quand même

Car nous avons deux soucis :

  • La communication avec la carte est LENTE (> 300ms) ;
  • Et surtout, on découvre... La draisine modèle 8525.

Jouef 8525

  • Voici notre adversaire : la draisine ! (Modèle 8525)
  • La place à l'intérieur ? 21mm * 42mm * 12mm (l/L/H)
  • Notre montage : 18mm * 21mm * 51mm (approx) 🫠 (l/L/H)
  • Il va falloir (encore) miniaturiser !

Seeed Studio (XIAO)

  • Fabriquant de cartes de très petites tailles
    • Taille standardisée : 21 * 17.8mm
  • Plusieurs variantes (ESP32, RP2040, nRF52480, etc)

Premier essai

  • Le RP2040 n'a pas de connectivité, donc on oublie
  • On tente avec un ESP32C6 (j'en commande 3)
  • Et ça fonctionne plutôt bien ! Mais TinyGo n'est pas compatible 😭
  • => On va mettre cette info dans notre inventaire et on en reparle après

Miniaturiser... Tout le reste.

14V => 5V

  • On trouve un remplaçant : Le Pololu 5V@500mA fixe

22mm * 17mm

=>

7.6mm * 11.4mm

Miniaturiser... Tout le reste.

Contrôle du moteur

  • On trouve un remplaçant : Le TA6586

24mm * 20mm

=>

9mm * 6mm

Et on retourne sur KiCad !

C'est l'heure du shield v2 (et on soude des 2 côtés désormais...)

15 jours plus tard...

Et ça passe !

Mais sans TinyGo 😭

  • Alors je regarde s'il existe une Seeed supportée par TinyGo ET de la connectivité sans fil
  • Un seul candidat potentiel : Seeed nRF52480.
    • mono-coeur 64MHz, 256KB de RAM, 2Mo de Flash... Quasiment notre Pi du départ !
    • Par contre, pas de WiFi : Bluetooth-only !
  • Ça se tente, j'en achète 2 pour voir... Et je découvre le ✨monde merveilleux du Bluetooth (BLE)✨
    • Mais on va peut-être résoudre le problème de latence ?

Bluetooth BLE

Variation du protocole Bluetooth adapté pour l'IoT. Pour le résumer rapidement :

  • Chaque appareil "client" expose un serveur GATT ;
    • Ce serveur expose des services ;
    • Chaque service expose des caractéristiques ;
    • Et on peut lire/écrire dans une caractéristique
  • Le serveur va pouvoir se connecter à plusieurs appareils en simultané, et garder la connexion ouverte en permanence
  • => La latence est donc très faible ! < 1ms en écriture 🎉

Client


  adapter.AddService(&bluetooth.Service{
		UUID: serviceUUID,
		Characteristics: []bluetooth.CharacteristicConfig{
			{
				UUID:  speedCharacteristicUUID,
				Flags: basicFlags,
				WriteEvent: func(client bluetooth.Connection, offset int, value []byte) {
					// On met à jour le moteur ici
				},
			},
		},
	})

	adv.Start();
            

Serveur


  // Detect
  devices, _ := ble.Find(ctx, false, func(a ble.Advertisement) bool {
		return strings.HasPrefix(a.LocalName(), "Trainberry::")
  })

  // Connect
  for _, k := range devices {
    cln, err := ble.Connect(ctx, func(a ble.Advertisement) bool {
			return a.LocalName() == k.LocalName()
    })

    //Send info
    cln.WriteCharacteristic(&ble.Characteristic{ValueHandle: 16}, data, true)
  }
            

Ça reste lent entre le front et le serveur

  • On remplace l'API par une WebSocket
  • On évite toute la latence liée à la stack TCP/IP (ou presque)
  • Et on envoie/reçoit les notifications en temps réel !
  • On garde juste un GET /state pour récupérer l'état initial

Et en plus, ça simplifie !

  • Plus besoin de réseau WiFi, donc :
    • Plus besoin d'avoir un routeur dédié à cet usage ;
    • Plus besoin de traîner une stack TCP/IP partout ;
    • Plus besoin d'avoir des dizaines de paramètres dans la configuration du train

What's next ?

  • Maintenant que les soucis sont résolus, on va pouvoir s'amuser !
    • Annonces en gare ;
    • Faire faire "tchou tchou" aux trains !
    • Stop avec des tags NFC
    • Carte temps-réel
    • Webcam intégrée
    • Passer les aiguillages au numérique
  • Bref : J'ai PLEIN d'idées ! 😁
  • Toujours en Open-Source, toujours en Open-Hardware. ❤️

Pour conclure

Et le making-off de la maquette sur forestier.re !