Aperçu SHIPHarbour
Glossaire des termes
- Socket : canal de communication qui utilise TCP/IP pour envoyer ou recevoir des données (pas les deux)
- protobuf API – La bibliothèque de protocole Google de fonctions et d’objets utilisés pour encoder et décoder des données dans différents langages de programmation
- API SHIPHarbour – Le protocole personnalisé défini par Serious Integrated et écrit à l’aide de la syntaxe protobuf
- SHIPHarbour Language Binding – Une bibliothèque de fonctions spécifiques à la langue destinée à rendre l’API SHIPHarbour plus pratique pour une utilisation dans la langue cible
- SHIPHarbour Master – Une application développée par Serious Integrated qui traduit les messages de l’API SHIPHarbour en commandes de l’API SHIPBridge
- SHIPBridge API – Le protocole propriétaire et la bibliothèque de fonctions définies par Serious Integrated et écrites en C embarqué
- Message – Une série d’octets représentant soit une mémoire tampon de protocole Google, soit une commande/réponse SHIPBridge
- Routine : message de mise en mémoire tampon du protocole Google contenant des instructions pour exécuter une ou plusieurs commandes SHIPBridge.
Introduction
Présentation de l’API SHIPHarbour
Tous les modules Serious Integrated sont capables d’utiliser le protocole SHIPBridge pour être contrôlés à distance sur des interfaces de communication courantes (par exemple, USB, UART, SPI, etc.). L’API SHIPBridge est disponible gratuitement pour être portée sur des plates-formes embarquées définies par le client. Cependant, les clients peuvent également souhaiter contrôler des modules à l’aide d’une application basée sur PC. La plupart des développeurs d’applications PC ne sont pas familiers avec le C intégré et ne sont pas prêts à porter l’API SHIPBridge dans leur langue cible. L’API SHIPHarbour permet au développeur d’utiliser des concepts de conception familiers et attendus des langages modernes de haut niveau.
SHIPHarbour est plus qu’une simple API de haut niveau pour exécuter des commandes SHIPBridge individuelles. En raison de la nature embarquée en temps réel des plates-formes, nous ne pouvons pas nous attendre à ce que le module gère une commande SHIPBridge sans bloquer ou être bloqué par d’autres processus importants. Par conséquent, la plupart des commandes SHIPBridge sont simplifiées pour garantir des temps d’exécution et de réponse plus courts dans l’ensemble. Cela signifie également qu’il peut être nécessaire d’exécuter plusieurs commandes SHIPBridge pour effectuer ce qu’un utilisateur pourrait penser être une seule opération. Les messages SHIPHarbour sont conçus pour réduire cette complexité pour le développeur de haut niveau, de sorte qu’une opération qui prend des dizaines de commandes SHIPBridge ne nécessitera qu’une seule requête SHIPHarbour. SHIPHarbour facilite également la communication de plusieurs applications avec un module sur des connexions uniques ou plusieurs connexions physiques, le tout simultanément. De plus, aucune modification de l’API ou du code de l’application n’est explicitement requise pour permettre les communications sur de nouvelles interfaces. Il n’est pas nécessaire ici d’entrer dans les détails de la manière dont les messages SHIPHarbour sont traduits en commandes SHIPBridge ou de la manière dont les commandes et réponses SHIPBridge sont transmises et reçues. En effet, tout le traitement nécessaire est standardisé et abstrait par l’application PC SHIPHarbour Master. Les applications des clients utilisant SHIPHarbour devraient être mises en œuvre en tant que clients de ce programme.
Douilles utilisées dans SHIPHarbour Master
Bien qu’il ne soit finalement pas nécessaire d’utiliser SHIPHarbour ou SHIPHarbour Master pour contrôler les modules, c’est certainement la méthode la plus rapide pour la plupart des développeurs qui cherchent à contrôler des modules directement à partir de l’environnement PC. Cela s’explique en partie par le choix des sockets comme méthode de communication avec SHIPHarbour Master. Tous les langages de programmation modernes de haut niveau seront capables d’utiliser des sockets et disposeront généralement d’une API intégrée pour la configuration et l’utilisation des sockets. La plupart de ces API permettront également au développeur de tirer parti des principes du multithreading. C’est important car le développeur ne peut généralement pas prédire quand SHIPHarbour Master devra envoyer des messages sur les changements de statut d’un module. Cela évite également d’avoir à imposer une synchronisation stricte entre l’application client et SHIPHarbour Master sur le moment où les demandes sont autorisées.
Le terme socket, utilisé ici, fait référence à la moitié d’une interface TCP/IP bidirectionnelle capable d’échanger des données arbitraires. En utilisant les adresses correctes, le PC peut se transmettre les données à lui-même sans avoir à les envoyer via une connexion réseau réelle. Cela permet à SHIPHarbour Master d’échanger des données avec la fiabilité et l’universalité fournies par l’utilisation de la norme TCP/IP. Incidemment, cela signifie également qu’il est possible pour le développeur de configurer SHIPHarbour Master pour qu’il soit accessible à partir d’une connexion réseau réelle. Le socket de réception est généralement appelé serveur, car il écoute les demandes entrantes et transmet les données de la demande à une application pour traiter la demande. Le socket de transmission est appelé client car il envoie simplement des requêtes à un serveur.
Les sockets de serveur que le développeur doit implémenter sont classés en deux catégories : sockets de contrôle d’application ou de contrôle de module. Un socket de serveur de contrôle d’application doit être actif pendant toute la durée pendant laquelle l’application personnalisée est destinée à communiquer avec SHIPHarbour Master. Ces sockets sont utilisés pour recevoir des mises à jour spontanées de l’état du module ainsi que d’autres données nécessaires à l’application personnalisée pour créer des demandes de routine. Il n’y a généralement qu’un seul socket de serveur Application Control par application. En revanche, plusieurs sockets de serveur de contrôle de module peuvent être créés. Ces sockets sont utilisés pour recevoir les mises à jour de statut de routine d’une demande de routine précédemment émise.
Les sockets clients que le développeur doit implémenter sont classés en deux catégories : sockets Announce, Application Control ou Module Control. Le socket client Annoncer est toujours le point de départ pour qu’une application commence à se connecter à SHIPHarbour Master et ne sera actif que lors de l’envoi d’une seule requête SHIPHarbour prédéterminée. Ce socket est utilisé pour envoyer des informations sur la façon dont SHIPHarbour Master doit se connecter au socket serveur de contrôle d’application de l’application personnalisée. Un socket client Application Control est censé être actif une fois que SHIPHarbour Master a envoyé des informations au socket serveur Application Control sur la façon dont l’application doit connecter le socket client et pendant toute la durée pendant laquelle l’application personnalisée a l’intention de communiquer. Un socket client Application Control est utilisé pour envoyer des requêtes de gestion des connexions de socket à SHIPHarbour Master. Les sockets clients de contrôle de module sont utilisés pour envoyer toutes les demandes de routine à exécuter sur un module cible. Une seule prise de contrôle de module peut être utilisée pour communiquer simultanément avec tous les modules connectés. Cependant, l’utilisation de cette approche nécessite que le développeur analyse certains champs de la réponse afin de la relier correctement à la demande d’origine.
Implémentation à l’aide de protobuf
Tous les messages définis dans l’API SHIPHarbour sont définis à l’aide de la syntaxe protobuf proto3 de Google. L’utilisation de protobuf permet aux définitions de messages d’être indépendantes du langage tout en tirant parti du compilateur protoc fourni pour générer un code spécifique au langage optimal pour la création, l’envoi et la réception de chaque message. Le langage protobuf présente de nombreuses commodités qui permettent aux définitions de messages d’être intrinsèquement extensibles, rétro compatibles et d’aboutir à un format d’encodage très flexible. La bibliothèque protobuf est ouverte et libre d’utilisation sans les limitations typiques des projets open source et prend en charge les langages de haut niveau les plus courants tels que C++, Java, C#, Python, Go, etc. Vous trouverez de plus amples informations sur le site Web suivant : https://developers.google.com/protocol-buffers/
SHIPHarbour API
Structure des fichiers
Les fichiers suivants définissent l’API SHIPHarbour :
- SHIPHarbour_DataTypes.proto – Définit les types de base et les structures de messages utilisés comme champs dans d’autres messages
- SHIPHarbour_Generic.proto – Définit le message wrapper utilisé pour envoyer tous les autres messages
- SHIPHarbour_Master.proto – Définit les structures de messages requises pour exécuter les routines de contrôle SHIPHarbour Master
- SHIPHarbour_Platform.proto – Définit les structures de messages requises pour exécuter les routines liées à la plate-forme
- SHIPHarbour_Firmware.proto – Définit les structures de messages requises pour exécuter les routines liées au micrologiciel
- SHIPHarbour_Files.proto – Définit les structures de messages requises pour exécuter les routines liées aux fichiers
- SHIPHarbour_XVars.proto – Définit les structures de messages requises pour exécuter des routines liées à des variables externes
Mots-clés
- import – utilisé pour permettre au fichier actuel d’accéder aux définitions d’un autre fichier .proto
- message – indique le début de la définition d’une structure de message
- Oneof des : indique qu’un seul des champs délimités peut être renseigné à un moment donné.
- enum – indique le début d’une définition de valeurs entières avec des significations spéciales
- repeated – indique que le champ doit être accessible sous forme de liste, peut être vide
Un exemple simple de syntaxe proto3 :
message RoutineInfo1 {
Endpoint2 ep3 = 14;
UInt32Value5 id = 2;
bool6 start = 3;
}
1 Nom qui sera utilisé pour faire référence à ce type de message
2 Un type de message précédemment défini contenu dans cette définition de message
3 Le nom qui sera utilisé pour faire référence à ce champ du message
4 Un numéro utilisé en interne qui identifie de manière unique un champ
5 Un wrapper pour un type de données primitif utilisé pour faciliter la détection si le champ est présent dans un message reçu
6 Type de données primitif, les valeurs par défaut sont toujours 0 (aucune distinction ne peut être faite entre la valeur attribuée équivalente à la valeur par défaut et la valeur n’ayant pas été attribuée)
Pour plus de détails sur la syntaxe proto3, rendez-vous sur https://developers.google.com/protocol-buffers/docs/proto3.
Afin d’éviter les collisions de noms avec d’autres bibliothèques, le schéma de nommage typique des structures de l’API SHIPHarbour commencera par quelque chose de similaire à « SHIPHarbour.Message. » suivi d’une version abrégée du nom de fichier du fichier .proto dans lequel la structure est définie, après quoi la définition spécifique apparaîtra. Par exemple, l’espace de noms complet pour la création d’un message d’annulation d’une routine précédemment demandée peut ressembler à quelque chose comme : « SHIPHarbour.Message.Generic.Routine.Cancel ». La syntaxe exacte dépendra de la langue cible utilisée. Pour plus de détails, consultez https://developers.google.com/protocol-buffers/docs/reference/overview.
SHIPHarbour Messages
Il existe deux catégories logiques de messages, le message de contrôle et le message de routine. Toutes les définitions de message définissent simultanément la structure de commande et de réponse pour cette demande particulière. Cependant, cela ne signifie pas que toutes les demandes nécessitent une réponse. Les messages de contrôle sont généralement destinés à être utilisés avec les sockets de contrôle d’application. Ces messages ouvrent et ferment les connexions de socket et transportent des données importantes nécessaires à l’application pour construire correctement les messages de routine. Les messages de routine doivent être envoyés à l’aide de sockets de contrôle de module et sont ensuite triés, livrés et mis en file d’attente pour être exécutés par SHIPHarbour Master par rapport à un module spécifié. L’exécution de l’un de ces messages implique la lecture des champs pour piloter la logique interne qui envoie une ou plusieurs commandes SHIPBridge directement au module.
- Exemples de messages de contrôle
- Message.Master.Open
- Message.Master.ModuleInfo
- Exemples de messages courants
- Message.Platform.SetState
- Message.Firmware.Installed
- Message.XVars.Exchange
Utilisation de Protobufs
Construire des messages
Bien que le code exact pour construire un message SHIPHarbour à l’aide de l’API protobuf dépende du langage, il y a quelques points à noter :
- Tous les messages envoyés ou reçus de SHIPHarbour Master doivent utiliser le message générique comme wrapper.
- Le champ version doit être défini pour tous les messages.
- Tous les messages doivent comporter exactement l’un des champs de la charge utile défini.
- Le champ d’erreur ne doit pas être défini pour un message qui n’est pas une réponse à une demande précédente.
Exemple C++ :
SHIPHarbour::Message::Generic generic;
generic.set_version(/* version */);
SHIPHarbour::Message::Master::Open * pOpen = generic.mutable_open();
pOpen->mutable_host()->set_value(/* host address string */);
pOpen->mutable_port()->set_value(/* port number */);
Exemple Java :
StringValue host = StringValue.newBuilder()
.setValue(/* host address string */)
.build();
UInt32Value port = UInt32Value.newBuilder()
.setValue(/* port number */)
.build();
Open open = Open.newBuilder()
.setHost(host)
.setPort(port)
.build();
Generic generic = Generic.newBuilder()
.setVersion(/* version */)
.setOpen(open)
.build();
Pour plus d’informations sur l’utilisation de protobufs dans la langue cible, visitez https://developers.google.com/protocol-buffers/docs/reference/overview
Envoi/réception de messages
Par défaut, le protobuf sérialisé n’inclut pas de description de la taille totale du message. Afin de permettre l’envoi de messages aussi rapidement que possible, l’API SHIPHarbour exige que la taille en octets du message à analyser apparaisse sous la forme d’un entier de 32 bits codé en variantes immédiatement avant les octets du message. Pour certains langages, l’API protobuf implémente un ensemble de fonctions nommées de la même manière que writeDelimitedTo et parseDelimitedFrom. Tous les messages doivent être encodés/décodés à l’aide de ces fonctions, ou le développeur doit implémenter manuellement l’encodage/décodage variable de la taille du message.
Exemple C++ :
/*
Least-significant byte first, most-significant bit is 1 when more bytes.
There can be up to 5 bytes of size information.
*/
void parseVarInt32(char * pSrc, int * pVarInt, int * pIndex) {
char data;
*pVarInt = 0;
do {
if (pSrc->size() <= *pIndex) {
break;
}
data = pSrc[*pIndex];
*pVarInt = (data & 0x7F) << ((*pIndex)++ * 7);
} while ((data & 0x80) && (*pIndex < 6));
}
void serializeVarInt32(char * pDst, int * pIndex, int varint) {
char data;
do {
data = varint & 0x7F;
varint = varint >> 7;
if (varint) {
data = data | 0x80;
}
pDst[(*pIndex)++] = data;
} while (varint);
}
ByteArray msg = new ByteArray(5 + generic.ByteSize());
int index;
serializeVarInt32(msg.data(), &index, generic.ByteSize());
generic.SerializeToArray(&(msg.data()[index]), generic.ByteSize());
pSocket->write(msg);
Exemple Java :
ByteArrayOutputStream bos = new ByteArrayOutputStream();
generic.writeDelimitedTo(bos);
socket.getOutputStream().write(bos.toByteArray());
Dans l’exemple Java ci-dessus, notez que l’API protobuf n’a pas été utilisée pour écrire directement dans le flux de sortie du socket. Au moment de la rédaction de cet article, il y a un bogue qui provoquera de manière imprévisible la corruption des messages écrits de cette façon à la réception. Ce bogue a également été observé lors de l’utilisation de langages autres que Java. Par conséquent, il est fortement recommandé d’éviter d’utiliser l’API protobuf pour accéder directement à un socket. L’API protobuf doit être utilisée pour lire ou écrire dans une structure intermédiaire.
Liaisons de langue disponibles
Pour certaines langues, Serious a déjà développé une liaison linguistique complète. Ces liaisons de langage peuvent être utiles car elles permettent au développeur de commencer immédiatement à se connecter et à envoyer des messages à SHIPHarbour Master en appelant des fonctions abstraites.
Si une liaison linguistique pour la langue cible n’est pas disponible ou si la liaison linguistique standard n’est pas adaptée à l’environnement cible, Serious peut être en mesure de créer ou de modifier la liaison linguistique officielle sur demande. Sinon, reportez-vous à la section « Utiliser directement l’API SHIPHarbour ».
Des exemples d’utilisation, le code source et les binaires pour chaque liaison de langue sont disponibles sur demande. Reportez-vous aux pages suivantes pour la documentation sur l’utilisation de chaque liaison de langue : – Java
Utilisation directe de l’API
Premiers pas avec protobuf
Cette section de la documentation n’est nécessaire que si une liaison linguistique effectuée par Serious n’existe pas ou ne peut pas être utilisée. Pour créer une liaison de langue personnalisée, les étapes suivantes sont nécessaires :
- Compiler le code source du protobuf dans une bibliothèque pour la langue cible (au moment de la rédaction : v3.0.0-beta3)
- Télécharger une version prédéfinie de la bibliothèque protobuf
- Compiler les fichiers proto pour la langue cible
- Tous les fichiers .proto pour l’API SHIPHarbour doivent être compilés avec le compilateur protobuf « protoc » en utilisant la structure de commande suivante :
protoc --= --proto_path=
- Exemples de changement de langue :
--cpp_out
--csharp_out
--java_out
- Utilisez la commande suivante pour trouver le commutateur approprié pour la langue cible :
protoc --help
- Le répertoire de destination peut être n’importe quel emplacement et c’est là que les fichiers générés apparaîtront
- Le répertoire source doit être l’emplacement de tous les fichiers .proto de SHIPHarbour.
Se connecter à SHIPHarbour Master
Les étapes ci-dessous doivent être suivies pour établir la connexion entre SHIPHarbour Master et une autre application :
- Ouvrez un socket de réception sur n’importe quel port (appelé Socket_AppControl_Rx).
- Ouvrez un socket de transmission vers SHIPHarbour Master, le port par défaut est 10400 (appelé Socket_Announce_Tx).
- Envoyez un message Open dans Socket_Announce_Tx avec votre adresse d’hôte et le numéro de port utilisé par Socket_AppControl_Rx.
- Socket_Announce_Tx sera fermé de force lors de la réception du message, car il n’est plus nécessaire.
- Un message Open sera envoyé à Socket_AppControl_Rx décrivant comment créer Socket_AppControl_Tx.
- Un message PluginInfo sera envoyé à Socket_AppControl_Rx décrivant quels plugins SHIPHarbour Master a chargés.
- Un message ModuleInfo sera envoyé à Socket_AppControl_Rx décrivant quels modules sont actuellement connectés à SHIPHarbour Master.
- Il est désormais possible d’émettre des messages de contrôle au niveau de l’application à l’aide de Socket_AppControl_Tx.
- La séquence ci-dessus correspond à la première moitié de l’organigramme.
Obtenir des informations sur les modules connectés
Bien qu’il soit possible d’interroger ces informations en envoyant un message ModuleInfo dans Socket_AppControl_Tx, il n’est ni recommandé ni nécessaire de le faire. SHIPHarbour Master enverra toujours un message ModuleInfo dans Socket_AppControl_Rx chaque fois que l’état de connexion d’un module ou de l’un des points de terminaison change.
Connexion à un module
Les étapes ci-dessous doivent être suivies pour établir la connexion entre une application PC personnalisée et un module :
- Ouvrez un socket de réception sur n’importe quel port (appelé Socket_ModuleControl_Rx).
- Envoyez un message Open dans Socket_AppControl_Tx avec votre adresse d’hôte et le numéro de port utilisé par Socket_ModuleControl_Rx.
- Un message Open sera envoyé à Socket_ModuleControl_Rx décrivant comment créer Socket_ModuleControl_Tx.
- Il est désormais possible d’envoyer des messages de routine à un module spécifié à l’aide de Socket_ModuleControl_Tx.
- La séquence ci-dessus correspond à la seconde moitié de l’organigramme.
Utilisation des messages ModuleInfo
Un message ModuleInfo ne comporte qu’un seul champ qui est marqué comme répétable de type ModuleDetails. Cela signifie qu’un seul message ModuleInfo peut contenir les informations de plusieurs modules, de sorte que le champ doit être accessible sous forme de liste, de tableau ou d’une autre collection de ModuleDetails en fonction de la langue cible.
La détection de la connexion ou de la déconnexion d’un module doit être effectuée par application. SHIPHarbour Master ne tient pas à jour une liste des modules que chaque application PC connectée a vus. Lorsqu’un module (ou un point d’extrémité au sein d’un module) change d’état, les informations actuelles de tous les modules sont diffusées à toutes les applications PC connectées. L’application PC doit conserver un enregistrement des modules et des points de terminaison connus, puis comparer et gérer cet enregistrement en conséquence à mesure que de nouveaux messages ModuleInfo sont reçus de SHIPHarbour Master.
SHIPHarbour_Master.Par conséquent :
message ModuleInfo {
repeated DataTypes.ModuleDetails info = 1;
}
Les ModuleDetails contenus dans un message ModuleInfo contiennent certains champs qui peuvent être considérés comme superflus par l’application PC. Le champ auquel toutes les applications PC doivent prêter attention est le champ de module de type Module. Le type de données Module contient le numéro de série unique de la carte SIM ou du SCM sous la forme d’une valeur de 64 bits et, plus important encore, une liste de données de point de terminaison décrivant comment le module est actuellement connectée à SHIPHarbour Master.
SHIPHarbour_DataTypes.Par conséquent :
message ModuleDetails {
Module module = 1;
UInt32Value family = 2;
UInt32Value version = 3;
UInt32Value variant = 4;
UInt32Value lcdCode = 5;
UInt32Value id = 6;
Firmware currentProgram = 7;
}
message Module {
Fixed64Value sn = 1;
BytesValue product = 2;
repeated Endpoint version = 3;
}
Le type de données point de terminaison comporte un champ ID qui est requis pour envoyer tous les messages de routine. Ceux-ci correspondent directement à une interface de communication physique sur laquelle le module utilise SHIPBridge pour communiquer avec SHIPHarbour Master. L’application PC doit toujours fournir un point de terminaison valide dans un message de routine afin de demander à SHIPHarbour Master de commencer à exécuter une opération SHIPBridge sur une interface physique spécifique. Comme le module peut réinitialiser ou perdre le contexte de connexion SHIPBridge avec SHIPHarbour Master, l’ID du point de terminaison changera au fil du temps. L’application PC est chargée de reconnaître quand l’ID d’un point de terminaison précédemment connu a changé et de faire la transition des opérations futures ou des opérations multi-requêtes en cours pour utiliser le nouvel ID.
SHIPHarbour_DataTypes.Par conséquent :
message Endpoint {
UInt32Value id = 1;
UInt32Value plugin = 2;
UInt64Value speed = 3;
}
Contrôle d’un module
Toute opération qui peut être effectuée sur un module se fait toujours en envoyant ce qui est classé comme des messages de routine. Les messages de routine sont définis dans des fichiers en fonction de la fonctionnalité du module à laquelle on accède. En raison de la façon dont les messages génériques sont utilisés, tous les messages de routine qui peuvent éventuellement être envoyés peuvent être vus dans le fichier SHIPHarbour_Generic.proto. Voici un exemple d’envoi d’un message de routine pour récupérer des informations sur les fichiers de microprogramme installés sur un module :
Exemple C++ :
SHIPHarbour::Message::Generic generic;
generic.set_version(/* version */);
/*
This gets us a new instance of a Routine and sets the payload field of the
Generic to this new instance.
*/
SHIPHarbour::Message::Generic::Routine * pRoutine = generic.mutable_routine();
SHIPHarbour::Message::DataTypes::RoutineInfo * pInfo = pRoutine->mutable_info();
pInfo->mutable_ep()->mutable_id()->set_value(/* known endpoint id */);
pInfo->mutable_id()->set_value(/* caller-generated unique id */);
pInfo->set_start(true);
/*
This sets the routine field of the Routine to a new instance of an Installed
message. Since there are no outgoing parameters required, we don’t
need to save the return value.
*/
pRoutine->mutable_installed();
ByteArray msg = new ByteArray(5 + generic.ByteSize());
int index;
serializeVarInt32(msg.data(), &index, generic.ByteSize());
generic.SerializeToArray(&(msg.data()[index]), generic.ByteSize());
pSocket->write(msg);
Exemple Java :
Endpoint ep = Endpoint.newBuilder()
.setId(/* known endpoint id */)
.build();
RoutineInfo info = RoutineInfo.newBuilder()
.setEp(ep)
.setId(/* caller-generated unique id */)
.start(true)
.build();
/*
There are no outgoing parameters required, so we build the Installed message
only as a parameter to the Routine builder.
*/
Routine routine = Routine.newBuilder()
.setInfo(info)
.setInstalled(Installed.newBuilder().build())
.build();
Generic generic = Generic.newBuilder()
.setVersion(/* version */)
.setRoutine(routine)
.build();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
generic.writeDelimitedTo(bos) ;
socket.getOutputStream().write(bos.toByteArray());