Archives du blog

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