Sommaire
Pour exposer des méthodes de votre Arduino/ESP dans la Constellation, vous pouvez enregistrer et par la même occasion déclarer auprès de Constellation des MessageCallbacks avec la méthode registerMessageCallback.
Enregistrer un MessageCallback
Par exemple enregistrons un MessageCallback “HelloWorld” via une expression lambda C++ à l’initialisation de votre Arduino/ESP (c’est à dire dans la méthode setup()):
1 2 3 4 |
constellation.registerMessageCallback("HelloWorld", [](JsonObject& json) { constellation.writeInfo("Hello Constellation !"); }); |
Enfin dans la boucle principal (loop), invoquez la méthode “constellation.loop” pour traiter et dispatcher les messages reçu par votre package virtuel :
1 |
constellation.loop(); |
Dès lors que votre Arduino/ESP recevra un message dont la clé (MessageKey) est “HelloWorld”, votre méthode associée (représentée ci-dessus par une lambda) sera invoquée.
Décrire son MessageCallback
Le MessageCallback enregistré ci-dessus est dit “caché”, car il n’est pas référencé sur Constellation ! Vous pouvez bien sur envoyer un message “HelloWorld” à votre package pour invoquer votre fonction lambda mais cela suppose que vous connaissez par avance que votre package a enregistré le MessageCallback ci-dessus nommé “HelloWorld”.
Pour enregistrer ET déclarer un MessageCallback vous devez ajouter un “MessageCallbackDescriptor” dans les arguments de la méthode. Par exemple pour reprendre notre exemple “HelloWorld” :
1 2 3 4 5 |
constellation.registerMessageCallback("HelloWorld", MessageCallbackDescriptor().setDescription("Hello World !"), [](JsonObject& json) { constellation.writeInfo("Hello Constellation !"); }); |
On a ici ajouté un “MessageCallbackDescriptor” en spécifiant la description de notre MC. Pour que la description du MC soit envoyée à Constellation, vous devez nécessairement publier le “Package Descriptor” à la fin de l’initialisation de votre Arduino/ESP par la ligne :
1 |
constellation.declarePackageDescriptor(); |
Maintenant, depuis la Console Constellation, ouvrez le “MessageCallback Explorer” et vous retrouvez un MC nommé “HelloWorld” pour votre package :
Vous pouvez cliquer sur le bouton “Invoke” pour envoyer un message “HelloWorld” au package “MyVirtualPackage” et donc invoquer le code de la lambda.
On retrouvera donc dans la Console Log Constellation un “Hello Constellation” tel que programmé dans la lambda (via un constellation.writeInfo) :
Récupérer le contexte
Vous pouvez également récupérer le contexte de réception du message dans la fonction liée en ajoutant un argument « MessageContext » dans la signature du callback :
1 |
(*)(JsonObject&, MessageContext); |
Le type “MessageContext” décrit de la façon suivante :
1 2 3 4 5 6 7 |
typedef struct { const char* sagaId; bool isSaga; const char* messageKey; MessageSender sender; ScopeType scope; } MessageContext; |
Avec :
1 2 3 4 5 6 7 8 9 10 11 12 |
enum SenderType : uint8_t { ConsumerHub = 0, ConsumerHttp = 1, ConstellationHub = 2, ConstellationHttp = 3 }; typedef struct { SenderType type; const char* friendlyName; const char* connectionId; } MessageSender; |
Ainsi vous pouvez obtenir des informations sur le contexte de réception du message à savoir :
- Si le message est dans une saga, si oui obtenir l’identifiant de la saga
- Connaitre le scope dans lequel a été envoyé le message
- Connaitre l’émetteur du message (ID de connexion, friendly name, …)
Par exemple, pour afficher le nom de l’émetteur (son FriendlyName) nous pourrions écrire :
1 2 3 4 5 |
constellation.registerMessageCallback("HelloWorld", MessageCallbackDescriptor().setDescription("Hello World !"), [](JsonObject& json, MessageContext ctx) { constellation.writeInfo("Hello %s from Constellation !", ctx.sender.friendlyName); }); |
En invoquant le MC depuis le “MessageCallback Explorer” en étant connecté sous l’utilisateur “seb”, nous obtiendrons dans les logs :
1 |
[MyVirtualSentinel/MyVirtualPackage] 17:28:41 : Hello Consumer/ControlCenter:seb from Constellation ! |
Décrire les paramètres de vos MessageCallbacks
Dans la philosophie des MessageCallbacks Constellation, un MessageCallback sert à invoquer des méthodes où la clé du message (MessageKey) est comme le nom d’une méthode.
Pour passer des paramètres à ces méthodes, on utilisera le contenu du message (Data).
Paramètres de types simples
Imaginez vouloir exposer une méthode “SayHello” qui prend en paramètre deux chaines de paramètres (= des types simples), le nom et prénom pour les afficher dans les logs.
On déclarera le MessageCallback suivant :
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].as<char *>(), json["Data"][1].as<char *>()); }); |
On a ajouté deux paramètres de type “String” dans le MessageCallbackDescriptor via la méthode “addParameter”. Dans le code de notre MessageCallback, nous exploitons les valeurs des paramètres via la propriété “Data” de notre message “json”.
Le 1er paramètre (json[« Data »][0]) étant ici le “FirstName” d’après la description et le 2ème (json[« Data »][1]) le “LastName”.
Attention :
- lorsque plusieurs paramètres sont passés comme dans l’exemple ci-dessus, la propriété “Data” est un tableau contenant la valeur de chaque argument (ici [0] est “FirstName” et [1] le “LastName”)
- Si un seul paramètre est passé dans votre MC, la propriété “Data” est la valeur de l’argument directement et non un tableau.
Vous pouvez tester votre MC depuis le “MessageCallback Explorer” :
Bien entendu vous pouvez déclarer un ou plusieurs paramètres de type différent.
Les types de base sont :
-
System.String
-
System.Char
-
System.Boolean
-
System.Int16
-
System.Int32
-
System.Int64
-
System.Double
-
System.Decimal
-
System.Float
Pour simplifier l’enregistrement des paramètres de type de base, vous pouvez les déclarer en utilisant la forme générique.
Par exemple pour un paramètre de type “String” on écrira :
1 |
addParameter<const char*>("FirstName") |
Autre exemple pour ajouter des paramètres de type “Boolean” et “Int32” :
1 2 |
addParameter<bool>("IsEnable") addParameter<int>("MyNumber") |
De ce fait notre MC “SayHello” pourrait s’écrire :
1 2 3 4 5 |
constellation.registerMessageCallback("SayHello", MessageCallbackDescriptor().setDescription("Say hello !").addParameter<const char*>("FirstName").addParameter<const char*>("LastName"), [](JsonObject& json) { constellation.writeInfo("Hello %s %s", json["Data"][0].as<char *>(), json["Data"][1].as<char *>()); }); |
Paramètre de types complexes
Il est également possible de spécifier des objets complexes contenant des propriétés.
Reprenons l’exemple précédent “SayHello” sauf qu’au lieu de passer deux paramètres de type “simple”, passons qu’un seul paramètre : un objet contenant deux propriétés (LastName et FirstName).
1 2 3 4 5 |
constellation.registerMessageCallback("SayHello", MessageCallbackDescriptor().setDescription("Say hello with complex object!").addParameter("User", "SampleUserData"), [](JsonObject& json) { constellation.writeInfo("Hello %s %s", json["Data"]["FirstName"].as<char *>(), json["Data"]["LastName"].as<char *>()); }); |
Dans le MessageCallbackDescriptor de notre MessageCallback nous déclarons qu’un seul paramètre nommé “User” et de type “SampleUserData”.
Au niveau du code de notre MessageCallback, nous récupérons nos deux valeurs via le 1èr et unique paramètre (json[« Data »]) en accédant à la propriété “FirstName” et “LastName”.
Bien entendu pour que cela fonctionne il faut déclarer le type “SampleUserData” via la méthode “addMessageCallbackType” :
1 |
constellation.addMessageCallbackType("SampleUserData", TypeDescriptor().setDescription("This is a smaple user data").addProperty<const char*>("FirstName").addProperty<const char*>("LastName"); |
Il n’y a donc plus maintenant qu’un seul paramètre, un objet complexe contenant deux propriétés de type “string”. Vous pouvez d’ailleurs cliquez sur le type “SampleUserData” pour afficher sa description :
Noter que vous pouvez enregistrer et déclarer des MessagesCallbacks mixant paramètre simple et paramètre complexe :
1 2 3 4 5 6 |
constellation.registerMessageCallback("MultipleParameters", MessageCallbackDescriptor().setDescription("Mixte de parametre simple & complexe").addParameter("User", "SampleUserData").addParameter<const char*>("Password"), [](JsonObject& json) { // json["Data"][0] is SampleUserData // json["Data"][1] is String }); |
Exposer des MessageCallbacks avec réponse : les sagas
Pour finir un MessageCallback peut retourner un message de réponse, c’est ce que l’on appelle les sagas. C’est un peu comme une méthode en programmation, elle peut être “procédure” (sans retour, un void) ou “fonction” (retourne un type simple ou complexe).
Votre Arduino/ESP peut également enregistrer des MC retournant une réponse. Pour cela deux choses sont nécessaire :
- Appeler la méthode “sendReponse” dans votre callback pour envoyer la réponse
- Appeler la méthode “setReturnType” sur le MessageCallbackDescriptor de votre MC pour déclarer le type de retour
Par exemple exposons un MessageCallback “Addition” pour réaliser des additions sur un Arduino/ESP. Ce MessageCallback prend en paramètre deux entiers et retourne le résultat de type entier (Int32) également :
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<int>("a").addParameter<int>("b").setReturnType<int>(), [](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 !"); } }); |
Pour retourner une réponse vous devez disposer du contexte de réception du message (le “MessageContext”). Vous noterez qu’ici, nous testons que le message appartient bien à une saga afin de renvoyer la réponse car il ne sert à rien d’envoyer une réponse sans saga, il ne sera jamais reçu par l’émetteur.
En déclarant le type de retour (ici un Int32), le MessageCallback Explorer reconnait ce MC comme une saga. Vous pouvez donc envoyer le message dans une saga (c’est à dire avec un identifiant de Saga) ou non grâce à la case à cocher :
Si le message est envoyé dans une saga, notre code Arduino ci-dessus retournera une réponse, ici à la Console Constellation :
La Console Constellation vous présentera ici toutes informations quant à la réponse et au message initial. On retrouve ici la réponse de notre addition :
Dans la Console Log, on retrouvera bien les logs produits par notre Arduino :
Vous pouvez également rejouer ce test mais cette fois ci en décochant la case “With Saga”, c’est à dire que nous invoquons le MC “Addition” sans n° de saga :
Dans la Console Log, notre Arduino réalise bien l’addition mais ne retourne pas de message de réponse car il ne dispose pas de n° de saga pour répondre :
Recevoir tous les messages dans une méthode
Vous pouvez aussi “attraper” tous les messages que votre package virtuel reçoit en définissant un “callback”, c’est à dire une fonction prenant en paramètre le message sous forme d’un JsonObject et qui sera appelée à chaque message reçu.
Par exemple, écrivez la méthode suivante pour afficher le “MessageKey” de chaque message reçu (si nécessaire n’hésitez pas à lire ou relire les fondamentaux du Messaging Constellation) :
1 2 3 4 |
void messageReceive(JsonObject& json) { Serial.print("MSG RCV : "); Serial.println(json["Key"].as<char *>()); } |
Au “setup”, attachez cette méthode à la réception des messages (setMessageReceiveCallback) et abonnez-vous à la réception des messages (subscribeToMessage) :
1 2 |
constellation.setMessageReceiveCallback(messageReceive); constellation.subscribeToMessage(); |
Avec les expression lambda en C++, vous auriez pu aussi écrire directement :
1 2 3 4 |
constellation.setMessageReceiveCallback([](JsonObject& json) { constellation.writeInfo("Message received ! Message key = %s", json["Key"].as<char *>()); }); constellation.subscribeToMessage(); |
Enfin dans la boucle principal (loop), invoquez la méthode “constellation.loop” pour traiter et dispatcher les messages reçu par votre package virtuel :
1 |
constellation.loop(); |
Par exemple, imaginez un ESP8266 avec 2 MessageCallbacks : “Restart” sans paramètre qui redémarre l’ESP et “SendCode” avec un objet JSON en paramètre pour envoyer un signal infrarouge.
Le code pourrait être :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
constellation.setMessageReceiveCallback([](JsonObject& json) { Serial.print("Message receive : "); const char * key = json["Key"].as<char *>(); if (stricmp (key, "SendCode") == 0) { const char * encoder = json["Data"]["Encoding"].as<char *>(); unsigned long code = strtoul(json["Data"]["Code"].as<char *>(), NULL, 0); // TODO : send the IR signal "code" else { constellation.writeInfo("Unable to send IR code : encoding '%s' not found !", encoder); } irrecv.enableIRIn(); } else if (stricmp (key, "Restart") == 0) { ESP.restart(); } }); constellation.subscribeToMessage(); |
Avec cette façon de faire, votre “MessageReceiveCallback” est invoqué à chaque message reçu mais c’est à vous de “dispatcher” les messages en fonction de leur “MessageKey”.
De plus côté Constellation, il n’y a aucun “Message Callback” déclaré sur Constellation, c’est a vous de connaitre les “MessageKey” que votre package virtuel acceptent, ici “SendCode” et “Restart” !
Obtenir contexte de réception
Comme pour l’enregistrement d’un MessageCallback, vous pouvez ajouter un 2ème argument de type MessageContext pour récupérer le contexte de réception du message.
Par exemple nous pourrions écrire très simplement :
1 2 3 4 |
constellation.setMessageReceiveCallback([](JsonObject& json, MessageContext ctx) { constellation.writeInfo("Message receive from %s. Message key = %s", ctx.sender.friendlyName, json["Key"].as<char *>()); }); constellation.subscribeToMessage(); |
S’abonner à un groupe Constellation
Jusqu’a présent votre package virtuel recevra les messages :
- Du Scope “All” (c’est à dire tout le monde connecté à Constellation)
- Du Scope “Others” si votre package n’est pas l’émetteur du message
- Du Scope de votre sentinelle
- Du Scope de votre package
Si nécessaire n’hésitez pas à lire ou relire les fondamentaux du Messaging Constellation.
Vous pouvez également vous abonner à des groupes pour recevoir les messages qui seraient envoyés dans ces groupes.
Pour cela il suffit d’invoquer la méthode “subscribeToGroup” en spécifiant le nom du groupe à joindre. Exemple :
1 |
constellation.subscribeToGroup("MonGroupe"); |
Noter cependant que l’appartenance d’un package, virtuel ou non, à un groupe peut être défini au niveau de la configuration de la Constellation éditable simplement via la Console Constellation afin de pouvoir faire évoluer ces appartenances à la volée sans devoir reprogrammer vos Arduino/ESP.
Démarrez la discussion sur le forum Constellation