Catégories
Recherche technologique

openThread et GrennPower – Environnement de développement

Comme beaucoup (trop) de développeur, mon pc de travail fonctionne sous Windows 10 or openThread fait appel à des outils Linux pour compiler. Dans la suite je vais vous décrire mon installation qui me permet de modifier le code dans un IDE digne de ce nom, de compiler ce même code comme si j’étais sous Linux et enfin de le programmer dans la cible avec les outils natif à mon os fournit par les fabricants de composants.

Environnement de compilation

Le plus simple est d’utiliser docker, d’autant qu’une image existe déjà !

Installation de docker sous Windows 10

Pas de difficulté particulière si ce n’est qu’il faut une version professionnel de Windows 10. Le lien vers la page de téléchargement de Docker Desktop : https://www.docker.com/get-started

A la fin de l’installation on ouvre une Invite de commandes Windows (pas le powershell) et l’on peut vérifier que docker est en route en lui demandant ça version :

Nous allons ensuite chercher l’image docker qui nous intéresse :

docker pull openthread/environment:latest

Pour faciliter la modification ultérieur des fichier sources depuis un IDE sous Windows nous allons activer le partage de disque sous docker. Pour cela on va dans la partie « Setting/Ressources/File sharing » de docker desktop et l’on sélectionne le disque que l’on souhaite partager :

J’ai créé un répertoire spécifique de partage sur mon disque « c » de Windows à l’emplacement « C:_DEV\docker_share » et pour simplifier la suite il sera monté à l’emplacement « /mnt » de l’image Linux. Il est temps de lancer un container :

docker run -it -v c:/_DEV/docker_share:/mnt --rm openthread/environment bash

Dans ce shell Linux nous allons effectuer un enchainement de commandes :

  • positionnement dans le répertoire partagé :

cd /mnt

  • Clone du repo Git :

git clone https://github.com/openthread/openthread

  • On se place dans le répertoire cloné :

cd openthread

  • Installation de la toolchain et autres dépendances :

./script/bootstrap

  • Setup de l’environnement :

./bootstrap

Nous voilà prêt à effectuer la compilation de notre premier firmware, pour ce préparer à l’étape suivante qui est la mise en place d’un sniffer, nous allons construire le firmware nécessaire pour la carte de développement de chez Nordic « nRF52840-DK » :

make -f examples/Makefile-nrf52840 USB=1

le binaire généré ce trouve dans le répertoire « /mnt/openthread/output/nrf52840/bin » mais a encore besoin d’une transformation pour le mettre au format hex :

arm-none-eabi-objcopy -O ihex output/nrf52840/bin/ot-rcp output/nrf52840/bin/ot-rcp.hex

Il ne reste plus qu’à utiliser nRf Programmer depuis Windows pour charger ce nouveau firmware dans la cible.

Liens utiles

Listes des liens qui ont permis d’obtenir cette synthèse :

Catégories
Recherche technologique

openThread et GreenPower – Mise en place d’un sniffer

Installation de wireshark

Charger l’installeur et l’exécuter : https://www.wireshark.org/#download

Installation de pyspinel

  • Premièrement installer python3 pour Windows et ne pas oublier de cocher l’ajout au PATH :
  • Dans un terminal Windows mettre installer les dépendances utiles et pyspinel :
    • pip3 install pyserial ipaddress pyspinel
  • Ce placer dans un répertoire de travail et cloner le repo openthread/pyspinel :
    • git clone https://github.com/openthread/pyspinel

Lancer la session de sniffing …

On commence par connecter la carte nRF52840 précédemment flashée avec le firmware ot-rcp.hex par l’usb de debug au PC. A l’aide du gestionnaire de périphérique on note le numéro du port série virtuel associé, COM3 dans mon cas.

Le mini réseau thread sera créé sur le canal 15, on décide de placer le sniffer en écoute sur celui-ci par la commande suivante :

python sniffer.py -c 15 -u COM3 --crc --no-reset --rssi | "C:\Program Files\Wireshark\wireshark" -k -i -

La fenêtre de wireshark s’ouvre et l’on peut lire sur notre terminal :

Liens utilisés

Catégories
Recherche technologique

openThread et GreenPower – Réseau à deux noeuds

L’objectif est d’avoir un réseau composé d’un leader et d’un routeur, de pouvoir effectué un ping du routeur sur le leader et une communication de type coap. Pour cela nous allons générer un firmware de type Full Thread Device pilotable grâce à la CLIdepuis un terminal série. Le matériel utilisé sera deux cartes Silicon Labs EFR32MG12.

Générer le firmware

Pour pouvoir générer un firmware Silicon Labs il est nécessaire de charger et installer simplicity studio pour pouvoir récupérer la stack flex contenue dans le sdk 2.7.

Lien vers la page de chargement de simplicity studio : https://www.silabs.com/products/development-tools/software/simplicity-studio

Une fois simplicity studio installé et la stack Flex SDK v2.7 téléchargée, il faut la copier du répertoire ou elle se trouve (C:\SiliconLabs\SimplicityStudio\v4\developer\sdks\gecko_sdk_suite) dans le repertoire third party de la stack openthread (C:_DEV\docker_share\openthread\third_party\silabs\gecko_sdk_suite).

Depuis notre terminal docker :

make -f examples/Makefile-efr32mg12 COMMISSIONER=1 COAP=1 JOINER=1 DHCP6_CLIENT=1 DHCP6_SERVER=1 BOARD=BRD4161A

Puis on transforme notre binaire au format hex :

arm-none-eabi-objcopy -O ihex output/efr32mg12/bin/ot-cli-ftd output/efr32mg12/bin/ot-cli-ftd.hex

Depuis Windows on éxecute l’outils Silicon Labs pour charger les firmware dans la cible : commander (C:\SiliconLabs\SimplicityStudio\v4\developer\adapter_packs\commander) et l’on charge nos deux cartes avec notre firmware fraichement compilé.

Construire le réseau et configurer le sniffer

A l’aide du gestionnaire de périphérique on note les ports séries associés à nos cartes. Dans mon cas les port COM11 et COM12. On ouvre un terminal série par port avec la configuration suivante : 115200 8-N-1

Configuration de la première carte (COM11) pour devenir le leader du réseau.

Initialisation d’un dataset et affichage des valeurs :

dataset init new

Le canal radio choisi est le 20, or nous avons configurer notre sniffer pour écouter sur le canal 15, on change la valeur du canal dans notre dataset pour se positionner sur le canal 15.

dataset channel 15

Maintenant que le dataset nous conviens, on le rend actif, puis on démarre l’interface réseau et la stack thread. au bout d’un quelque secondes lorsque l’on demande son état elle nous informe qu’elle est le leader.

Configuration du sniffer

Plusieurs protocoles sont à configurer en utilisant les valeurs fournies par le dataset du leader.

6LoWPAN

CoAP

IEEE 802.15.4

Thread

Configuration de la deuxième carte (COM12) en tant que routeur

Contrairement à la première carte, on ne va renseigner que la « master key » dans le dataset avant d’activer l’interface et de démarrer la stack.

dataset masterkey 9d7ddcb4408be470e2240256feaf9cf2

La trace wireshark de l’entrée réseau :

Test – Ping

Un premier test consiste à envoyer un ping en ICMPv6 du routeur vers le leader et d’observer la transcation :

ping fdad:574c:3814:8442:9c44:e832:a11e:4939

Test – UDP

Le routeur va envoyer un message « hello » en udp sur un port du leader qui aura été ouvert :

Leader – Ouverture du port en écoute

udp open

udp bind :: 1234

Routeur – Envoie du message « hello »

udp open

udp send fdad:574c:3814:8442:9c44:e832:a11e:4939 1234 hello

Leader – Résultat

Wireshark – Trace

Test – CoAP

L’exemple fournit ne pose plus de problème à présent et peut être suivi sans modification : https://github.com/openthread/openthread/blob/master/src/cli/README_COAP.md

Liens

Liste des liens qui ont permis de réaliser cet article :

Catégories
Recherche technologique

openThread et GreenPower – DotDot

Et pourquoi pas CHIP ? (https://www.connectedhomeip.com/) Et bien tout simplement car aucune spécification n’est encore publique actuellement !

Les documents de référence

L’objectif

L’objectif n’est pas d’implémenter l’ensemble du protocole dotdot mais le strict minimum pour passer à l’étape suivante : le pilotage depuis une commande green power. Pour se faire seul la commande toggle du cluster on/off sera implémentée sur le premier endpoint de l’actionneur. Seront donc ignoré :

  • La découverte des produits et des ressources depuis le /.well-known et de fait la gestion de l’UID produit.
  • Tous les mécanismes de sécurité.
  • La gestion du multicast, seul un post unicast sera géré par le serveur.
  • Les bindings et autres tokens

On peut donc résumé que le client enverra un post coap depuis la cli, le serveur quand à implémentera ce post sur la ressource « /zcl/e/1/s6/c/2 » qui correspond a la commande toggle du cluster on/off sur l’enpoint 1 du produit. Commande qui n’a pas besoin de payload.

Le code !

Jusqu’à présent nous avons compilé le projet exemple qui donne accès à une ligne de commande. Pour réaliser notre objectif on définira le leader comme l’actionneur, on ne touchera pas au code du routeur. Pour simplifier l’on va modifier le projet example cli pour :

  • créé automatiquement le réseau en tant que leader
  • activé coap et lui ajouter la ressource zcl/e/1/s6/c/2
  • ajouter un handler sur cette ressource
  • imprimer sur la sortie RTT les informations permettant de s’assurer que toute les étapes ce sont bien passée
  • a chaque fois que le handler de la ressource coap est appelée changé d’état une variable représentant la charge et l’afficher.
void CoAPHandleRequest(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
 {
     static bool light_on_state = false;
     OT_UNUSED_VARIABLE(aContext);
     OT_UNUSED_VARIABLE(aMessage);
     OT_UNUSED_VARIABLE(aMessageInfo);

     otPlatLog(OT_LOG_LEVEL_NONE, 
               OT_LOG_REGION_PLATFORM, 
               "Light %s",light_on_state?"ON":"OFF");
     light_on_state = !light_on_state;
}
int main(int argc, char *argv[])
 {
     otInstance *instance;
...

// configure node as leader
otError lError;

otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "Create new network : dataset ");
otOperationalDataset aDataset;
uint8_t demoMasterKey[OT_MASTER_KEY_SIZE] = {0x9d,0x7d,0xdc,0xb4,0x40,0x8b,0xe4,0x70,0xe2,0x24,0x02,0x56,0xfe,0xaf,0x9c,0xf2};

if( OT_ERROR_NONE == otDatasetCreateNewNetwork(instance, &aDataset) )
{
    otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "SUCCESS, set active ");
    aDataset.mChannel = 15;
    memcpy(aDataset.mMasterKey.m8, demoMasterKey, OT_MASTER_KEY_SIZE);
    if( OT_ERROR_NONE == otDatasetSetActive(instance, &aDataset) )
    {
        otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "SUCCESS, enable IPv6 ");
        if( OT_ERROR_NONE == otIp6SetEnabled(instance, true) )
        {
            otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "SUCCESS, start as leader ");
            lError = otThreadSetEnabled(instance,true);
            if( OT_ERROR_NONE == lError )
            {
                otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "SUCCESS\n");
                const otNetifAddress *unicastAddrs = otIp6GetUnicastAddresses(instance);

                for (const otNetifAddress *addr = unicastAddrs; addr; addr = addr->mNext)
                {
                    otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM,
                        "%x:%x:%x:%x:%x:%x:%x:%x", tSwap16(addr->mAddress.mFields.m16[0]), tSwap16(addr->mAddress.mFields.m16[1]),
                        tSwap16(addr->mAddress.mFields.m16[2]), tSwap16(addr->mAddress.mFields.m16[3]), tSwap16(addr->mAddress.mFields.m16[4]),
                        tSwap16(addr->mAddress.mFields.m16[5]), tSwap16(addr->mAddress.mFields.m16[6]), tSwap16(addr->mAddress.mFields.m16[7]));
                }                    
            }
            else { otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "FAILED %s\n", otThreadErrorToString(lError)); }    
        }
        else { otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "FAILED\n"); }    
    }
    else { otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "FAILED\n"); }    
}
else { otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "FAILED\n"); }    


// Initialize coap service
otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "CoAP start : ");
if( OT_ERROR_NONE == otCoapStart(instance, OT_DEFAULT_COAP_PORT) )
{
    otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "SUCCESS\n");

    // add ressources
    otCoapResource mResource;
    char zclToggleRscr[] = "zcl/e/1/s6/c/2";
    memset(&mResource, 0, sizeof(mResource));
    mResource.mUriPath = zclToggleRscr;
    mResource.mHandler = CoAPHandleRequest;

    otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "CoAP add ressource : ");
    lError = otCoapAddResource(instance, &mResource);
    if( OT_ERROR_NONE == lError )
    {
        otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "SUCCESS\n");
    }
    else { otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "FAILED %s\n", otThreadErrorToString(lError)); }    
}
else
{
    otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "FAILED\n");
}


while (!otSysPseudoResetWasRequested())
{
    ...

return 0;
}

Et le résultat :

Catégories
Recherche technologique

openThread et GreenPower – GreenPower

Pour le dernier article de cette étude, nous allons voir comment recevoir une trame green power issue d’une commande Philips Tap (https://zigbeealliance.org/zigbee_products/philips-hue-tap/) puis la transformer pour la renvoyer sous forme d’un message DotDot.

Méthodologie:

  • L’actionneur sera le leader et conserve le code développé précédemment.
  • Le nœud enfant contiendra le proxy Green Power et sera également modifier pour rejoindre automatiquement le réseau créé par le leader.
  • Pour simplifier l’adresse ipv6 du leader sera codée en dur dans l’enfant pour l’envoie du message CoAP.
  • La sécurité Green Power est ignorée pour cette étude. La trame issue de la commande Philips Tap contien l’ordre en clair.

La trame Green Power

Sans chercher à décoder entièrement la trame Green Power pour notre étude, nous utiliserons les filtres suivants:

  • Au moins 24 octets de long.
  • Le frame control 802.15.4 doit valoir 0x0801.
  • Le frame control du header network doit être de type data et la version du protocole doit être 3 (green power).
  • Le frame counter de la trame reçue doit être supérieur au frame counter de la trame précédente.
  • L’ordre green power doit être 0x22 (Toggle)

Le code du nœud enfant

Rejoindre le réseau et activer le handler de réception 802.15.4 à placer dans la fonction main avant la boucle infinie.

    // enable promiscuous mode for capture Grenn Power Frame
    // set callback
    otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "Set Pcap callback.");
    otLinkSetPcapCallback(instance, &PromicuousHandleLinkPcapReceive, 0);
    
    // join existing network
    // set master key
    memset(&aDataset, 0, sizeof(aDataset));
    memcpy(aDataset.mMasterKey.m8, demoMasterKey, sizeof(aDataset.mMasterKey));
    aDataset.mComponents.mIsMasterKeyPresent = true;
    otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "Commit dataset : ");
    lError = otDatasetSetActive(instance, &aDataset);
    if( OT_ERROR_NONE == lError )
    {
        otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "SUCCESS, enable IPv6 ");
        if( OT_ERROR_NONE == otIp6SetEnabled(instance, true) )
        {
            otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "SUCCESS, start as router ");
            lError = otThreadSetEnabled(instance,true);
            if( OT_ERROR_NONE == lError )
            {
                otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "SUCCESS\n");
                const otNetifAddress *unicastAddrs = otIp6GetUnicastAddresses(instance);

                for (const otNetifAddress *addr = unicastAddrs; addr; addr = addr->mNext)
                {
                    otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM,
                        "%x:%x:%x:%x:%x:%x:%x:%x", tSwap16(addr->mAddress.mFields.m16[0]), tSwap16(addr->mAddress.mFields.m16[1]),
                        tSwap16(addr->mAddress.mFields.m16[2]), tSwap16(addr->mAddress.mFields.m16[3]), tSwap16(addr->mAddress.mFields.m16[4]),
                        tSwap16(addr->mAddress.mFields.m16[5]), tSwap16(addr->mAddress.mFields.m16[6]), tSwap16(addr->mAddress.mFields.m16[7]));
                }                    

                // Initialize coap service
                otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "CoAP start : ");
                lError = otCoapStart(instance, OT_DEFAULT_COAP_PORT);
                if( OT_ERROR_NONE == lError )
                {
                    otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "SUCCESS\n");
                }
                else { otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "FAILED %s\n", otThreadErrorToString(lError)); }    
            }
            else { otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "FAILED %s\n", otThreadErrorToString(lError)); }    
        }
        else { otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "FAILED\n"); }    
    }
    else { otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "FAILED %s\n", otThreadErrorToString(lError)); }    

Filtrer les messages 802.15.4 pour n’être réactif qu’à la trame Green Power qui nous intéresse.

void PromicuousHandleLinkPcapReceive(const otRadioFrame *aFrame, bool aIsTx, void *aContext)
{
    OT_UNUSED_VARIABLE(aContext);

    // receive frame only
    if( !aIsTx )
    {
        otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "Rx %d bytes on channel %d, at time %d",
                                                                aFrame->mLength,aFrame->mChannel,aFrame->mInfo.mRxInfo.mTimestamp);
        // get only frame starting 0x01 0x08 : frame type data without security and short addressing mode fot destination, minimal length shall be 24
        if( (aFrame->mLength>=24) && (aFrame->mPsdu[0]==0x01) && (aFrame->mPsdu[1]==0x08) )
        {
            // green power use data frame with protocol 3, use it only
            if( (aFrame->mPsdu[7]&0x0F)==0x0C )
            {
                static uint32_t ls_previous_frm_counter = 0;
                otError lError;
                otMessage *   message = NULL;
                otMessageInfo messageInfo;  
                otIp6Address coapDestinationIp;              
                uint32_t l_src_id = aFrame->mPsdu[9] + (aFrame->mPsdu[10]<<8) + (aFrame->mPsdu[11]<<16) + (aFrame->mPsdu[12]<<24);
                uint32_t l_frm_counter = aFrame->mPsdu[13] + (aFrame->mPsdu[14]<<8) + (aFrame->mPsdu[15]<<16) + (aFrame->mPsdu[16]<<24);
                uint8_t l_cmd_id = aFrame->mPsdu[17];
                
                otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "GPF from %08X, frame counter %d, command %02X", l_src_id, l_frm_counter, l_cmd_id);

                // send CoAP message to leader only if frame counter is higher than previous message and only toggle (0x22) GP order
                if( (0x22 == l_cmd_id) && (l_frm_counter > ls_previous_frm_counter) )
                {
                    ls_previous_frm_counter = l_frm_counter;
                    message = otCoapNewMessage(instance, NULL);
                    otCoapMessageInit(message, OT_COAP_TYPE_NON_CONFIRMABLE, OT_COAP_CODE_POST);
                    otCoapMessageGenerateToken(message, 2);
                    otCoapMessageAppendUriPathOptions(message, "zcl/e/1/s6/c/2");
                    otIp6AddressFromString("fd85:74ee:7560:9626:dd0e:3508:db6:44c9", &coapDestinationIp);
                    memset(&messageInfo, 0, sizeof(messageInfo));
                    messageInfo.mPeerAddr = coapDestinationIp;
                    messageInfo.mPeerPort = OT_DEFAULT_COAP_PORT;                

                    otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "Send CoAP message : ");
                    lError = otCoapSendRequestWithParameters(instance, message, &messageInfo, NULL, NULL,NULL);
                    if( OT_ERROR_NONE == lError )
                    {
                        otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "SUCCESS\n");
                    }
                    else { otPlatLog(OT_LOG_LEVEL_NONE, OT_LOG_REGION_PLATFORM, "FAILED %s\n", otThreadErrorToString(lError)); }                   
                }
            }
        }
    }
}

Le résultat

Conclusion

Tout d’abord POURQUOI ? Pourquoi vouloir utilisé des devices Green Power dans un réseau thread ? Pour plusieurs raison :

  • Thread possède un type de device Low Energy MAIS il est encore trop consommateur par rapport à un device Green Power
  • Une commande Green Power peut devenir indépendante du type de réseau mesh 802.15.4 en place (Zigbee ou Thread).

Et la sécurité ? En effet Thread est un réseau sécurisé dans toutes les étape de sa vie. Pour cela il oblige l’utilisation d’un border routeur et d’un outils commissioneur, généralement un smartphone. Green Power contient bien des niveaux de sécurités dont un qui permet d’obtenir le même résultat que la sécurité mise en place dans Thread. Grâce à l’outil on peut venir entrer la clé de sécurité du device Green Power que l’on souhaite utiliser directement dans les proxy/actionneur concerné.

Pour résumer seul les devices Green Power Mono directionnel avec une sécurité de type install code et un frame counter incrémentiel présente un intérêt d’utilisation dans un réseau Thread. En effet ils permettent de combler le manque de spécification d’un device vraiment low power sans dégrader le niveau de sécurité du système.