Catégories
Non classé

Release 0.1.0

It is finally here! After 4 months of development, the first release of ZigRouille is out. The following post will go through what we have done so far, and what still has to be done.

Overview

ZigRouille’s goal is to create a safe Rust interface to the ZBOSS stack on nRF52840 chips. ZBOSS is a proprietary library that integrates a ZigBee communications API. The ZigRouille Rust library has to be split in two parts to connect to this API:

  • zboss-sys, which links the ZBOSS binary and provides bindings to the API entry points.
  • zboss-rs, a higher-level abstraction library to wrap the raw bindings into a new safe API, as well as a Rust implementation of the ZBOSS hardware interface.

To test those libraries, we use a binary crate test-env that also provides a quick-start configuration and several examples using ZigRouille.

Features

The main part of the project resides in zboss-rs: it has a duty to enforce Rust safety rules on the unsafe C API.

Initialization

We enforce at compile time that we have a unique initialization of the ZBOSS stack, and that this stack is configured and started before any other ZBOSS-related code runs.

Memory

The ZBOSS stack holds a buffer pool to perform all ZigBee operations that require memory storage. Those buffers can be requested by the user, or given as a function returns. The buffers do not have an explicit structure in ZBOSS, so we had to structure them explicitly when we use them in the Rust world. Also, the buffers are sometimes required to be explicitly freed by the user, so we enforce that too.

Scheduler

ZBOSS uses serialized callbacks to implement an internal multitasking. We wanted to allow the user to have more variety in the callback types they can use, so we added an extra level of indirection to keep track of the callback requests and responses in the Rust world.

Signals

ZBOSS uses a system of signals to inform the user of some events and allow them to react accordingly. We structured it so that they only have to register individual signal handlers, and implemented a default signal handler (similar to the one Nordic uses).

ZigBee Procedures

Each ZigBee procedure implemented in ZBOSS has its own entry point, so we have to implement them one by one. Therefore, only a small subset off all procedures are currently implemented. We wanted to achieve at least simple light automation for the first release, the other procedures can be added later.

OSIF

The OSIF contains entry points to hardware or OS functions from ZBOSS. For example, the OSIF provides hardware initialization functions called during ZBOSS initialization, or even the code needed to put the device into sleep mode. This OSIF also relies on the radio driver provided by Nordic for the nRF52840 (everything else is written in Rust), that should be linked when building zboss-rs.

What’s next?

The 0.1.0 release is fully functional. However, there still are a lot of features present in ZBOSS that are not yet interfaced with our implementation. In particular, we cannot keep track of some (optional) callbacks given to ZBOSS through special entry points, and we do not handle ZCL callbacks yet. Therefore, it is currently not possible to implement server ZCL clusters e.g. Level Control for a light bulb. Also, ZDO information cannot be retrieved by the user in signal callbacks, a wrapper has to be implemented for that. We would gladly welcome any upcoming contribution! ?

Catégories
Non classé

Fonctions étrangères

Pour réaliser notre projet nous devons pouvoir nous interfacer en Rust avec la bibliothèque C ZBoss. Le C étant beaucoup plus dangereux que le Rust, et différent, de nombreuses précautions sont à prendre. Cet article recense les outils et les bonnes pratiques à suivre pour interagir avec du code étranger dans notre projet.

Caisses intéressantes

cty

Fournit les types C de base (on ne peut pas utiliser libc pour notre cible).

On a les correspondances suivantes :

CctyRust (ARM)
charc_chari8
signed charc_schari8
unsigned charc_ucharu8
shortc_shorti16
unsigned shortc_ushortu16
intc_inti32
unsigned (int)c_uintu32
longc_longi32
unsigned longc_ulongu32
long longc_longlongi64
unsigned long longc_ulonglongu64
floatc_floatf32
doublec_doublef64
XXbool
T *X*mut T
const T *X*const T
void **mut c_void*mut c_void
const void **const c_void*const c_void
size_tsize_tusize
ptrdiff_tptrdiff_tisize
Correspondance des types C, cty et Rust

Bien entendu les types de taille fixe de stdint peuvent être utilisés :

C (stdint.h)ctyRust
int8_tint8_ti8
uint8_tuint8_tu8
int16_tint16_ti16
uint16_tuint16_tu16
uint32_tuint32_tu32
int32_tint32_ti32
int64_tint64_ti64
uint64_tuint64_tu64
Correspondance des types stdint, cty et Rust

cstr_core

Les chaînes de caractères en Rust ne sont pas construites de la même manière qu’en C : il n’y a pas de caractère nul à la fin, et les caractères nuls sont admis au sein de la chaîne.

cstr_core fourni CStr (et CString si l’allocation est activée) comme chaîne se comportant comme en C.

rust-bindgen

A partir des entêtes C (éventuellement C++) d’une bibliothèque, génère les prototypes et les structures de données à l’interface adaptés à Rust.

Note : la caisse peut être utilisée pour la génération (build script, donc utilisé comme dépendance dans Cargo sous [build.dependencies]), puis enlevée des dépendances pour alléger le projet, ou encore utilisée dans une caisse à part du projet dédiée aux bindings.

Attributs utiles

#[link(name = « ext_library »)]

Indique au compilateur que les fonctions d’un bloc extern sont à lier à la bibliothèque donnée.

Exemple :

#[link(name = "git2")]
extern "C" {
    pub fn git_libgit2_init() -> c_int;
    pub fn git_libgit2_shutdown() -> c_int;

    . . .

}

#[repr(C)]

Force l’agencement en mémoire d’une structure à suivre les règles du C (ordre des champs, espacement entre les champs, alignement)

#[repr(C)]
pub struct git_error {
    pub message: *const c_char,
    pub klass: c_int,
}

#repr(align(n))

Force l’alignement sur n bytes, n doit être une puissance de 2.

#[no_mangle]

Empêche le mangling par défaut du symbole, utile pour aider le linker à retrouver les symboles en multi-langage.

Techniques et bonnes pratiques

Unsafe et contrat

Lorsqu’une fonction ou un trait sont déclarés comme unsafe il est impératif de documenter le contrat d’utilisation, c’est-à-dire de préciser le cadre d’utilisation pour ne pas avoir de comportement indéfini.

Exemple dans la bibliothèque standard : la fonction Vec::get_unchecked :

pub unsafe fn get_unchecked<I>(&self, index: I) -> &I::Output
    where
        I: SliceIndex<Self>,
    {
        // SAFETY: the caller must uphold most of the safety requirements for `get_unchecked`;
        // the slice is dereferencable because `self` is a safe reference.
        // The returned pointer is safe because impls of `SliceIndex` have to guarantee that it is.
        unsafe { &*index.get_unchecked(self) }
    }

Ce contrat est essentiel car s’il est violé, cela peut introduire des comportements indéfinis dans du code Rust supposé sûr.

Utiliser une variable globale externe en Rust

Utiliser static dans le bloc extern lié à la bibliothèque externe :

#[link(name = "readline")]
extern {
    static rl_readline_version: libc::c_int;
}

Utiliser une fonction C en Rust

On déclare la fonction dans un bloc extern "C". Tous les appels à celle-ci sont nécessairement unsafe.

La déclaration en C :

// header.h
int git_libgit2_init();

Le code Rust :

// main.rs
extern "C" {
    pub fn git_libgit2_init() -> c_int;
}

fn main() {
    unsafe {
        git_libgit2_init();
    };
}

Fonctions variadiques

On peut utiliser les fonctions variadiques du C en Rust :

extern {
    fn foo(x: i32, ...);
}

fn main() {
    unsafe {
        foo(10, 20, 30, 40, 50);
    }
}

Utiliser une fonction Rust en C

On utilise #[no_mangle] et extern "C" comme vu précédemment.

#[no_mangle]
pub extern "C" fn magic_number() -> c_int {
    42
}

Et on ajoute bien évidemment du côté C :

extern int magic_number();

Panique

A l’extérieur de Rust, les paniques causent un comportement indéfini, il faut donc les éviter. S’il faut quand même paniquer, utiliser catch_unwind.

Pointeur nul

Beaucoup de types Rust ne peuvent pas être nuls, notamment les références et les pointeurs de fonction. Ce n’est en revanche pas le cas en C. Le langage Rust donne une approche simple pour s’interfacer avec le C dans cette situation : pour faire simple, utiliser une Option. La structure sera bien optimisée avec None correspondant à la valeur nulle et Some correspondant à la valeur non-nulle.

CRust
int (*)(int)Option<extern "C" fn(c_int) -> c_int>
data_t *Option<*mut data_t>
Exemples

Libération de la mémoire

On utilise le trait Drop pour appeler les fonctions étrangères qui conviennent à la libération de la mémoire.

Exemple :

impl Drop for Repository {
    fn drop(&mut self) {
        unsafe {
            raw::git_repository_free(self.raw);
        }
    }
}

Structure opaque

Si les entêtes en C décrivent un type opaque :

typedef struct Foo Foo;

Ou bien en argument de fonction :

void bar(void *foo);

Alors on utilise dans le code Rust :

#[repr(C)] pub struct Foo { _private: [u8; 0] }

extern "C" {
    pub fn bar(arg: *mut Foo);
}

On manipulera donc uniquement ces structures avec des pointeurs bruts.

Catégories
Non classé

Terminologie applicative d’un réseau ZigBee

Cet article a pour but de lister les différents concepts liés aux applications disponibles sur les différents nœuds d’un réseau ZigBee. Il n’a pas pour but d’expliquer quels sont les différents types de nœuds d’un réseau, comment celui-ci va se constituer, etc. Ces aspects seront traités dans d’autres articles. Nous allons uniquement nous concentrer sur les nœuds terminaux (pouvant effectuer des actions concrètes, directement utiles à l’utilisateur final).

Un réseau de nœuds interagissants

Dans un réseau ZigBee, des nœuds vont communiquer entre eux afin d’effectuer des actions ou de récupérer des informations. Ces interactions auront généralement des buts communs. La ZigBee Alliance les regroupe dans des Application Profiles. Le tout premier profil défini s’appelle « Home Automation ». Son but est de définir un ensemble de ressources communes pouvant être utile pour faire de la domotique dans un cadre domestique. Les fabricants voulant commercialiser des produits prévus dans ce but ont ainsi un standard auquel se raccrocher. Ces profils précisent, pour un type d’application donnée, ce qu’il peut être possible de faire faire à un nœud, et quelles seront les ressources nécessaires pour cela.

On entend par nœud (node) tout appareil connecté au réseau ZigBee. Un nœud ZigBee peut ainsi être un simple interrupteur qui supportera également un mode de mise à jour. Un nœud peut donc avoir différents rôles.

Les Application Profiles définissent ces ensembles de rôles, qu’on appellera device. Un nœud peut ainsi héberger plusieurs devices. On peut par exemple imaginer un nœud comportant deux devices : un capteur de lumière (Light Sensor) et un capteur d’occupation (Occupancy Sensor). Ces deux devices sont par exemple définis dans le profil Home Automation.

Lorsqu’un client (celui qui va faire la demande) fait effectuer à un device (qui a donc un rôle de serveur) une action, on dit que celui-ci effectue une commande. Si le device nous permet d’accéder ou de modifier une information qui lui est relative (exemple : l’intensité d’une ampoule), on parle d’attribut.

Les clusters définissent un ensemble de commandes (et de comportements associés), d’attributs et de dépendances. La ZigBee Cluster Library (ZCL) définit un grand nombre de clusters qui a vocation à suffire dans la plupart des cas. Chaque fabricant peut ensuite décider, comme pour les Application Profiles ou les devices, d’en redéfinir selon ses besoin.

Définir un device revient donc à lister les clusters que ce dernier implémentera de manière obligatoire ou non.

Par exemple, considérons le device « On/Off Light » du profil Home Automation (section 2.4.1 du document cité). Ce dernier doit supporter différents clusters. Il doit par exemple supporter le cluster « Basic ». Cela veut simplement dire qu’il doit pouvoir renseigner le réseau a propos de quelques uns de ses attributs (dont il doit donc disposer, obligatoirement ou non selon les attributs), tels que le nom de son fabricant, la version de l’application, etc. voire permettre d’en modifier, comme par exemple la description de son emplacement. Les attributs peuvent être des chaînes de caractères, des nombres, ou des énumérations, définies dans la ZCL. Ce cluster ne prend qu’une commande, qui est la réinitialisation du device. Ce device implémente aussi le cluster « On/Off« , qui donne accès entre autres à trois commandes, Off, On et Toggle, qui permettront respectivement d’éteindre, allumer, ou faire basculer l’état de la lampe.

Un endpoint est une instance d’un device sur un nœud donné, adressé avec une valeur comprise entre 1 et 240.

Si on reprend l’exemple de la lampe ci-dessus, et qu’un client veut l’allumer, il aura ainsi simplement à envoyer une requête au nœud correspondant à la lampe, avec le profil Home Automation, le numéro d’endpoint repéré lors de la découvert de service (imaginons que c’est 1 et que c’est le seul device présent sur le nœud), le numéro de cluster correspondant à « On/Off Light », 0x0006, et enfin la commande Off, d’identifiant 0x00, d’après la ZCL.

Nous verrons plus tard comment fonctionne la découverte des services et nous verrons aussi qu’il n’est pas nécessaire aux nœuds d’envoyer à chaque fois ces informations pour parvenir à leurs fins.

Catégories
Non classé

Des messages de commit suivant nos conventions

Ainsi que nous l’avions annoncé dans un précédent post, nous désirons que nos messages de commit suivent les standards de rédaction classiques. Afin de limiter les risques de contrevenir par mégarde à ces règles, plusieurs options s’offrent à nous.

L’option GitLab

Le dépôt distant central, hébergé à Télécom, est un GitLab Community Edition. Gitlab permet de configurer des Push Rules, qui ressemblent à des hooks Git configurables graphiquement et qui nous permettraient de rejeter, lors du push, tout commit ne respectant les règles que nous nous sommes fixées. Malheureusement, la version de Gitlab en place est la version gratuite, qui ne permet pas l’utilisation de cette fonctionnalité. Les hooks classiques peuvent toutefois être configurés, mais cela nécessite un accès administrateur à la machine sur laquelle repose Gitlab (ou du moins à son système de fichiers), ce dont nous ne disposons pas.

Hook local

Après un avoir traversé un court, mais intense, moment de frustration, nous avons décidé de nous rabattre sur une solution alternative : configurer le hook sur nos dépôt locaux. Cette solution permet de rejeter les commits si les messages ne correspondent pas aux règles (ou du moins à certaines) que nous avons fixées.

Cette solution présente quelques petits inconvénients :

  • Il faut compter sur chaque développeur pour configurer le hook en question. Comme nous ne sommes que deux, et si nous partons du principe que nous ne réinstallerons pas nos dépôts tous les 4 matins, cela ne devrait pas poser problème. De toute façon, il est plus agréable que le hook se déclenche au commit plutôt qu’uniquement au push : dans le cas contraire, le développeur est forcé de revenir sur son commit et de modifier son message avant d’espérer que son push soit accepté.
  • Certaines actions, en particulier les fusions, peuvent parfois se dérouler via l’interface graphique fournie par Gitlab, très confortable et pratique pour cela. Lors de la validation d’un commit de fusion via cette interface, les règles correspondant à nos hooks locaux ne seront pas vérifiées. Il faudra être particulièrement vigilant dans ces cas là.

Script retenu

Après quelques recherches, nous avons décidé de tester git-good-commit. Ce dernier a le bon goût de ne pas nécessiter l’installation d’outils supplémentaires. Le script s’assure que les règles basiques (longueur des différentes parties du message, ponctuation) sont respectées, et embarque une liste de mots à bannir : quelques verbes anglais courants au prétérit et à la troisième personne du singulier. Comme nous avons choisi de rédiger nos messages de commit en anglais, cette liste nous permettra d’éviter les erreurs les plus classiques.

Installation

Il suffit de télécharger le script et de le définir comme hook commit-msg (dernier hook déclenché lors de la constitution d’un commit, lorsque l’éditeur du message se ferme) :

curl https://cdn.rawgit.com/tommarshall/git-good-commit/v0.6.1/hook.sh > .git/hooks/commit-msg
chmod +x .git/hooks/commit-msg

À partir de là, après la rédaction d’un message de commit ne correspondant pas aux standards, et si le script peut le détecter, un prompt proposera d’éditer le message, de l’abandonner ou de le forcer tel quel.

Catégories
Non classé

Découverte du matériel et des outils

Le matériel

Nous utilisons deux kits de développement fournis par Nordic :

Le nRF52840 (référence) est construit à partir d’un Cortex-M4, et implémente plusieurs protocoles de communication, notamment ZigBee qui nous intéresse dans notre projet.

Les outils de programmation

Les workshop de Ferrous Systems fournissent des outils de développement en Rust pour compiler les programmes, programmer la carte, recevoir du logging, etc.

L’essentiel

probe-run

C’est l’utilitaire qu’on utilisera le plus : construit l’exécutable (cargo-run), programme la carte en utilisant la sonde JTAG (probe-rs), et récupère les log et les traces de la pile d’exécution via le protocole RTT.

nrfutil

Utilitaire fourni par Nordic, il permet notamment de programmer le dongle (qui ne possède pas de sonde intégrée).

Autres utilitaires

cargo-flash

Permet de programmer la carte en utilisant la sonde JTAG de la carte de développement (utilise probe-rs).

cargo-embed

Pour visualiser les log via le protocole RTT. (peut servir aussi à programmer la carte grâce à probe-rs)

cargo-binutils

Fournit nm, objdump, objcopy, etc.

cargo-bloat

Analyse l’espace utilisé dans un fichier ELF (ou équivalent).

nrf-recover

Pour déverrouiller la flash si nécessaire.

Projet de référence

(voir https://embedded-trainings.ferrous-systems.com/from-scratch.html)

Pour commencer un nouveau projet en Rust sur la cible nRF52840, on utilise le projet de référence fourni ici : https://github.com/rust-embedded/cortex-m-quickstart en paramétrant pour un Cortex-M4.

Pour obtenir un HAL pour la carte, on utilise nrf52840-hal.

Si l’on veut allouer des structures de données sur la pile, on utilise heapless.

On peut ensuite utiliser rtt-target dans le projet pour générer des log qui transitent par la sonde.

Si nécessaire, on utilisera aussi le système d’exploitation RTIC.

Notre projet de référence se trouve sur notre dépôt ici : quickstart_project

Catégories
Non classé

Premiers pas

Et c’est parti ! …ou pas. Avant de commencer à coder quoi que ce soit, quelques règles s’imposent.

Organisation

Même si Sylvain et moi ne sommes que deux, nous nous sommes mis d’accord sur quelques règles pour ne pas nous marcher sur les pieds.

Git, Gitlab, etc.

Première règle absolue : ce qui est sur main est considéré comme étant du code en production. Cela implique que tout code en cours de développement est mis sur une branche à part tant qu’il n’est pas prêt pour la mise en production. On essaiera de faire idéalement une branche par fonctionnalité développée.

Lorsqu’une fonctionnalité est prête à être fusionnée dans main, on fera une merge request dans Gitlab afin d’approuver à deux la fusion.

Concernant les messages de commit, on utilisera les conventions données par git. Les messages sont en anglais et à l’impératif, leur titre ne doit pas faire plus de 50 caractères et le corps doit faire au plus 80 caractères de large.

Suivi de l’avancement

Les issues de Gitlab permettent de discuter sur des bugs ou des fonctionnalités, et peuvent être visualisées comme un kanban. On utilisera donc celles-ci pour suivre le développement des fonctionnalités.

Licence

Comme imposé pour notre projet, tout le code que nous écrirons sera sous licence libre duale Apache License 2.0 / MIT.


En résumé

  • branche main = production
  • Les fonctionnalités sont isolées sur des branches à part
  • On utilise les pull requests pour fusionner avec main
  • Les messages de commit :
    • en anglais
    • titre à l’impératif
    • titre <= 50 caractères
    • corps : lignes de moins de 80 caractères
  • issues Gitlab pour le suivi
  • Licence Apache License 2.0 / MIT

C’est bon on peut y aller ?

Il faut d’abord savoir où l’on va ! Avant de pouvoir développer sur notre carte développement nrf52840 une interface ZBOSS en Rust pour communiquer en ZigBee, il faut se former à chacun de ces éléments. Notre démarche initiale est la suivante :

  1. Beginner workshop de Ferrous Systems
  2. Advanced workshop de Ferrous Systems
  3. Etudier le fonctionnement de ZigBee et de 802.15.4
  4. Prendre en main l’interface ZigBee/ZBOSS de Nordic : https://infocenter.nordicsemi.com/index.jsp?topic=%2Fstruct_sdk%2Fstruct%2Fsdk_thread_zigbee_latest.html&cp=7_3
  5. Réaliser les bindings Rust de ZBOSS

On peut envisager si le temps nous le permet d’aller plus loin :

  • Faire une surcouche haut niveau si l’API ZBOSS est peu pratique
  • Réfléchir à des applications IoT, domotique, etc.
  • Utilisation conjointe de ZigBee et du Bluetooth

Nous avons commencé par prendre en main la carte de développement en réalisant les workshops organisés par Ferrous Systems, mais ça ce sera pour une prochaine fois…