mardi 20 août 2019

Serialisation JSON, essayons d'en faire une librairie

Mon petit code de sérialisation en JSON du behavior tree m'a finalement donné l'intérêt de tester la création d'une librairie. J'ai donc commencé le projet JSONSerialWriter qui j'espère pourra devenir une librairie supportée par l'IDE sur plusieurs architectures. Il y a sûrement d'autre librairie JSON me direz-vous. Bin kin, y a en pas mal en fait et ArduinoJSON est excellente, mais elles sont toutes un peu trop gourmante en mémoire à mon goût. Comme la sérialisation de graph est assez verbose et que je veux utiliser le moins de RAM possible, tout spécialement sur les Uno/Nano/Micro,  je crois qu'il y a une niche pour ce projet. Il n'est pas exclus d'utiliser plus d'une librairie sdans un même projet aussi ( ici, ça ne sérialise, ça ne parse pas).

Mais sincèrement, le but est de se faire les dents à la création d'une librairie.

Que sont les prérequis pour être inclus dans le "library manager"?


Voici sommairement ce qui est nécessaire pour qu'une librarie soit incluse dans le manager de l'IDE d'Arduino.
  • Doit être héberger sur un site de gestion de source tel github, gitlab et bitbucket.
  • Doit avoir à la racine du repo les fichiers qui seront installer sous Arduino/librairies/[nomdelalibrairie]
  • Le fichier librairy,property doit être présent
  • La numéro de version est sous la forme major.minor.revision (semver)
  • Le fichier README.adoc doit décrire la librairie
  • Le fichier keywords doit définir les mots clés à "highliter" dans l'IDE
  • Le fichier .development ne doit pas être présent ( mais utile pour le dévelopment )
  • Le code source doit être accessible à partir de la racine, quoi que les dernières version supporte le répertoire /src
  • Les examples sont contenus dans le sous-répertoire /examples
  • Si supporté, utiliser le mécanisme de "release" pour spécifier les nouvelles versions
  • Écrire dans le forum d'Arduino pour annoncer la nouvelle librairie.

 Une fois la librairie inscrite, un sytème automatisé monitore le repo et mets à jours assez rapidement les nouvelles versions ( peut prendre quelques heures ).

Où trouver l'information complète pour le support du dévelopment de librairies



Fonctionnalités et utilisation de la librairie


Une seule classe est accessible via la libraire et c'est JSONSerialWriter. Les méthodes sont relativement évidentes dans leur fonctionnalité. Puisque c'est un sérializer qui écrit de manière synchrone dans le stream, l'ordre d'appel est important. C'est aussi à l'utilisateur de gérer la hiérarchie des objets sauvegardés. Par défaut, c'est l'objet Serial qui est assigné comme stream de sorti. Il est cependant possible d'assigner tout autre classe dérivant de Print pour la sérialisation.

Comme le but est d'être le moins gourmand en mémoire, la classe ne contient que trois booléens, un int et un pointeur vers le Print a utiliser. Très probable que plus tard dans le développement, le tout soit encore plus compact. Puisque les appels ne garde pas vraiment d'états ni de buffer, c'est tout aussi compact lors de l'exécution.


JSONSerialWriter writer;
writer.startWriter();
writer.writeValue( "temperature", 10 );
writer.writeValue( F("humidity"), 22.87f );
writer.writeValue( F("winddirection"), F("North"));
writer.closeWriter();

Le résultat èa la sorti sera:

{"temperature":10,"humidity":22.8700,"winddirection":"North"}


À noter que l'utliliation des string en flash F(str) est important pour sauvegarder de la mémoire.

Cédule de développement

Bien qu'il puisse y avoir pas mal de fonctionnalités utilitaires à cette librairie, j'espère faire un release rapidement. J'imagine qu'il est possible de sortir une version avant la 1.0.0 . Si c'est le cas, tant que le tout est fonctionnel, compilable sur AVR et megaAVR, ce sera parfait comme ça.

Dans les ajouts qui viendront par la suite, je prévois:

  • Support pour écrire directement dans le stream.
  • Permettre d'écrire la valeur de chaine de caractère en plusieurs appels.
  • Compiler pour les architecture sam et samd
  • Quelques méthodes utilitaires pour facilité la vie avec les arrays.
  • Optimisation 



jeudi 8 août 2019

JSON, Flask et "Quality of life" pour Arduino

J'essaie d'implémenter la gestion d'évènements et les utiliser comme déclencheur mais le tout devient un peu tannant de déboguer avec seulement un output texte et des nombres comme identifiant. J'ai donc décider de me faire une meilleure visualisation du Behavior Tree.

Pour y arriver, je veux:
  • Un sortie standardisée facile à décoder
  • Un système simple permettant de se connecter sur le port série et lire le contenu
  •  Du code portable et rapide a implémenter
Pour ce qui est de l'output, j'ai décider de le faire en JSON. Cependant, comme c'est un arbre et qu'il peut y avoir pas mal d'info a sérialiser, une librairie comme ArduinoJSON bien qu'excellente, nécessite de générer un "document" qui sera sérialisé. C'est trop gourmand, donc j'ai écrit un serialiseur "in place" qui envoit directement sur Serial l'information.

Pour le moment la classe de sérialisation est incluse dans le code de Behavior Tree mais je pourrais en faire une librairie simpliste qui j'imagine pourrait être utile à condition de permettre de spécifier le stream de sortie ( Wifi, BT, RF module y compris ) et d'y ajouter le support pour les chaines de caractères en progmem.

Ensuite, le projet python Flask permet de réaliser un petit site web dont des url permettent de se connecter, déconnecter et lire le port série. C'est encore en chantier mais c'est prometteur. Ce pourrait être pratique si configuré pour répondre autre que sur localhost et monitorer à distance ou sur appareil mobile sur le même réseau.

Comme c'est en Python ça n'utilise que Flask, pySerial, le code est relativement simple et surtout portable. Comme l'output série est déjà en JSON, c'est simple à "parser" et pourrait même être décoder côté client. Il me reste à faire une configuration pour l'affichage selon le type et ce sera déjà pas mal mieux que regarder le "serial monitor".

C'est encore un "work in progress", mais le code est dispo sur github

samedi 3 août 2019

Blackboard et Behavior Tree

Les noeuds n'ont pas vraiment de mémoire excepté les 2 octets qui sert de paramètre. Qui plus est, il difficile d'échanger des données entre les noeuds. Pour y arriver, le concept de "tableau noir" est implémenté. C'est un espace disponible pour tous les noeuds d'un arbre afin d'y laisser de l'information qui peut être lu et modifié par n'importer quel autre noeud. Ici, comme l'espace est limité, la clé est composée que d'un octet et le nombre d'espace dans le tableau doit lui aussi être limité. Seulement 2 octets peut être assigné comme valeur. Ici encore, un pool d'objet d'une taille immuable est définie et utilisé pour l'entièreté de l'exécution. Une valeur de 255 indique un espace libre dans le tableau, donc ne doit pas être utilisé comme clé.

Deux actions de behavior de base sont ajoutées, soit l'assignation et le retrait d'élément au tableau. Puisque seulement 2 octets sont disponible comme paramètre aux action, la valeur assignée est limité à un octet. Ce sont des actions utilitaires, les actions propriétaires pourront mieux utilisé l'espace.

Puisque l'espace est si limité, le blackboard n'est pas fait pour stocker l'ensemble du monde comme pourrait être fait dans une système plus performant. Un robot pourrait encoder les valeur des ses senseur dans une structure plus performante qui serait disponible lors de l'exécution de noeuds propriétaires. Cependant, c'est une structure généraliste qui est pratique.

La classe Blackboard est simple à utiliser. Les méthode set et get permettent l'assignation et l'accès aux valeurs disponible. La méthode releaseElement permet de retirer un élément précis et hasKey retourne si un l'élément existe dans le tableau. L'action SetBBValue aura le statut de SUCCESS si l'assignation réussi et  FAILURE s'il n'y a plus de place dans le tableau.

Prochaine étape, l'utilisation d'évènements pour déclencher l'exécution de sous-arbres.

dimanche 28 juillet 2019

Behavior Tree : Première version pour Arduino Uno

On a tendance à ne pas vouloir montrer notre travail tant que ce n'est pas terminé. La peur de la critique ou tout simplement qu'avoir le nez dans un projet ne nous fait voir que les défaut et les manquements. Tout ça pour dire que j'ai décidé de diffuser la première version fonctionnelle de mon projet de Behavior Tree pour Arduino malgré sont état encore prototype. Vous pouvez cloner le projet sur github.

Voici ce qui le projet en date du 28 juillet supporte:
  • Classe de gestion du graph de l'arbre
  • Classe visiteur pour naviguer l'arbre
  •  Désérialisation a partir de la mémoire Flash et en RAM
  • Banque de sous-arbre pour facilité la désérialisation
  • Affichage de l'état de l'arbre via le lien série
  • Noeuds de composition: sequence, selector, random, parallel et loop
  • Noeud de décoration: Success, Failure, Inverter
  • Noeud de désérialisation: Proxy
  • Noeud de débogage: Delay et Print
  • Noeud spécifique au projet: setLED 
  • Quelque méthodes pour effectuer différent tests. 
  • Classe utilitaire pour la gestion de DEL, boutons et alarmes
Les méthodes "degugPrint" envoient sur le port série l'état de l'arbre et des noeuds. L'indice, le type, la domnée, le statut, la priorité, l'id de l'enfant et l'id du suivant sont afficher dans hiérarchique, séparés par des deux point ":". Des tirets avant le type permet de connaitre la profondeur du noeud dans l'arbre. Comme l'arbre est parcouru à partir de la racine, l'affichage permet de visualiser rapidement les liens et l'état de l'ensemble des noeuds de l'arbre.

La sérialisation utilise 4 octets par noeud selon le schéma:

Octet 0 - type du noeud
Octet 1 - octet data 0
Octet 2 - octet data 1
Octet 3 - déplacement du prochain noeud

Le déplacement utilise les constantes suivantes:

0: Fin de l'arbre
1: Ajout à la suite
2: Ajout en tant qu'enfant
-X : remonte de X dans l'arbre

L'utilisation d'une valeur négative évite d'encoder la fin d'une branche ou de détecter selon le type si un enfant est possible ou non. C'est un peu plus pointilleux sur la désérialisation mais moins gourmand en mémoire.

Ce qui reste à implémenter:

  • Compilation pour Arduino d'architecture SAMD .
  • Meilleur gestion des type de base ( utiliser uint_8 au lieu de byte, etc. )
  • Utilisation d'un "Blackboard" dont les clés sont une valeur entre 0 et 254.
  • Permettre le déclenchement de sous-arbre selon  un système d'évènements.
  • Désérialisation à partir d'une carte SD ou via une réception ( Série, RF) et stockage en EEPROM
  • Transfert du code d'exécution des noeuds de base hors du fichier de projet ( .ino ).
    • Soit par une surcharge de la classe ou d'appel de méthode recevant en paramètre les objets nécessaire.
  • Permettre d'interrompre le parcours après un certains délais et reprendre au prochain tick.
  • Gestion de la priorité des sous-arbre afin d'être réclamé par une sérialisation plus importante .
  • Optimisation!
  • Code python pour facilité la création de sous-arbre.
  • Code python + Web pour afficher les sous-arbre.
    • Aide à l'édition.
    • Au runtime en lisant le contenu de la connexion série..
Une fois le blacboard implémenté, possible que je commence un petit projet de robot 2 roues pour tester le behavior tree sur un projet moins théorique, mais je garderai ce projet comme projet de base pour les tests et la documentation. D'ailleur, je me demande comment en faire une librairie puisque le code du handler doit être étendu. Enfin, c'est un creusage de méninges pour un autre jour.

vendredi 5 juillet 2019

PROGMEM et Behavior Tree, la suite

Puisque des parties de l'arbre pourront être désérialisées à la demande dans le projet de Behavior Tree pour Arduino, il est nécessaire d'avoir un mécanisme d'accès pour se facilité la vie. Dans mon cas, le type de noeud "proxy" à la comportement suivant:

  • Premier appel: désérialise selon une valeur en "data" le sous-arbre en tant qu'enfant.
  • En traitement, appel le traitement de l'enfant
  • Sur "Success" ou "Failure" de l'enfant, prend l'état et détruit le sous-arbre.

De cette manière, toute une partie de l'arbre n'existe pas en mémoire et n'est que présent que lors de son exécution. Comme l'élément "data" d'un noeud n'est un int, il est nécessaire de pouvoir adresser une liste de noeuds et d'en instancier au besoin.

Pour ce faire, j'ai créer la classe BehaviorBank qui permet de gérer une liste d'éléments et de répondre à la question de la taille sans devoir en parcourir le contenue. Pour le moment, l'implémentation de l'arbre et des classes utilitaire est faite de façon naïve sant me soucier de l'optimisation ou de la gestion d'erreur. La défition de la classe est la suivante:

 
class BehaviorBank
{
  int totalElements;
  const byte* behaviorIndexesSizes;
  const int* behaviorIndexes;
  const char* behaviorDatas;
public:
  void init( const byte* sizes, const int* indexes, const char* datas, int total );
  byte getNbrNodes( int idx );
  char* getDataPtr( int idx );
};
 

Ici, trois tableaux permettent de garder en mémoire Flash un ensemble de sous-arbre. L'indice dans le tableau et le nombre de noeud par sous-arbre permettent d'extraire selon un indice passé en paramère le pointeur vers les donnée. Le nombre d'indice total est passé en paramètre lors de l'inisialisation.

Les données sont présentement encoder comme suit:

const PROGMEM byte BBankSizes[] = {3, 3};
const PROGMEM int BBankIndexes[] = { 0, 12 };
const PROGMEM char BBankData[] = {1, 0, 0, 2, 20, 11, 0, 1, 20, 22, 
    0, 0, 1, 0, 0, 2, 20, 33, 0, 1, 20, 44, 0, 0};

 Et l'initialisation ressemble à:

BehaviorBank bBank;
bBank.init( BBankSizes, BBankIndexes, BBankData, 2 );

L'implémentation des méthode getNbrNodes et getDataPtr est la suivante:

byte BehaviorBank::getNbrNodes( int idx )
{
  byte ret = 0;
  if ( idx <= this->totalElements )
  {
    ret = pgm_read_byte_near( this->behaviorIndexesSizes + idx );
  }
  return ret;
}

char* BehaviorBank::getDataPtr( int idx )
{
   if ( idx <= this->totalElements )
  {
    int pos = pgm_read_word_near(this->behaviorIndexes + idx);
    return this->behaviorDatas + pos;
  }
  return NULL;
}

Ensuite, le code lors de la désérialisation dans le noeud "proxy" ayant accès à cette instance de BehaviorBank est:

if ( this->bBank.getNbrNodes(mePtr->data) <= this->b_tree.getFreeNodes() )
{
  char* behavSer = this->bBank.getDataPtr( mePtr->data );
  if ( this->b_tree.deserialize_flash( this->visitor.getIndex(), behavSer ) == false )
    return false;
} 

Il reste encore un peu de nettoyage dans mon projet avant de le partager mais il est maintenant fonctionniel avec un ensemble restreint d'éléments de test. Il me reste aussi à le faire compiler pour architecture SAMD en utilisant quelque directive de compilation.


dimanche 23 juin 2019

Arduino progmem : Différence entre UNO et SAMD21

Puisque la RAM est si limitée sur un Arduino, il est important de l'utiliser à bon escient et surtout utiliser toute technique qui permet de tout simplement éviter de l'utiliser tout simplement. Un Arduino Uno utilisant un microcontroleur ATMEGA 328P n'a que 2 KByte de RAM mais 32 KBytes de mémoire Flash. Tout le programme compilé réside en Flash et bien qu'il soit possible d'avoir tellement de code en utilisant plein de librairie pour remplir ce 32KByte, la plupart du temps c'est pas mal vide là-dedans.

Il est possible d'ajouter des données dans cette espace Flash tant qu'elles sont utilisées qu'en lecture seule. Cependant, il faut ramener en RAM octet par octet pour en lire le contenue. La directive progmem indique au compilateur d'ajouter une variable en mémoire Flash et non en RAM.

Ex:

const PROGMEM char str_flash[] = "Je suis en memoire FLASH!";

 Ici, les 26 octets utilisés par cette chaine de caractères résident dans l'espace mémoire Flash et non en RAM, alors que:

const char str_flash[] = "Je suis en memoire FLASH!";

Réduit de 26 octets la RAM disponible et ce pour toute la durée d'exécution du programme.
Le compilateur inclus déjà une macro F() pour simplifier cette technique et les classe dérivant de Print tel de Serial permettent de passer en paramètre un pointeur vers un chaine de caractère en progmem.

Ex:

Serial.println(F("Je suis en memoire FLASH!"));

Il est a noter que le compilateur ne semble pas détecter que deux chaines identiques pourraient être stockées qu'une seule fois en mémoire, donc:

Serial.println(F("Je suis en memoire FLASH!"));
Serial.println(F("Je suis en memoire FLASH!"));

Utilise 52 octets en mémoire Flash puisque la chaine est stocké en deux endroits séparés.

Pour ce qui est d'accéder directement à une donnée, il est nécessaire d'utiliser les méthodes  tel que pgm_read_byte_near et pgm_read_word_near. Ces méthodes permettent de ramener en RAM un type de données sotcké en Flash. L'ensemble des méthodes disponibles est défini dans le fichier , qui est en passant nécessaire d'ajouter à votre programme. Puisse que ces méthodes nécessitent un pointeur vers la données, prenons par exemple:

const PROGMEM byte  untableudebyte[] = { 1, 2, 3, 4, 5 ,6 ,7, 8};

Afin de lire le 3e élément, il faut augmenter le pointeur de 2 et demander une lecture d'un seul octet à cette adresse:

byte byteEnRAM = pgm_read_byte_near(untableudebyte + 2);

la valeur de byteEnRAM sera maintennt de 3. Évidemment qu'il faut être à l'aise avec l'arithmétique de pointeur, ce qui est un sujet en soit. Mais si vous avez de long texte, de grosses tables de valeurs pré-calculées ou tout autre blob binaire devant être fourni tel qu'une image pour un écran, c'est vraiment pratique.

Ceci dit, toute cette mécanique n'est pas nécessaire sur les Arduino à base d'architecture ARM. Dès qu'une variable est déclarée const, elle est automatiquement stockée seulement qu'en Flash et l'adressage permet l'accès direct, donc pas de pgm_read_byte_near nécessaire. Si vous écrivez une librairie, il est possible de mettre le code selon différente architecture dans des sous-dossier, par contre si vous utilisez cette technique dans le .ino, il est nécessaire d'utiliser des directive du préprocesseur tel que #idef __AVR_ATmega328P__ pour identifier le genre d'architecture et inclure le code compatible.

Tout ce petit volet sur l'utilisation est utile pour la sérialisation des behavior tree. Ce sera le sujet du prochain post, avec un peu plus d'exemples concrets cette fois. 

mardi 18 juin 2019

Behavior Tree et Arduino, la suite

Comme j'ai peu de temps à mettre sur le projet, le code n'est pas encore terminé mais la structure principale est là. Cependant, avec le peu de RAM d'un Arduino UNO, il est impératif de pouvoir utilisé un mécanisme de proxy et de désérialisation au besoin. Il y a donc un type de noeud "proxy" qui désérialise lorsque demandé d'être exécutée et détruit les sous-éléments lorsque le résultat de l'enfant est disponible ( fail ou success ). 

Une étape plus loin serait de permettre une gestion de priorité et retirer une sous-structure si elle est jugée moins importante qu'une nouvelle devant être instanciée. La structure courante d'un nœud de l'arbre est la suivante:

class BehaviorTreeNode
{
public:
  int data;
  byte type;
  byte state;
  byte child;
  byte next;

};

Il y a donc 255 sortes de nœuds possibles, le state identifie si le nœud est running, fail, success ou encore non touché ou peut être remis dans le pool de non utilisé.Il serait possible d'utilisé 4 bits pour l'état et les 4 autre pour 16 niveaux de priorités.

Je veux permettre d’interrompre le parcours de l'arbre après un certains délais, et reprendre au prochain "tick", ce qui en ferai un type de système multitâche préemptif. Cependant, si des sections complète de l'arbre peuvent disparaitre en plein milieu de l'exécution, il faudra y faire bien attention.

Autres contraintes, un arbres est limité à 255 nœuds, quoi que trop pour la RAM d'un UNO, pourrait être utile pour un MEGA ou Zero. Aussi, il n'y a que 16 bits pour l'encodage des données. Soit deux 8 bits ou un entier. Les 8 bits utilisés pour l'indice de l'enfant pourrait être utilisés comme valeur temporaire pour une tâche n'ayant pas d'enfant mais devrait être refléter dans le state.

En passant, comme pour utiliser les données en Flash sur un UNO il est nécessaire d'utilisé la directive progmem et le méthodes de la famille des pgm_read_word_near, il y aura deux méthode pour la désérialisation, peut-être même trois si je décide de supporter les cartes SD. La version Flash pour microcontrôleur ARM ( SAMD ), ne fera que pointer version la version en RAM puisque les ARM peuvent adressé directement la Flash et n'a besoin que de lecture seule.

Ceci dit, avant d'implémenter de nouveau concept, je vais terminer la version de base. Il me reste quelques méthodes de parcours puis je pourrait commiter.