Nos 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) ;


  • 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

DCC au secours

  • 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.

Time flies... 2011 -> 2024


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

On démonte un train...

  • 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)

Et on va builder une v1...

  • Pont abaisseur de tension (car notre Pi supporte 5V, et notre réseau est en 14 18-20V
  • Contrôleur PWM pour le moteur...




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

... Et très vite une v2

  • Car le réseau électrique n'est pas stable : roues, rails, ... => Beaucoup de pièces mouvantes
  • On rajoute un condensateur


Et là, ça fonctionne !

Par contre, ça rentre pas

On minifie un peu

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

Et ça démarre !



Sauf que...

... Nous avons un problème de taille



Alors on retourne miniaturiser

  • Raspberry Pi (51*21mm) => Seeed Studio nRF52840 (21*17mm)
  • Adafruit DRV8871 (24*20mm) => TA6525 (9*6mm)
  • Pont abaisseur de tension générique (22*17mm) => Polulu 5V (7*11mm)


Et ça rentre (au chausse-pied)



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 ?

  • Notre première idée : un client-serveur REST classique. MAIS :
  • Pas de Wi-Fi natif sur TinyGo => On a recodé un truc (sale) nous-même
  • La latence est abominable (Plus de 300ms) ;
  • La carte consomme énormément (le Wi-Fi, c'est gourmand en élec) ;
  • Et notre carte pour la Draisine (le Seeed Studio nRF52840) n'a pas de puce Wi-Fi.


Une solution ? Bluetooth BLE

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 🎉

  • Un serveur peut se connecter à plusieurs clients (ce PC = 8 clients)
  • Et en plus, c'est un protocole très connu et utilisé !

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();
            

Client - Mono-cœur

  • On doit mettre à jour les données du moteur, mais le processeur est déjà occupé avec la communication Bluetooth...
  • Résultat => Une deadlock où plus rien ne se passe

Client - Mono-cœur

  • La solution ? On va ajouter un chan qui stocke les informations pour les traiter après
  • L'impact sur les performances est négligeable

Client - Mono-cœur


// constants.go
var EventChannel = make(chan structures.Event, 5)

// bluetooth.go
// ...
  Characteristics: []bluetooth.CharacteristicConfig{
  {
        UUID:  speedCharacteristicUUID,
        WriteEvent: func(client bluetooth.Connection, offset int, value []byte) {
            constants.EventChannel <- structures.Event{
                Data:      value,
                Operation: constants.SpeedOperation,
            }
            constants.SpeedState = value
        }
	},
// ...
      

Client - Mono-cœur


// main.go
func main() {
  // ...
	for {
		event := <-constants.EventChannel
		switch event.Operation {
		case constants.LightOperation:
			controls.SetLight(event.Data)
			break
		case constants.SpeedOperation:
			controls.SetSpeed(event.Data, reverseBool)
			break
		}
	}
}
      

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)
  }
            

Et ça marche (en prod) !

Et ça marche (en prod) !

Architecture finale

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 : On a PLEIN d'idées ! 😁
  • Toujours en Open-Source, toujours en Open-Hardware. ❤️

Pour conclure...

  • Ce talk montre 1% des difficultés qu'on a pu avoir !
  • On a pas parlé des watchdogs, des cartes sacrifiées sur l'autel des tests, etc
  • Un projet qui nous a fait sortir de nos habitudes et de notre zone de confort
  • Et qui nous a permis d'onboarder des gens très loin de la tech !

Merci !


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