Cloner une télécommande Radio Fréquence (433MHz) – Part 3 Le cas Blyss

3615 Ma Life ou chronique d’une débâcle annoncée

Ce vendredi je suis passé chez Casto***** et j’ai acheté un pack de 3 prises radio commandées « Blyss » dans le but de les commandes à distance par le montage précédent.
Après avoir pris connaissance du fonctionnement normal des prises avec le montage d’acquisition (via Audacity) je me suis rendu compte que les trames envoyées changeaient à chaque envoi (tout du moins une partie …). Aye !

La cavallerie

Heureusement de bienveillants hackers (que leur noms soient glorifiés !) avaient déjà fait l’analyse du protocole :
http://skyduino.wordpress.com/2012/07/17/hack-partie-1-reverse-engineering-des-interrupteurs-domotique-blyss/
http://skyduino.wordpress.com/2012/07/19/hack-partie-2-reverse-engineering-des-interrupteurs-domotique-blyss/
http://forum.arduino.cc/index.php?topic=110677.0

Tout est dit dans les articles sur la méthode d’analyse et sur les format de données :

Analyse de protocole

Je copie ici le format de la trame :

- entête 2.4ms HIGH,
- code fixe : 0xFE (8 bits),
- canal global (4 bits), ( canal A = 0, canal B = 1, canal C = 2, canal D = 3 )
- adresse (16 bits), (qui identifie le bouton de la télécommande)
- sous canal (4 bits), (canal 1 = 8, canal 2 = 4, canal 3 = 2, canal 4 = 1, canal 5 = 3, tout les canaux = 0)
- état lumière (4 bits), (0 = éteins, 1 = allumé )
- rolling code (8 bits), ( 0x98 -> 0xDA -> 0x1E -> 0xE6 -> 0x67, à chaque nouvelle trame on passe au rolling code suivant  )
- timestamp (0 ~ 255), (8 bits), (en mettant n'importe quoi dedans ça semble marcher quand même, ce qui va simplifier la chose)
- footer 24ms LOW

Du Code !

Voici donc le code python correspondant pour commander une prise Blyss à partir de mon Pi.
J’ai nommé le script ci dessous Blyss.py, mais maintenant vous faites comme vous voulez 😉

#!/usr/bin/python
# -*- coding: utf-8 -*-

import RPi.GPIO as GPIO
import time
import sys

from os.path import expanduser

class Blyss :

        # Table des identifiants programmés dans les prises
        tableauBoutons = { "1" : "1000010111011010"
                                          , "2": "1000010111011011"
                                          , "3": "1000010111011100"
                                          }
        # Table des codes roulants trouvés par les hackers d'arduino.cc
        # http://skyduino.wordpress.com/2012/07/17/hack-partie-1-reverse-engineering-des-interrupteurs-domotique-blyss/
        # http://skyduino.wordpress.com/2012/07/19/hack-partie-2-reverse-engineering-des-interrupteurs-domotique-blyss/
        tableRollingCode = [ 0x67, 0x98, 0xDA, 0x1E, 0xE6]
        echantillonnage = 44100
        paramBits =     { 0 : [ [ 25, 0 ], [ 12, 1 ] ] ,
                                  1 : [ [ 12, 0 ], [ 25, 1 ] ] }
        dataPin    = 17
        nb_retry   = 7
        # Nombre de secondes d'attente entre deux retry
        attenteEntrePaquets = 0.024

        path2FileIdxLastRollingCode = expanduser("~") + "/.lastRCBlyss.idx"
        idRollingCode = 0

        def __init__(self, dataPin ):
                self.dataPin = dataPin

                # A stocker dans un fichier
                try :
                        idRollingCodeFile = open( self.path2FileIdxLastRollingCode , 'r')
                        self.idRollingCode     = int ( idRollingCodeFile.readline() )
                        self.idRollingCode    += 1
                        if self.idRollingCode >= len( self.tableRollingCode ):
                                self.idRollingCode = 0
                        idRollingCodeFile.close()
                except :
                        self.idRollingCode = 0

                GPIO.setwarnings(False)
                GPIO.setmode(GPIO.BCM)
                GPIO.setup(self.dataPin, GPIO.OUT)

        def sendDataPulse( self, dataPin, duration, value ):
                if value == 1 :
                        GPIO.output(dataPin, GPIO.HIGH)
                else:
                        GPIO.output(dataPin, GPIO.LOW)
                time.sleep( duration )

       def genereTrame( self, identifiantTelecommande, canal, sousCanal, etatLumiere ):
                trame = ""
                # empreinte 0xFE (8 bits),
                trame += "11111110"
                # canal global (4 bits),
                trame += canal
                # adresse (16 bits),
                trame += identifiantTelecommande
                # sous canal (4 bits),
                trame += sousCanal
                # état lumière (état logique) (4 bits),
                trame += etatLumiere
                # rolling code, MSBFIRST (8 bits),
                trame += "{0:08b}".format( self.tableRollingCode[ self.idRollingCode ] )
                # timestamp incrémentiel (0 ~ 255), MSBFIRST (8 bits),
                trame += "00000011"
                return trame

        def send( self, button, action ):

                if button in self.tableauBoutons:
                        identifiantTelecommande = self.tableauBoutons[ button ];
                else:
                        print "Boutton non défini"
                        exit( 1)
                # canal : a paramètrer ?
                canal = "0000"
                # sous canal : a paramètrer ?
                sousCanal = "1000"

                if action == "ON" :
                        etatLumiere = "0000"
                elif action == "OFF" :
                        etatLumiere = "0001"
                else :
                        print "l'action doit être ON ou OFF"
                        exit( 1)

                trame = self.genereTrame( identifiantTelecommande, canal, sousCanal, etatLumiere )
                print trame

                for i in range( 0, self.nb_retry ) :
                        # Envoi d'un HIGH pendant 2.4ms
                        self.sendDataPulse( self.dataPin, 0.0024 , 1 )

                        for bit in trame:
                                for paramBit in self.paramBits[ int( bit ) ]:
                                        self.sendDataPulse ( self.dataPin, 1.0 * paramBit[0]/self.echantillonnage, paramBit[1])
                        # Envoi d'un LOW pendant 0.24ms
                        self.sendDataPulse( self.dataPin, 0.00024 , 0 )

                        time.sleep( self.attenteEntrePaquets )

        def __del__(self):
                GPIO.cleanup()
                path2FileIdxLastRollingCode = open( self.path2FileIdxLastRollingCode , 'w')
                path2FileIdxLastRollingCode.writelines( str( self.idRollingCode ) )
                path2FileIdxLastRollingCode.close()

if __name__ == "__main__" :
        button = "1"
        action = "ON"

        if len(sys.argv) > 1:
                button = sys.argv[1]
                action = sys.argv[2]
        else :
                print "arguments incorrects :"
                print sys.argv[0] + "<id> <ON|OFF>"
                sys.exit(1)
        rf = Blyss( 17 )
        rf.send( button, action )

Ce programme doit être lancé en tant que root (comme toujours dés qu’on utilise le gpio sur le Pi).
Il stocke le dernier rolling code utilisé dans un fichier de config dans /root.

D’après mes tests, l’adresse est libre (vous pouvez mettre ce que vous voulez), il faut juste l’apparier avec la prise (comme pour une télécommande « normale »). Dans le code c’est « tableauBoutons » qu’il faut modifier si vous voulez mettre l’identifiant de votre télécommande (c’est de cette manière que j’ai commencé mes tests).
Utiliser plusieurs télécommandes pour une seule prise fonctionne, du coup je penses que le rolling code semble géré par adresse (d’ailleurs il y a peut être une limite au nombres de télécommandes d’une prise ?). Je ne m’explique cependant tout les tenants et les aboutissants de ce rolling code, si une prise est hors tension ou hors portée elle peut, amha, être désynchronisée (ça expliquerait pourquoi on a que 5 codes, quelques appuis sur une touche permettent de resynchroniser les compteurs).
La zone timestamp est inutile, je l’ai fixée à 00000010 et je n’y touche pas ( j’ai cru a une défaillance à cause d’une valorisation à 0, mais le problème venait d’ailleurs, mais j’ai laissé 0000010, ça marche chez moi 😉 )
La trame d’envoi doit être répétée ( la télécommande analysée le fait 7 fois !) et le temps entre deux répétitions (24ms) n’est pas neutre. Je l’ai modifié et du coup ça ne marchait plus. Donc on reste sur 24ms

Travail sous Licence Demerdenzizich

C’est un travail « brut de fonderie » que je livre ici, je ne trouve pas le système très fiable pour l’instant.
J’ai eu des trames tres déformées à un moment de la journée, sans être capable de savoir si ça venait du raspberry, du transmetteur RF ou de l’antenne.
Le fait que cela fonctionne mal si on a une attente de plus de 24ms n’est pas très rassurante, il suffit que le raspi rame pour que les temps s’envolent.
A voir si j’arrive a faire communiquer un attiny avec un raspi (envoi de la trame à envoyer via GPIO, l’attiny s’occupant de l’envoi).

L’antenne

D’ailleurs en parlant de l’antenne j’ai testé avec un fil de 17 cm. J’ai fait des tests avec une antenne d’un mettre qui n’a pas semblé améliorer les perfs, mais il faut que je refasse des tests à ce sujet, c’est peut être tombé en même temps que les problèmes généré en modifiant le temps d’attente entre réémission de deux trames (cf ci dessus).

Domoticz

Pour piloter tout cela j’ai installé le logiciel libre domoticz. Après avoir tâtonné dans leur interface( pas forcement très simple de premier abord, mais ça semble très puissant).
J’ai pu demander l’allumage et l’extinction des interrupteurs en lui faisant appeler les scripts déjà réalisés (Blyss.py et RF.py déjà décrit dans la partie 2). J’ai aussi vu pour spécifier des appels

Publicités

Publié le 10 juin 2014, dans Non Trié, Raspberry Pi, et tagué , , , . Bookmarquez ce permalien. 5 Commentaires.

  1. Juste pour info en complément de ton très bon article, une antenne ne se fait pas au pifomètre, ça se calcule. Chaque fréquence appartient à un lambda (longueur d’onde) genre la cibi en 27mhz ou les ptits talkywalky sont sur la bande des 11m. Plus tu montes en freq, à plus tu rétrécies le lambda. Donc, que tu aies mis une antenne de 1m, de 10m ou de 15cm, ça ne changera pas l’amélioration de la qualité du signal ou la puissance d’émission.
    A 433Mhz, il te restera à calculer ce dont tu as besoin, de tête grosso merdo, t’as une self de 1cm sur 6 spire en diam de 1cm et le brin rayonnant qui devrait faire dans les 7 cm. A savoir que la plupart des antennes utilisées en 433 sont des dipoles ou doublets, càd un brin rayonnant et son opposé en plan de masse équivalent à la longueur du brin rayonnant. Bon, toussa bien lu, te reste à mettre en oeuvre le schtreumel…

  2. Bonjour,
    très bon article et fort bien décrit 🙂
    tu utilise cela pour une module de la marque « BLYSS » mais est-ce une norme qui leur est propre ou est-ce la norme CHACON ? et du coup as tu eu l’occasion de le tester sur des module de la marque DI-o ?
    pour dire vrais j’ai des module DI-O avec code tournant.. et donc impossible via le RPI de les commander. et avant de me lancer dans des recherches et derivé (don mes connaissance limité en ce domaine risque fort de solder mes investigations par un échec ..) peut être que tu as des pistes à me proposer ?
    pour info je commande des modules de la marque OTIO avec mon RPI (code fixe) et cela marche parfaitement… mais on ne trouve plus de module code fixe sur le marcher…

    et pour ce qui est de l’antenne (s’il n’est pas un peu tard …) j’ai pris un fil de 17,3 cm et de 2mm² que j’ai mis en spire sur un diamètre de 5mm.. et j’ai de bon résultat .

  3. Hello, super travail ! Merci. Si tu aime Apple et Homekit, j’ai adapté ton code et encapsulé le tout sur un appareil Homebridge 😉 ça se trouve ici > https://www.npmjs.com/package/homebridge-programmableswitch

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :