uMCPIno: Écriture d'un protocole simple avec livraison garantie pour Arduino

Salutations, chers amis!


À un certain stade de leur vie, chaque boîte de bricolage tenace et tenace cesse de manquer le Kantian Arduino en tant que «choses en soi» qu'ils ne peuvent tout simplement pas! : Faire clignoter la LED, prendre les données des capteurs et les transférer sur le fil vers le PC est certes amusant, mais le Saint Graal est en mobilité, en libération des «liens de cuivre», en véritable liberté parmi les vagues d'éther universel.
C'est là que la dure réalité des canaux de communication instables, des erreurs de transmission, des messages non livrés s'ouvre pour nous.
Dieu ne plaise à revendiquer l'originalité dans ce domaine: l'humanité a longtemps utilisé tout un tas de protocoles pour toutes les occasions.
Mais notre objectif est d'apprendre, et comme je suis un ardent partisan de la reconnaissance au combat, nous étudierons en inventant notre propre protocole «vélo».
Aujourd'hui, je propose de développer un protocole qui garantit la livraison, l'intégrité et la séquence des messages entre deux abonnés (point à point, point à point), sache comment et applique l'algorithme Nagle et le pipelining du protocole , quoi que cela signifie. Dans le même temps, il devrait avoir une surcharge minimale et se faufiler jusque dans l'Arduino UNO exigu.



Je demande à tous ceux qui sont intéressés à bord, on ferme les écoutilles, on ouvre les pierres de taille, on remplit les ballasts. Nous avons une excursion dans le passé, destination: année 1974!

Pour les impatients (je le suis moi-même!)
Voici le référentiel github où les implémentations sont:


Selon la bonne vieille tradition, au moins deux experts reconnus dans ce domaine sont impliqués dans la description des algorithmes et protocoles cryptographiques, si quelqu'un d'autre ne les connaît pas, faites connaissance:
Alice
image

Et
Bob
image


Nous décrivons d'abord une tâche simple


Alice et Bob sont assis dans des tranchées adjacentes et ne peuvent pas lever la tête pour se voir. Ils ne peuvent parler que par la voix, à côté d'eux des balles sifflent et des obus éclatent, noyant leurs cris, et en plus, quand l'un d'eux parle, vous devez crier pour ne rien entendre du tout.
La situation est compliquée par le fait qu'ils sont entendus par des ennemis - et vous devez utiliser un langage de code, pour une raison quelconque composé de longues séquences de nombres.
Étant donné qu'Alice et Bob sont des gens, ils doivent périodiquement sortir manger ou aller aux toilettes, et ils sont tellement impatients qu'ils peuvent l'être au moment le plus inopportun!

Comment et pourquoi établir une connexion?


Comment organiser un transfert de données fiable dans une situation aussi déprimante, alors qu'il semblerait que tout soit simplement voué à l'échec?

La première solution qui peut vous venir à l'esprit est d'utiliser des phrases codées en mots vides pour démarrer et terminer le transfert.

Eh bien, disons que si Alice veut envoyer un message, alors elle doit crier «Commencer la transmission!» Et attendre que Bob réponde «Commencer la réception!».
Si Alice n'attend pas la réponse de Bob, elle répète simplement sa demande pour commencer le transfert. Naturellement, vous ne devriez pas le faire trop souvent, sinon, comme nous le savons, vous n’entendez simplement pas la réponse de Bob.

Super. Mais que se passe-t-il si Alice en réponse entend de la prochaine tranchée, "Commencer la transmission!"?
Il s'avère que Bob a également décidé de transférer des informations importantes dès maintenant. Alice a un caractère doux, et elle aurait pu penser: "D'accord, je vais attendre, mon message n'est, en principe, pas urgent, laissez Bob le transmettre en premier." Pensant à cela, elle répond: "Commencez la réception!".

Comme en temps de guerre, la valeur du sinus peut atteindre quatre, la vitesse du son est finie, et il faut un certain temps pour comprendre ce qu'Alice et Bob ont entendu, et même Bob, en tant que gentleman, peut décider de céder à la dame, il hausse les épaules et crie «Je commence à recevoir!» ...

Pour illustrer l'indignation, nous utiliserons des chronogrammes. Le temps leur revient.

Le cas où Alice et Bob n'étaient pas d'accord sur le temps:


Le cas où le message a été perdu:


Ceci est un fiasco. Tout devient trop confus et est aggravé par le fait que le destinataire peut entendre ou ne pas entendre l'une des phrases, et dans chaque cas, l'interlocuteur ne sait pas si son message a été entendu par le destinataire.

Maintenant, Alice et Bob attendent un accueil. Il serait logique de réaliser qu'un conflit s'est produit et que quelqu'un doit reprendre la transmission. Mais que se passe-t-il si tout se reproduit d'une nouvelle manière? Et nous voici à nouveau là où nous avons commencé.

Si vous pensez que la situation est extrêmement rare, rappelez-vous la dernière fois que vous avez parlé à quelqu'un via la voix, lorsque votre abonné ou vous (ou les deux) avez une connexion Internet lente. "Bonjour bonjour bonjour, tu disparais." "Tu n'entends pas bonjour bonjour."

Pendant ce temps, dans les tranchées, la situation se réchauffe, les commandants demandent la transmission des rapports.
Il est temps de se tourner vers les sources primaires: pour étudier Marx, Engels reviendra il y a plus de 40 ans et verra comment ces problèmes ont été résolus par les ingénieurs DEC lors de la conception du protocole DDCMP .

Selon les développeurs de DDCMP, Alice et Bob doivent rejeter les émotions et devenir comme des machines à états finis .
Cela signifie qu'à partir de maintenant, nos Alice et Bob n'auront que quelques états fixes, les transitions entre ces états peuvent se produire strictement selon certaines règles lorsque certains événements se produisent.

Tout d'abord, nous listons simplement les états:

  • Arrêté
  • DÉBUT INITIAL
  • DÉMARRAGE RECONNU
  • COURIR


Comme vous pouvez le voir, il n'y en a que quatre. Et maintenant, quoi qu'il arrive, chacun des abonnés sait au moins avec certitude que son vis-à-vis est dans un seul de ces états. En fait, en regardant un peu plus loin, je dirai que presque toujours un abonné saura dans quel état se trouve le deuxième abonné, mais ce n'est pas exact .

Examinons les états séparément, en détail


HALTED est l'état le plus simple, personne ne va nulle part, tout le monde reste à sa place, rien n'est transmis et non reçu, les stimuli externes sont ignorés. Tous sauf un - la volonté des autorités supérieures. Dans le protocole DDCMP d'origine, la transition de l'état HALTED ne peut être dans l'état INITIAL START qu'à la demande de l'utilisateur - Alice ou Bob reçoivent une commande pour établir une connexion.

Que se passe-t-il lorsque Alice ou Bob reçoivent une telle commande?
Ils constatent immédiatement que l'État est passé de HALTED à INITIAL START , cette transition, comme toute autre, implique une séquence d'actions strictement définie. Dans ce cas, vous devez crier "DO IT!" Et régler l'horloge sur l'horloge. C’est tout.

Alors Alice a crié ce qu'on attendait d'elle et a appuyé sur un bouton du chronomètre. Maintenant, pour comprendre à quoi s'attendre de Bob, nous allons découvrir ce qui peut arriver à Alice lorsqu'elle est à l'état DÉBUT INITIAL .

- À partir du moment où Alice a remarqué que le temps s'est écoulé, disons 10 secondes et qu'elle n'a entendu aucune réaction de Bob (remarque, je ne dis pas que Bob ne lui a rien crié - cela n'est pas connu, mais seulement qu'Alice ne sait rien entendu pendant ce temps, Alice est une femme sage et rationnelle et se fie uniquement aux faits). Nous appelons cet événement un délai d'attente - l'intervalle d'attente a été dépassé. Dans ce cas, le protocole nous dit de répéter: criez «FAITES-LE UNE FOIS!» Et recommencez. Pas encore épais.

- Si Alice a entendu que Bob a crié la même chose - "FAIRE UNE FOIS!", Alice passe alors de manière non sélective à l'état DE DÉPART RECONNU , à propos duquel elle devrait immédiatement crier "FAIRE DEUX!" Et régler à nouveau l'horloge.

- Encore une fois, si Alice a entendu parler de Bob "FAITES DEUX!", Alors elle passe immédiatement à l'état EN COURS (!), Crie "ACCEPTÉ NOOOOOL!". Si son chronomètre a été démarré, elle l'éteint par prudence.

Il est très important de ne pas faire de mouvements inutiles qui ne sont pas prévus par l'état actuel. Quoi que Bob pleure, peu importe comment maudire ou mendier, Alice ne réagit que comme convenu.

Il est commode de présenter de telles choses sous la forme d'un tableau. Commençons donc par les états HALTED et INITIAL START déjà décrits, puis nous réapprovisionnerons davantage le tableau.

ÉTAT ACTUELÉVÉNEMENT
NOUVELLE CONDITIONACTION
N'IMPORTE QUIL'ordre "rompre la connexion"Arrêté
Arrêté
Commandez «Connect»
ÉTAT INITIAL1) Criez «FAITES-LE UNE FOIS!»
2) Démarrer la minuterie
DÉBUT INITIAL
J'ai entendu "FAITES-LE UNE FOIS!"
DÉMARRAGE RECONNU
1) Criez "FAITES DEUX!"
2) Démarrer la minuterie
J'ai entendu "FAIRE DEUX!"COURIR
1) Criez «NOOOL ACCEPTÉ!»
2) Arrêtez la minuterie
Le temps est écoulé - timeoutDÉBUT INITIAL
1) Criez «FAITES-LE UNE FOIS!»
2) Démarrer la minuterie


J'omet consciemment certains points de la description originale du DDCMP - nous n'en avons pas besoin, nous voulons non seulement répéter le DDCMP, mais construire sur sa base le même, seulement un autre nouveau protocole.

Mais revenons à la description des états et des transitions. Le prochain état est ACKNOWLEDGED START .
Dans cet état, tout ce qui peut inquiéter Alice ou Bob est:

- comme avant, l'expiration du temps d'attente, dans ce cas, vous devez rester dans le même état, crier "FAITES DEUX!" Et redémarrez le chronomètre

- le «DO TWO!» entendu se traduit par l'état RUNNING , tout en criant «ACCEPTED NOOOOL!» Et arrête le chronomètre;

- le "DO IT!" entendu laisse dans le même état, vous devez crier "DO TWO!" Et démarrer le chronomètre;

- entendu «NOOOL ACCEPTED!» - transition vers l'état RUNNING , arrêtez la minuterie.

Nous avons mis tout ce qui précède dans un tableau.
ÉTAT ACTUELÉVÉNEMENT
NOUVELLE CONDITIONACTION
DÉMARRAGE RECONNU
J'ai entendu "FAITES-LE UNE FOIS!"
DÉMARRAGE RECONNU
1) Criez "FAITES DEUX!"
2) Démarrer la minuterie
J'ai entendu "FAIRE DEUX!"COURIR
1) Criez «NOOOL ACCEPTÉ!»
2) Arrêtez la minuterie
J'ai entendu "NOOOL ACCEPTÉ!"COURIR
1) Arrêtez la minuterie
Le temps est écoulé - timeoutDÉMARRAGE RECONNU1) Criez "FAITES DEUX!"
2) Démarrer la minuterie


Avec une poignée de main, presque tout est prêt - il ne reste à considérer qu'un seul état RUNNING , car l'un des abonnés peut déjà y entrer, et le second - se précipiter immédiatement aux toilettes, et quand il revient, oubliez tout et essayez d'établir une nouvelle connexion.

Du point de vue de la procédure de prise de contact (nous ne traitons pas encore du transfert de données, pour lequel tout a commencé - c'est une histoire distincte) dans l'état RUNNING , nous sommes intéressés par deux événements:

- s’ils nous crient "FAITES-LE UNE FOIS!" - tout va très mal, c’est une désynchronisation complète, il faut tout recommencer. Le protocole d'origine vous demande d' entrer simplement l'état HALTED . Mais cela ne nous aidera en rien - si pour une raison quelconque, cela s'est produit sur un Arduino autonome, qui transmet certaines données de certains capteurs, alors pour nous, c'est un échec complet. Comme nous le savons, à partir de HALTED, vous ne pouvez accéder à INITIAL START que sur ordre des autorités.
Par conséquent, nous modifions le protocole ici: la réception à l'état HALTED de la commande "DO ONCE!" Devrait fonctionner comme un ordre des autorités - c'est-à-dire passer à l' état DÉMARRAGE INITIAL , crier «FAITES-LE UNE FOIS!», démarrez la minuterie. De plus, dans certains cas, il est pratique de donner un ordre pour établir la communication immédiatement après vous être alimenté.
Ainsi, maintenant, dans le cas le plus gênant, nous allons simplement réinitialiser la connexion.

- le deuxième événement auquel il faut réagir à l'état COURANT - si nous entendons «FAIRE DEUX!» D'une tranchée voisine. C'est déjà plus intéressant. Dans ce cas, vous devez crier «ER ACCEPTÉ!» Où, par ER, on entend le nombre de messages reçus avec succès dans la session de communication en cours. Ceci est un nouveau concept. Ci-dessous, nous examinerons tout plus en détail, mais pour le moment, nous allons mettre tout ce que nous avons appris à l'heure actuelle dans un tableau:

ÉTAT ACTUELÉVÉNEMENT
NOUVELLE CONDITIONACTION
DÉMARRAGE RECONNU
J'ai entendu "FAITES-LE UNE FOIS!"
DÉMARRAGE RECONNU
1) Criez "FAITES DEUX!"
2) Démarrer la minuterie
J'ai entendu "FAIRE DEUX!"COURIR
1) Criez «NOOOL ACCEPTÉ!»
2) Arrêtez la minuterie
J'ai entendu "NOOOL ACCEPTÉ!"COURIR
1) Arrêtez la minuterie
Le temps est écoulé - timeoutDÉMARRAGE RECONNU1) Criez "FAITES DEUX!"
2) Démarrer la minuterie


Maintenant, si Alice et Bob suivent strictement le protocole, ils n'ont tout simplement pas d'options pour entrer dans quelque chose d' incompréhensible , sauf comment établir une connexion, passer conjointement à l'état RUNNING ou, dans un mauvais cas, essayer de l'établir avant de cliquer sur la victoire.

Un lecteur agressif peut essayer de trier toutes les options et arriver à la conclusion que la série d'états et de transitions s'avère être fermée et strictement déterminée. Nous (avec l'aide des esprits des ingénieurs de DEC) avons maintenant lié Alice et Bob à un ensemble de règles qui, simplement, après quoi ils établiront une connexion, si dans les conditions actuelles cela est généralement possible en principe.

Comment transférer des données maintenant?


D'accord, c'était un bon entraînement. Période de bouquet de bonbons dans la relation de deux nœuds de réseau. Rappelons que nous avons démarré une entreprise: nous devons transférer des données avec une livraison garantie et prioritaire! Avec reprise après sinistre. Dans la mesure où les ressources matérielles le permettent (après tout, Alice et Bob peuvent s'avérer être de faibles contrôleurs 8 bits avec 2 kilo-octets de RAM!).

Les ingénieurs DEC nous apprennent que les messages dont nous avons besoin pour numéroter, nous devons compter combien nous avons envoyé, combien nous avons reçu et combien de messages que nous avons envoyés ont atteint le destinataire.

Il est temps de faire une digression!
Admettez-le. quand j'ai vu les noms des variables dans la description du protocole DDCMP, j'ai décidé que ce n'était pas un hasard: les Américains aiment beaucoup attirer de belles abréviations par leurs oreilles.

Pour notre commodité, il existe même plusieurs ressources où les personnes intéressées peuvent toucher la beauté.
Mon préféré est celui-ci - Site d'acronymes astronomiques stupides ou trop forcés (ou DOOFAAS)

Que valent ces fabrications!
Voici un exemple:

WASP - Spectromètre analogique à large bande (mais pas du tout ce que vous pensiez!)
SAURON - Unité surfacique spectroscopique pour la recherche sur les nébuleuses optiques
CISCO - Spectrographe infrarouge refroidi et caméra pour SST (c'est donc ce que cela signifie!)

Et ici, il suffit de tirer:
SQUIRT (oh oui, article 18+!) - Satettile QUick Research Testbed
MERDE (Ni plus ni moins!) - Télescope interférométrique super énorme, avec l'inscription «regarde par toi-même», auquel le lien vers le résumé est attaché à l'article du même nom.

Ainsi, les variables indiquant le nombre de paquets reçus, envoyés et livrés sur le nœud dans la description originale du protocole sont appelées ARN .

Ah, pourquoi n'ont-ils pas nommé le protocole de cette façon - l'ARN! Une sorte de réseau d'ARN. Les protocoles DECnet avaient toutes les chances de devenir des protocoles Internet si l'histoire avait tourné différemment.


Mais revenons à nos tranchées


La norme de protocole d'origine définit que tous les compteurs sont à 8 bits et incrémentent le module 256. Cela signifie qu'il peut y avoir un maximum de 256 messages envoyés pour lesquels aucune confirmation n'a encore été reçue.
Et si la confirmation n'est pas reçue, alors ils peuvent avoir besoin d'être retransmis, et si cela peut être requis, alors ils doivent être stockés jusqu'à la confirmation. Après tout, nous avons garanti la livraison!

Les paramètres physiques de nos Alice et Bob nous dictent des conditions différentes. Dans Arduino 8 bits, cette quantité de données est simplement nulle part où stocker et nous devons faire des compromis. Et je ne parle pas du fait que dans la norme, la longueur des paquets (messages) en octets est limitée à un nombre de 16 bits, c'est-à-dire 64 kilo-octets est un luxe inadmissible!

Ainsi, la connexion est établie. Et ensuite?


Au moment où Alice ou Bob entre dans l' état RUNNING , les compteurs sont réinitialisés.
Comme je l'ai déjà mentionné, le protocole d'origine implique la numérotation des messages modulo 256, mais nous devons réduire ce nombre pour convenir à la petite quantité de mémoire dans les choses de type Arduino.
Afin de pouvoir immédiatement limiter tous les incréments de compteurs, nous allons introduire une certaine constante UMCP_PACKETS_NUMBER, et maintenant tous les incréments se produiront dans ce module.

Si vous prenez UMCP_PACKETS_NUMBER = 8 et que la taille maximale des paquets est UMCP_PACKET_DATA_SIZE - les parties des données transmises à la fois sont limitées à 64 octets, alors tout ira dans l'Arduino UNO et restera un peu pour les besoins des utilisateurs.
Il est important de se rappeler que ces deux paramètres doivent être les mêmes pour les deux parties.

De toute évidence, maintenant, si Alice et Bob ont réussi à établir une connexion et que l'un d'eux a besoin de transférer des données, les données doivent d'abord être divisées en portions ne dépassant pas 64 octets, et deuxièmement, chaque paquet doit également contenir un état deux compteurs d'expéditeur: le nombre de messages reçus et envoyés (R et N).

Voyez comme il est maintenant facile d'organiser ce que l'on appelle pipelining et comment il est facile de gérer les situations d'erreur!

Si Alice envoie 3 paquets d'affilée juste après l'établissement de la connexion, alors le compteur R sera mis à 0 à tous (elle n'a encore reçu aucun paquet) et le compteur N augmentera de un à chaque nouveau paquet.

Si Bob les accepte tous avec succès, alors pour confirmer la réception des trois paquets, il lui suffira d'envoyer une confirmation uniquement pour le dernier, en fait, s'il renvoie simplement le statut de ses compteurs R = 3 et N = 0, alors Alice comprendra immédiatement que tous les messages envoyés ses messages ont atteint le destinataire.

C'était un cas idéal où aucun cas de force majeure ne s'était produit. Voyons maintenant ce qui pourrait mal tourner et comment y faire face.

Si pour une raison quelconque, Bob saute le premier paquet et accepte l'un des suivants, il attire immédiatement l'attention sur le fait que le compteur N qu'il contient (le nombre de paquets transmis par Alice) dépasse clairement le compteur R du côté de Bob et Bob se rend facilement compte qu'il a raté le premier paquet . Dans ce cas, il a juste besoin de jouer les preuves de capitaine les plus plates et de dire à Alice l'état de son compteur de paquets reçus (R = 0). Alice comprend en même temps qu'elle est N = 3, et Bob a R = 0, c'est-à-dire qu'il est nécessaire de transférer les paquets d'une nouvelle manière, en commençant par la première.

Si vous examinez attentivement ce schéma, vous pouvez voir que toute transmission du statut de ses compteurs par un abonné l'informe immédiatement du résultat de la transmission des paquets de données, et la différence entre le compteur transmis d'un côté et reçu de l'autre indique combien de paquets ont été perdus et de quel nombre il est parti.

C'est-à-dire que dans le pire des cas, il y a une retransmission complète de la transmission, dans le cas moyen, le compteur A du côté de l'émetteur augmente à la valeur du compteur R du côté de la réception et "envoie" les paquets perdus.

Il est facile de comprendre que de cette manière la continuité de l'incrément des compteurs est maintenue, ce qui signifie que la transmission des messages (paquets) est garantie.
En plus des variables d'ARN, chaque abonné a deux drapeaux SACK et SPEP. Si le premier est installé, vous devez envoyer une confirmation (Envoyer un accusé de réception), si le second - alors vous devez envoyer une demande de confirmation (Envoyer REPly à un message).

Soit dit en passant, un autre indicateur a été impliqué dans le DDCMP - SNAK d'origine (accusé de réception négatif). Son installation implique l'envoi d'un message d'erreur avec une sorte de code. Mais dans notre version du protocole, nous résoudrons toutes les erreurs exclusivement à l'aide du mécanisme de temporisation, car le protocole peut être utilisé, par exemple, dans des communications sonar ou radio dans une bande de fréquence commune - cela n'a aucun sens de colmater l'environnement commun avec des codes d'erreur.
Si le message a été reçu avec une erreur d'intégrité, il s'agit à proprement parler d'un message non accepté.

À ce stade, le lecteur corrosif devrait avoir le sentiment que quelque chose manque. Quelque chose ne va pas avec ce schéma mince. Et c'est vrai.Plus d'informations à ce sujet plus tard.
En attendant, je propose, en suivant l'exemple du processus de configuration de la connexion, de collecter toutes les réflexions fragmentaires sur le transfert de données vers une table.

Puisque nous n'avons maintenant qu'un seul état, le tableau ne contiendra que deux colonnes - l'événement et les actions à entreprendre. Pour éviter toute confusion entre ceux à qui appartiennent les variables, nous marquons les locales avec l'index L et les distantes (celles qui sont contenues dans le message reçu avec l'index R).
ÉVÉNEMENTACTION
Le paquet de données est venu NR = RL + 11) Arrêtez le temporisateur
2) Envoyez le paquet à l'utilisateur
3) RL = RL + 1
4) Si RR = NL ou AL <= RR <= NL marquez tous
les paquets transmis avec des nombres de AL à RR comme
confirmé
5) AL = RR
Une demande de confirmation est arrivée - REP1) SACK = true
2) SREP = false
— ACK1)
2) SREP = false
3) RR=NL AL<=RR<=NL
AL RR

4) AL=RR
1) SREP = true
SREP1) REP(RL, NL)
2)
AL<NL1) AL+1
2)
, SACK1) ACK(RL,NL)
, AL=NL, SACK SREP
,
1) NL=NL+1
2)


Maintenant, regardez bien, faites défiler tout le circuit dans la tête. Nous réalisons ce qui manque ici.
Dans la description originale de DDCMP, dont nous nous sommes éloignés assez fortement, cela s'appelle l'indicateur SELECT - un nœud (Alice ou Bob) peut ou non être «choisi».
Ce qui nous a déroutés, c'est qu'aucun mécanisme n'était autorisé à autoriser ou à interdire le transfert.
Eh bien, le voici: c'est le drapeau SELECT. Il est appliqué très simplement: si le drapeau est positionné, alors il est possible de transmettre, sinon, c'est impossible.

Tous les messages de contrôle tels que ACK et REP doivent contenir cet indicateur. Le dernier paquet de la file d'attente doit également contenir cet indicateur. Si un nœud «coud» un drapeau dans un paquet, il le «donne» et, par conséquent, il n'est plus installé. Le nœud qui détecte cet indicateur dans le package, au contraire, doit l'installer seul. Cela revient à passer un bâton ou à jouer une viande hachée (rappelez-vous cela?).

La chose la plus importante dans l'utilisation de cet indicateur est que l'un des nœuds doit avoir cet indicateur par défaut, et l'autre non. C'est un autre temporisateur très important - le temporisateur de retour du drapeau SELECT.

Nous avons maintenant un ensemble complet de règles pour établir une connexion et transmettre des données par-dessus.

Nous n'avons pas abordé uniquement la mise en œuvre concrète de cet ensemble de règles.
Eh bien, corrigez-le!

Formation et format du package


C'est ce qu'on appelle le cadrage des messages - les règles d'analyse et de génération des messages et le format.
Calculons combien nous avons besoin.

1. Au minimum, nous avons besoin que chaque message contienne l'état des compteurs R et N de l'expéditeur. Pour Arduino, nous avons convenu que nous pouvons avoir un maximum de 8 messages envoyés mais non confirmés. Mais puisque nous transférons des octets, nous poussons les deux compteurs dans un octet, qu'ils soient de 4 bits.

Cet octet sera formé comme ceci:
 = (RL & 0x0F) | (NL << 4); 

Et nous lirons le statut des compteurs comme ceci:
 NR = (c >> 4) & 0x0F; RR = c & 0x0F; 

c - octet correspondant du message

2. Nous rappelons également que chaque message doit contenir l'état de l'indicateur SELECT. Et les différents types de messages eux-mêmes seront:
Nom drôle
Nom sérieux
La description
Valeur de l'indicateur SELECT
Valeur PTYPE
"FAITES-LE UNE FOIS!"
STR
STaRt
vrai40
"FAITES DEUX!"
STA
STart acquittévrai36
"NOOL ACCEPTÉ!"
ACK (NL = 0, RL = 0)
Reconnaissance
vrai33
"ACCEPTÉ PAR ER, ENVOYÉ PAR EN"ACK (NL, RL)Reconnaissancevrai33
"CONFIRMER COMMENT JE COMPRENDS?"
REP (NL, RL)
REPly à un messagevrai34
"PAQUET DE DONNÉES"
DTA (NL, RL)
Paquet de donnéesfaux17
FORFAIT DE DONNÉES EXTRÊMES
DTE (NL, RL)Paquet DaTa - Finvrai49


Autrement dit, seulement 6 types de messages différents. Tous les messages sauf DTA "libèrent" l'indicateur SELECT - ils ont besoin d'une réponse immédiate de l'abonné distant et sans l'indicateur, il ne pourra pas le transmettre. Le message DTA ne renvoie pas d'indicateur pour rendre le pipelining possible.

En général, nous avons suffisamment de 3 bits pour le type de message, mais afin de ne pas jouer avec les bits, nous attribuons un octet entier au type - en cas de révision, nous aurons une certaine liberté d'action.

Si le message contient des données, nous devons transférer leur quantité et leur somme de contrôle. Étant donné que la taille maximale des paquets est de 64 octets, nous prendrons également un octet pour la somme de contrôle et pour la longueur - tout à coup, vous devrez augmenter la taille des paquets.

3. Nous avons également besoin d'une signature du début du message et d'une somme de contrôle distincte pour l'en-tête.

Avec tout cela à l'esprit, l'en-tête (alias messages de contrôle) ressemble à ceci:
Décalage, octet
La descriptionTaille, peu
0SIGN = 0xAD
8
1PTYPE8
2TCNT4
2RCNT4
3Hchk8


Et le bloc de données est comme ceci:
Décalage, octet
La descriptionTaille, peu
4DCNT
8
5..5 + DCNT-1
DONNÉES
8 * DCNT
5 + DCNTDchk8


C'est tout. Ceci est une description complète du protocole que nous avons obtenu de DDCMP.
Vous pouvez maintenant passer par l'implémentation.

Comment est-il organisé et comment l'utiliser?


Tout d'abord, un peu sur la structure du référentiel.
Comme je l'ai mentionné au début, le code du projet se trouve sur le github: uMCPIno

Afin de voir comment tout fonctionne, vous pouvez exécuter une application de test sur un PC.

Dans l'archive, exécutez uMCPIno_Test.exe, sélectionnez le port COM souhaité et essayez son fonctionnement.
Vous pouvez vérifier une paire de ports COM virtuels (je fais généralement cela).
Pourquoi vous pouvez exécuter deux copies de l'application. N'oubliez pas d'activer «SELECTED BY DEFAULT» dans une copie - ce sera Master, et dans l'autre - désactivez-le. Au fait, si vous êtes intéressé, vous pouvez voir ce qui se passe si vous ne respectez pas cette règle =)

L'option EXTRAS vous permet de voir tous les mouvements de pensées à l'intérieur du cerveau du protocole. Tous les changements dans l'état des drapeaux SELECT, les événements des temporisateurs, les changements dans l'état du nœud, ainsi que les valeurs des variables R et N dans les messages transmis et reçus seront affichés.

Je connecte mon Arduino UNO à mon ordinateur portable via un convertisseur USB UART <->. Les connecteurs à broches vous permettent de simuler un saut de ligne à tout moment:


Si vous exécutez maintenant l'application sur l'ordinateur portable, après avoir appuyé sur le bouton "CONNECTER", l'arduina établira une connexion:


Et voici comment le système réagit à une tentative d'envoi via une ligne «déchirée»:


Pour intégrer uMCPIno dans votre application pour PC:
  1. Le référentiel possède une bibliothèque uMCPIno. Connectez-le aux références de votre projet
  2. Il contient la classe uMCPInoPort. Nous déclarons son instance:
     uMCPInoPort port; port = new uMCPInoPort("COM1", UCNLDrivers.BaudRate.baudRate9600, true, 8100, 2000, 64, 8); 

    Paramètres dans l'ordre: nom du port, puis vitesse du port, état SELECT par défaut, intervalle pour SELECT, intervalle de temporisation, taille du paquet et nombre maximal de messages non acquittés.
  3. Abonnez-vous aux événements:
    lorsque l'indicateur SELECT - port.Select change:
     OnSelectChangedEventHandler 

    lorsque l'état change - port.État:
     OnStateChangedEventHandler 

    L'hôte distant confirme la réception du code:
     OnDataBlockAcknowledgedEventHandler 

    quand arrive le paquet de données:
     OnDataBlockReceivedEventHandler 

  4. Avant de travailler, ouvrez le port
     port.Open(); 

  5. Pour envoyer des données, nous appelons la méthode:
     port.Send(byte[] data); 

  6. Une fois l'opération terminée, fermez le port:
     port.Close(); 



Envoyez simplement deux octets!

Passons maintenant à l'implémentation d'Arduino. Deux exemples se trouvent dans le dossier github.com/AlekUnderwater/uMCPIno/tree/master/Arduino

Le premier est juste un convertisseur de et vers uMCP. Le premier Serial sert à communiquer avec l'hôte, et Serial1 (s'il est sur votre carte) ou SoftwareSerial sur les broches 2 et 3 - pour communiquer avec un autre nœud uMCPIno. Vous pouvez connecter Bluetooth ou un module radio ici.

Le second est un modèle de projet avec prise en charge du protocole uMCPIno

Les deux projets ont des paramètres où vous pouvez et devez grimper. Les voici:

L'état par défaut de l'indicateur SELECT. S'il est défini sur (true), même si le nœud distant ne renvoie pas l'indicateur, il sera défini sur true par le temporisateur.
 #define CFG_SELECT_DEFAULT_STATE (false) 


Pour définir la période de ce temporisateur, il existe le paramètre suivant: intervalle pour renvoyer l'indicateur SELECT en millisecondes
 #define CFG_SELECT_DEFAULT_INTERVAL_MS (4000) 


L'intervalle d'attente d'une réponse en millisecondes, il est préférable de le laisser légèrement inférieur à l'intervalle de retour de l'indicateur SELECT.
 #define CFG_TIMEOUT_INTERVAL_MS (3000) 


Le débit en bauds réel de la ligne. Ce paramètre est nécessaire pour déterminer la fin du transfert.
 #define CFG_LINE_BAUDRATE_BPS (9600) 


Intervalle d'accumulation de données pour l'algorithme Nagle. Insolemment, prenez-le égal à 100 millisecondes. Pendant ce temps, nous attendons un ensemble de paquets, s'il n'est pas tapé, nous l'envoyons tel quel. La tâche de l'algorithme Nagle est de débarrasser le réseau d'un tas de petits paquets d'un à plusieurs octets.
 #define CFG_NAGLE_DELAY_MS (100) 


Ces paramètres définissent les vitesses de port pour la communication avec le système de contrôle (hôte) et la ligne. Ne confondez pas la vitesse du port avec une ligne avec un taux de transfert physique.
 #define CFG_HOST_CONNECTION_BAUDRATE_BPS (9600) // Host connection port speed #define CFG_LINE_CONNECTION_BAUDRATE_BPS (9600) // Line connection port speed 


Si ce paramètre est activé, lorsque le courant est fourni au contrôleur, le protocole lui-même se commandera pour commencer à établir une connexion.
 #define CFG_IS_AUTOSTART_ON_POWERON (true) 


Il s'agit de la taille en octets de la mémoire tampon pour les paquets de données entrants.
 #define CFG_IL_RING_SIZE (255) 


Voyons ensuite à quoi ressemble la boucle d'esquisse principale:
 void loop() { uMCP_ITimers_Process(); DC_Input_Process(); DC_Output_Process(); //  ip_ready  ,      if (ip_ready) { uMCP_OnIncomingPacket(); } //        ,     -  if ((state == uMCP_STATE_HALTED) && ((ih_Cnt > 0) || (isStartup && CFG_IS_AUTOSTART_ON_POWERON))) { if (isStartup) { isStartup = false; } uMCP_STATE_Set(uMCP_STATE_ISTART); uMCP_CtrlSend(uMCP_PTYPE_STR, 0, 0, true); } else if (state == uMCP_STATE_RUNNING) { uMCP_Protocol_Perform(); //      -   if (il_ready) { il_ready = false; USER_uMCPIno_DataPacketReceived(); } } } 


Voyons maintenant comment fonctionne le protocole. La logique principale est contenue dans la fonction uMCP_Protocol_Perform (); Voici son code:
 void uMCP_Protocol_Perform() { if (state == uMCP_STATE_RUNNING) { //              SELECT  if ((!iTimer_State[uMCP_Timer_TX]) && (!iTimer_State[uMCP_Timer_TMO]) && (select)) { //     if (ih_Cnt == 0) { //    REP -  if (srep) { uMCP_CtrlSend(uMCP_PTYPE_REP, N, R, true); srep = false; } //     -   else if (sentBlocksCnt > 0) { uMCP_DataBlockResend((A + 1) % UMCP_PACKETS_NUMBER, true, true); } //    SACK  -        //  -       ACK else if ((!selectDefaultState) || (sack)) { uMCP_CtrlSend(uMCP_PTYPE_ACK, N, R, false); sack = false; } } //     -  else if (ih_Cnt > 0) { //              -  if ((ih_Cnt >= UMCP_PACKET_DATA_SIZE) || (millis() >= ih_TS + CFG_NAGLE_DELAY_MS)) { //   N N = (N + 1) % UMCP_PACKETS_NUMBER; uMCP_NextDataBlockSend(); } } } } } 


Un analyseur de paquets qui vit dans une fonction
 On_NewByte_From_Line 
également organisé selon le principe d'une machine à états finis et fonctionne "octet par octet". Ceci est fait afin d'économiser de la mémoire.

Le reste de la mise en œuvre ne présente pas d'intérêt particulier. Nous analyserons mieux la façon dont l'utilisateur interagit avec le protocole. Dans cet exemple, il existe quatre «points de contact».

Le premier est la fonction d'envoi de données sur la ligne uMCPIno:
 bool uMCPIno_SendData(byte* dataToSend, byte dataSize); 

Tout est simple ici - vous avez un tampon d'octets dataToSend, sa taille est dataSize. La fonction renvoie vrai si l'envoi est possible (il y a de la place pour ajouter des données), et faux sinon.
Afin de ne pas conduire en vain, vous pouvez immédiatement vérifier la disponibilité de suffisamment d'espace en utilisant la fonction:
 bool uMCP_IsCanSend(byte dataSize); 


Afin d'analyser les paquets de données entrants, vous devez ajouter votre code au corps de la fonction
 void USER_uMCPIno_DataPacketReceived(); 


Les données entrantes sont écrites dans le tampon en anneau il_ring. La lecture peut être organisée comme ceci:
 while (il_Cnt > 0) { c = il_ring[il_rPos]; il_rPos = (il_rPos + 1) % CFG_IL_RING_SIZE; il_Cnt--; //   "c" -      } 


Pour les plaisirs sophistiqués, il y a une fonction
  void USER_uMCP_OnTxBufferEmptry(); 

Qui est appelé lorsque toutes les données ont été envoyées avec succès. Il est également possible et nécessaire d'y mettre une sorte de code.

Pourquoi tout cela et où?


Je m'occupais principalement de Just for fun. De plus, j'avais besoin d'un protocole simple et, surtout, «léger» pour envoyer des données via nos modems sonar uWAVE . Puisqu'ils transmettent des données dans l'eau à une vitesse de seulement 80 bps, et avec leur portée de communication maximale de 1000 mètres et la vitesse du son dans l'eau d'environ 1500 m / s, la transmission est associée à des retards notables, et il n'y a qu'un seul canal sonar (sinon le plus !) des plus bruyants, des plus lents et des plus instables.
En grande partie à cause de cela, j'ai dû abandonner le mécanisme de reconnaissance négative (NAK) - s'il est possible de ne pas transmettre - dans l'eau il vaut mieux ne pas transmettre à 100%.
En réalité, le protocole a été utile lors de la transmission de données sur un canal radio à l'aide de modules DORJI et du NS-012 bien connu des arduinoes.

Et ensuite?


S'il reste du temps, je prévois d'ajouter la possibilité d'adressage (qui, soit dit en passant, était en DDCMP). Puisque la tâche principale de ce protocole est maintenant de fournir une commodité pour toutes sortes de tests de nos modems sonar et d'autres réseaux de capteurs, il y a (littéralement!) Des pièges là-bas. Je peux seulement dire que le problème ne peut pas être résolu en ajoutant simplement les champs "Sender" et "Target".
Cela viendra peut-être du routage géographique et de tout ce jazz.

PS


Traditionnellement, je serai très reconnaissant des critiques constructives, des souhaits et des suggestions. Il est toujours important de comprendre si vous faites quelque chose d'utile pour les gens ou si vous perdez du temps.
Peut-être, en essayant d'éviter la transition de cette longue lecture au roman "Guerre et paix", j'ai manqué quelques détails - n'hésitez pas à demander.

PPS


Un grand merci à la honte de mon analphabétisme, signalant des erreurs (grammaticales et logiques):

Le projet était à l'origine open source, mais maintenant l'article est également open source.

Source: https://habr.com/ru/post/fr480110/


All Articles