Sommaire
J’ai le plaisir de vous annoncer l’arrivée de la nouvelle libraire Constellation pour Arduino/ESP en version 2.0.
Cette nouvelle version amène beaucoup de nouveautés que je vous propose de découvrir dans cet article. La librairie a été testé avec succès sur différentes cartes animées par un ESP8266 (ESP-01, 05, 07 et D1 Mini) ainsi qu’un Arduino/Genuino MKR1000.
Pour la mise à jour, téléchargez la libraire ci-dessous et dézippez les fichiers dans le dossier “Constellation” dans votre répertoire de libraire Arduino.
Attention : cette version 2.x de la librairie Constellation pour Arduino/ESP nécessite la version 1.8.1.16249 ou plus récente du serveur Constellation pour fonctionner correctement. Pour mettre à jour votre plateforme Constellation, lancez le Web Platform Installer.
Réception de message et StateObject “asynchrone”
Il s’agit d’une grosse évolution par rapport à la version 1.x. Pour “récupérer” du serveur Constellation les messages à destination de votre package virtuel ou les StateObjects pour lequel votre package (virtuel) s’est abonné, il faut invoquer les actions “GetMessages” et “GetStateObjects” sur l’interface REST.
Ces méthodes supportent le “long-polling” c’est à dire qu’elles mettent en attente la requête tant qu’il n’y a pas de messages ou StateObjects à retourner avec la notion de timeout (par défaut 60 secondes).
Le problème sur les versions 1.x de la libraire Arduino est que le client HTTP est “synchrone”, c’est à dire qu’il envoi la requête HTTP et attend la réponse du serveur pour traitement ce qui est incompatible avec du long-polling car cela bloquerait le code de l’Arduino. De ce fait, les anciennes versions spécifiées le “timeout” de ces requêtes à 1 seconde.
Ainsi jusqu’à maintenant, l’interrogation des messages et SO pouvaient bloquer jusqu’à 2 secondes votre Arduino (1 seconde pour le GetMessage et 1 seconde pour le GetStateObject) et engendrait donc 1 requêtes HTTP toutes les secondes entre votre ESP/Arduino et le serveur Constellation.
Dans cette nouvelle version, la librairie utilisent 3 clients HTTP différents : un pour toutes les requêtes synchrones, un pour la réception de message (GetMessage) et un pour la réception des StateObjects (GetStateObjects).
Le client synchrone est donc toujours disponible et il n’y a aucun blocage, aucune attente pour la réception de message et de SO. De plus il n’y a plus besoin de “poller” ou “flooder” le serveur d’une requête toute les secondes, en réactivant le long-polling le 2ème et 3ème client HTTP, on lance une requête toutes les minutes (60 sec par défaut) dans l’attente de message et SO.
De ce fait, il y a quelques modifications à apporter dans votre code !
Premièrement, vous ne devez plus passer la référence de votre client réseau mais plutôt spécifier le type de votre client dans le template de la classe Constellation. Par exemple, si vous utilisez un ESP8266 ou un Arduino MKR1000, vous utiliserez la classe “WifiClient “” :
1 2 |
/* Create the Constellation client */ Constellation<WiFiClient> constellation("constellation.monserveur.com", 8088, "MyVirtualSentine", "MyPackage", "MyAccessKey123!"); |
Ensuite, il n’y a plus besoin d’invoquer la méthode “poll” dans votre boucle principale avec un timer, vous devez tout simplement appeler la méthode “loop” autant de fois que vous voulez :
1 2 3 4 |
void loop(void) { // Process incoming message & StateObject updates constellation.loop(); } |
Cette méthode vérifie en fait si il y a eu une réponse sur le client utilisé pour la réception de message et sur le client pour la réception de SO afin de les dispatcher dans votre code.
Support du PackageDescriptor
Autre nouveauté majeure pour cette version 2, le support du “Package Descriptor”.
Pour rappel le “Package Descriptor” est un objet envoyé par un package au serveur pour décrire ses StateObjects et ses MessageCallbacks exposés par le package.
Le MessageCallback Explorer de la Console Constellation s’appuie sur le “Package Descriptor” de chaque package pour lister chaque MessageCallback avec un formulaire de test. Sans cette description cela serait impossible !
Les versions 1.x ne supportaient pas “Package Descriptor”, de ce fait il n’était pas possible d’explorer/découvrir les MessageCallbacks d’un package Arduino/ESP. Vous devez nécessairement connaitre par vous même les MC exposés par le package et le type des StateObjects qu’il pouvait publier.
Avec la version 2.0, l’Arduino/ESP envoie au serveur la description des MC exposés et des types utilisés par les SO et arguments des MC.
Dans la suite de cet article, vous découvrirez comment ajouter des types dans la description de votre package, mais n’oubliez jamais d’envoyer votre “Package Descriptor” en invoquant la méthode “declarePackageDescriptor”, typiquement une fois votre code Arduino initialisé (par exemple à la fin de la méthode setup()) :
1 2 |
// Declare the package descriptor constellation.declarePackageDescriptor(); |
Amélioration du PushStateObject
La méthode “PushStateObject” permet de publier un StateObject sur le serveur Constellation.
Tout d’abord la méthode expose des surcharges vous permettant de spécifier comme “value” de votre SO des int, uint, double, float, long et bool et supporte en paramètre optionnel le “lifetime”, c’st à dire la durée en seconde de “vie” de votre StateObject avant d’être considéré comme expiré.
1 2 3 4 5 6 7 8 |
// Dummy data uint16_t lux = 123; // Push a simple type on Constellation constellation.pushStateObject("DemoLux", lux); // Push a simple type on Constellation with lifetime of 20 seconds constellation.pushStateObject("DemoLux", lux, 20); |
Vous pouvez toujours publier des StateObjects dont la valeur (value) est un objet complexe (formaté en JSON).
Le JSON peut être formaté sous forme d’une chaine de caractère avec la méthode “stringFormat” :
1 2 |
// Push a complex object on Constellation with stringFormat constellation.pushStateObject("DemoLux", stringFormat("{ 'Lux':%d, 'Broadband':%d, 'IR':%d }", lux, full, ir)); |
Vous pouvez également définir votre propre type pour ce StateObject, par exemple nommons cet objet “MyLuxData” :
1 |
constellation.pushStateObject("DemoLux", stringFormat("{ 'Lux':%d, 'Broadband':%d, 'IR':%d }", lux, full, ir), "MyLuxData"); |
Sans oublier d’ajouter ce type “personnalisé” dans le “package descriptor”. Par exemple, dans le “setup()” et avant de faire un “declarePackageDescriptor”, on aurait décrit “MyLuxData” de la façon suivante :
1 |
constellation.addStateObjectType("MyLuxData", TypeDescriptor().setDescription("MyLuxData demo").addProperty("Broadband", "System.Int32").addProperty("IR", "System.Int32").addProperty("Lux", "System.Int32")); |
Pour finir, vous pouvez toujours publier un StateObject en construisant la valeur dans un objet JsonObject de façon suivante :
1 2 3 4 5 6 7 8 |
// Push a complex object on Constellation with JsonObject const int BUFFER_SIZE = JSON_OBJECT_SIZE(5); StaticJsonBuffer<BUFFER_SIZE> jsonBuffer; JsonObject& myStateObject = jsonBuffer.createObject(); myStateObject["Lux"] = lux; myStateObject["Broadband"] = full; myStateObject["IR"] = ir; constellation.pushStateObject("DemoLux", myStateObject); |
Dans les surcharges de cette méthode, vous pouvez également spécifier le type de votre StateObject et/ou sa durée de vie :
1 |
constellation.pushStateObject("DemoLux", myStateObject, "MyLuxData", 20); |
Pour finir, nouveauté de la librairie 2.0, vous pouvez également spécifier des “meta-données” à vos StateObjects. Par exemple :
1 2 3 4 5 |
// Ajout de metadatas au StateObject JsonObject& metadatas = jsonBuffer.createObject(); metadatas["ChipId"] = ESP.getChipId(); metadatas["Timestamp"] = millis(); constellation.pushStateObject("DemoLux", myStateObject, "MyStateObject", 20, &metadatas); |
Les StateObjectLinks
En version 1.x de la librairie Arduino, vous pouvez :
- interroger des StateObjects de la Constellation en invoquant la méthode “requestStateObjects” qui vous retourne les StateObjects à l’instant T correspondant à votre requête :
1 2 3 4 5 |
// Example : print the all SO's value named "/intelcpu/0/load/0" and produced by the "HWMonitor" package (on all sentinels) JsonArray& cpus = constellation.requestStateObjects("*", "HWMonitor", "/intelcpu/0/load/0"); for(int i=0; i < cpus.size(); i++) { constellation.writeInfo("CPU on %s is currently %d %", cpus[i]["SentinelName"].asString(), cpus[i]["Value"]["Value"].as<float>()); } |
- vous abonnez aux mises à jour des StateObjects pour être notifié en temps réel des que les SO changent :
1 2 3 4 5 6 |
// set a StateObject update callback and subscribe to SO constellation.setStateObjectUpdateCallback([] (JsonObject& so) { constellation.writeInfo("StateObject updated ! StateObject name = %s", so["Name"].asString()); }); // Subscribe to SO named "/intelcpu/0/load/0" and produced by the "HWMonitor" package (on all sentinels) constellation.subscribeToStateObjects("*", "HWMonitor", "/intelcpu/0/load/0"); |
Vous pouvez invoquer plusieurs fois la méthode “subscribeToStateObjects” pour ajouter des abonnements à d’autre StateObjects mais il n’y a qu’un seul callback de réception des SO (défini par la méthode setStateObjectUpdateCallback). C’est à vous de “dispatcher” les SO reçus.
La nouveauté en 2.0 vient de l’ajout de la méthode “registerStateObjectLink” qui vous permet d’associer un callback à un abonnement laissant ainsi à la librairie la charge du “dispatch”.
Par exemple, on peut désormais écrire :
1 2 3 |
constellation.registerStateObjectLink("*", "HWMonitor", "/intelcpu/0/load/0", [](JsonObject& so) { constellation.writeInfo("CPU on %s is currently %d %", so["SentinelName"].asString(), so["Value"]["Value"].as<float>()); }); |
Ici on enregistre un “StateObject Link” sur tous les SO nommés « /intelcpu/0/load/0 » produits par le package “HWMonitor” de toutes les sentinelles de votre Constellation. Dès qu’un de ces StateObjects changent on exécutera le callback associé, qui ici écrit dans le log un message indiquant la nouvelle valeur du CPU.
Vous bien entendu enregistrer autant de StateObjectLink que vous souhaitez.
Enregistrement des MessageCallbacks
Le principe est le même que celui décrit ci-dessus pour les abonnements aux StateObjects.
En version 1.x, on devait définir un callback pour réception de message et invoquer la méthode “subscribeToMessage” :
1 2 3 4 5 6 |
// Set callback for all incoming messages constellation.setMessageReceiveCallback([](JsonObject& json) { constellation.writeInfo("Message receive ! Message key = %s", json["Key"].asString()); }); // Subscribe to message constellation.subscribeToMessage(); |
C’était donc à votre charge de “dispatcher” les messages reçus en fonction du “MessageKey”.
Désormais avec la version 2.0, vous pouvez enregistrer un callback pour un “MessageKey” donné. Par exemple :
1 2 3 4 |
constellation.registerMessageCallback("HelloWorld", [](JsonObject& json) { constellation.writeInfo("Hello Constellation !"); }); |
Dans l’exemple ci-dessous, on ajoute/expose un MessageCallback “HelloWorld” qui écrit dans les logs !
Il n’y donc plus besoin de “dispatcher”, un MessageCallback est donc associé à un “MessageKey” unique et un callback. Il n’y a plus besoin non plus de faire un “subscribeToMessage” (cette méthode est invoquée implicitement par le registerMessageCallback).
De plus cette méthode ajoute implicitement les MessageCallbacks dans le Package Descriptor de sorte que chaque MC soit ainsi référencé dans la Constellation
(console)
Vous pouvez également passer un “MessageCallbackDescriptor” dans l’enregistrement de vos MC pour ajouter une description par exemple :
1 2 3 4 5 |
constellation.registerMessageCallback("HelloWorld", MessageCallbackDescriptor().setDescription("Say Hello to Constellation"), [](JsonObject& json) { constellation.writeInfo("Hello Constellation !"); }); |
Et même définir les paramètres de vos MC :
1 2 3 4 5 |
constellation.registerMessageCallback("SayHello", MessageCallbackDescriptor().setDescription("Say hello !").addParameter("FirstName", "System.String").addParameter("LastName", "System.String"), [](JsonObject& json) { constellation.writeInfo("Hello %s %s", json["Data"][0].asString(), json["Data"][1].asString()); }); |
Ici on déclare un MC nommé “SayHello” prenant deux paramètres de type String avec un texte de description. Dans la code nous verrons :
(console)
Tout comme les StateObjects, les types des arguments des MC peuvent être des types complexes.
Par exemple enregistrons un MC prenant un seul argument de type “SampleUserData” que nous allons décrire avec la méthode “addMessageCallbackType” comme un objet composé de deux propriétés de type String :
1 2 3 4 5 6 7 8 |
// Expose a MessageCallback with complex parameter : constellation.registerMessageCallback("SayHello2", MessageCallbackDescriptor().setDescription("Say hello with complex object!").addParameter("User", "SampleUserData"), [](JsonObject& json) { constellation.writeInfo("Hello %s %s", json["Data"][0]["FirstName"].asString(), json["Data"][0]["LastName"].asString()); }); // and describe the complex parameter "SampleUserData" constellation.addMessageCallbackType("SampleUserData", TypeDescriptor().setDescription("This is a smaple user data").addProperty("FirstName", "System.String").addProperty("LastName", "System.String")); |
Enfin la méthode “registerMessageCallback” accepte également des callbacks prenant en paramètre le “MessageContext” :
1 2 3 4 5 |
constellation.registerMessageCallback("HelloWorld", MessageCallbackDescriptor().setDescription("Say Hello to Constellation"), [](JsonObject& json, MessageContext ctx) { constellation.writeInfo("Message received from %s", ctx.sender.friendlyName); }); |
L’objet “MessageContext” est défini de la façon suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
typedef struct { const char* sagaId; bool isSaga; const char* messageKey; MessageSender sender; ScopeType scope; } MessageContext; typedef struct { SenderType type; const char* friendlyName; const char* connectionId; } MessageSender; |
Vous pouvez donc récupérer les informations sur l’émetteur (Sender) du message, le scope et l’identifiant de Saga si c’est une saga (isSaga = true).
Support des Saga (messages avec réponse)
Une saga est un identifiant unique qu’on affecte à des messages pour les lier entre eux. Cela permet de faire des couples de message “Requête / Réponse”. Une “réponse” étant un message renvoyé à l’émetteur de la requête avec le même identifiant de saga pour que ce dernier puisse l’identifier comme la “réponse” à son message d’origine.
Vous pouvez exposer des MessageCallbacks qui “répondent”, c’est à dire des MessageCallbacks qui retournent un message de réponse.
Par exemple, exposons un MC pour réaliser des Additions sur un Arduino. Vous enregistrerez un MC “Addition” prenant en parametre deux entiers et qui en retourne un (le résultat) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
constellation.registerMessageCallback("Addition", MessageCallbackDescriptor().setDescription("Do addition on this tiny device").addParameter("a", "System.Int32").addParameter("b", "System.Int32").setReturnType("System.Int32"), [](JsonObject& json, MessageContext ctx) { int a = json["Data"][0].as<int>(); int b = json["Data"][1].as<int>(); int result = a + b; constellation.writeInfo("Addition %d + %d = %d", a, b, result); if(ctx.isSaga) { constellation.writeInfo("Doing additoon for %s (sagaId: %s)", ctx.sender.friendlyName, ctx.sagaId); // Return the result : constellation.sendResponse<int>(ctx, result); } else { constellation.writeInfo("No saga, no response !"); } }); |
Vous remarquerez dans le “MessageCallbackDescriptor” l’appel de la méthode “setReturnType” pour spécifier le type de retour (et que le fait qu’il s’agit d’un MC acceptant les sagas).
L’envoi de la réponse étant réalisé par la méthode sendReponse. Cette méthode a plusieurs surcharges :
1 2 3 |
template<typename T> bool sendResponse(MessageContext context, T data); bool sendResponse(MessageContext context, const char* data, ...); bool sendResponse(MessageContext context, JsonObject& data); |
Notez aussi que vous devriez faire un “sendResponse” si et seulement le message reçu est associé à une saga, c’est à dire que le champs “isSaga” du contexte (MessageContext) est “vrai”. Sinon ca ne sert à rien de répondre
Pour finir, cette nouvelle version supporte également l’envoi de message dans une saga. C’est à dire que vous pouvez envoyer un message qui attend une réponse et donc enregistrer un callback de traitement de la réponse.
Par exemple le package “NetworkTools” expose un MC “Ping” permettant de faire un ping. Le package retourne dans la saga un message contenant le temps en milliseconde du ping. On pourrait alors invoquer cette méthode depuis notre Arduino de la façon suivante :
1 2 3 4 5 |
const char* ip = "192.168.0.10"; // Send message in a saga and attach a callback for response : constellation.sendMessageWithSaga([](JsonObject& json) { constellation.writeInfo("Ping response in %s ms", json["Data"].asString()); }, Package, "NetworkTools", "Ping", "[ '%s' ]", ip); |
Ici on envoi un message au scope “Package” pour atteindre le(x) package(s) “NetworkTools” afin d’invoquer le MessageCallback “Ping” en passant en argument l’IP à pinger. Vous remarquerez qu’on utilise le formatage implicite des arguments.
La méthode n’est pas la traditionnelle “sendMessage” mais “sendMessageWithSaga” qui prend un argument supplémentaire : le callback de traitement de la réponse.
Ainsi quand le package “NetworkTools” répondra, on executera le callback associé qui ici affichera le temps de réponse de notre ping (Data) dans les logs Constellation (writeInfo).
De ce fait, avec cette nouvelle libraire vos Arduino/ESP peuvent invoquer des MC dans sagas pour exploiter la réponse mais également exposer des MC qui retournent des résultats.
Autres nouveautés
En vrac, vous disposez maintenant une méthode “purgeStateObjects” permettant de supprimer des StateObjects de votre package.
De plus la méthode “setDebugMode” accepte en argument l’énumération “DebugMode” composée des valeurs suivantes :
- “Quiet” (mode silencieux, la libraire ne produit aucune trace dans l’interface Serial)
- “Normal” (mode par défaut, écrit quelques informations dans l’interface Serial)
- “Verbose” (écrit beaucoup d’information dans l’interface Serial comme par exemple les requêtes et réponse HTTP v ers Constellation)
Enfin les exemples fournis dans la libraire ont été complètement revus.
Salut Sébastien, le lien vers la librairie semble être ko
Au temps pour moi, on peut l’avoir directement depuis le gestionnaire de libs.