Tutoriel : Thèmes de programmation
Tags, messages et PMT.
Objectifs
- En savoir plus sur les PMT
- Comprendre ce que sont les tags / ce qu’ils font / quand les utiliser
- Comprendre la différence entre le streaming et la transmission de messages
- Pointez sur le manuel pour une manipulation de bloc plus avancée
Conditions préalables
- Connaissance générale de C ++ et Python
- Tutoriels précédents recommandés:
Jusqu’à présent, nous n’avons discuté que du streaming de données d’un bloc à l’autre. Les données sont souvent constituées d’échantillons, et une architecture de streaming a beaucoup de sens pour ceux-ci. Par exemple, un bloc de pilote de carte son produira constamment des échantillons audio une fois actif.
Dans certains cas, cependant, nous ne voulons pas diriger un flux d’échantillons, mais plutôt transmettre des messages individuels à un autre bloc, tels que «c’est le premier échantillon d’une rafale» ou «changer la fréquence de transmission à 144 MHz» . Ou considérez une couche MAC au-dessus d’un PHY: à des niveaux de communication plus élevés, les données sont généralement transmises dans des PDU (unités de données de protocole) au lieu de flux d’éléments.
Dans GNU Radio, nous avons deux mécanismes pour transmettre ces messages:
- Synchrone à un flux de données, à l’aide de balises de flux
- De manière asynchrone, en utilisant l’ interface de transmission de messages
Avant d’en discuter, considérons ce qu’est un tel message du point de vue de la programmation. Ce pourrait être une chaîne, un vecteur d’éléments, un dictionnaire… tout, vraiment, qui peut être représenté comme un type de données. En Python, ce ne serait pas un problème, car il est faiblement typé, et une variable de message pourrait simplement être affectée tout ce dont nous avons besoin. C ++ d’autre part est fortement typé, et il n’est pas possible de créer une variable sans connaître son type. Ce qui rend les choses plus difficiles, c’est que nous devons pouvoir partager les mêmes objets de données entre Python et C ++. Pour contourner ce problème, nous introduisons des types polymorphes (PMT) .
5.1 Types polymorphes (PMT)
5.1.1 Introduction
Les types polymorphes sont utilisés comme porteurs de données d’un bloc / thread à un autre tels que les balises de flux et les interfaces de passage de messages. Les types de données PMT peuvent représenter une variété de données allant des booléens aux dictionnaires. Mais plongeons directement dans du code Python et voyons comment nous pouvons utiliser les PMT:
>>> import pmt >>> P = pmt.from_long (23) >>> type (P) pmt.pmt_swig.swig_int_ptr >>> print P 23 >>> P2 = pmt.from_complex (1j) >>> type ( P2) pmt.pmt_swig.swig_int_ptr >>> print P2 0 + 1i >>> pmt.is_complex (P2) True
Tout d’abord, le pmt
module est importé. Nous attribuons deux valeurs ( P
et P2
) aux PMT en utilisant respectivement les appels from_long()
et from_complex()
. Comme nous pouvons le voir, ils sont tous les deux du même type! Cela signifie que nous pouvons transmettre ces variables à C ++ via SWIG, et C ++ peut gérer ce type en conséquence.
Le même code que ci-dessus en C ++ ressemblerait à ceci:
#include // [...] pmt :: pmt_t P = pmt :: from_long (23); std :: cout << P << std :: endl; pmt :: pmt_t P2 = pmt :: from_complex (gr_complex (0, 1)); // Alternativement: pmt :: from_complex (0, 1) std :: cout << P2 << std :: endl; std :: cout << pmt :: is_complex (P2) << std :: endl;
Deux choses ressortent à la fois en Python et en C ++: Premièrement, nous pouvons simplement imprimer le contenu d’un PMT. Comment est-ce possible? Eh bien, les PMT ont une capacité intégrée de convertir leur valeur en chaîne (ce n’est pas possible avec tous les types, cependant). Deuxièmement, les PMT doivent évidemment connaître leur type, nous pouvons donc l’interroger, par exemple en appelant la is_complex()
méthode.
Lors de l’attribution d’une valeur non PMT à un PMT, nous pouvons utiliser les from_*
méthodes et utiliser les to_*
méthodes pour reconvertir:
pmt :: pmt_t P_int = pmt :: from_long (42); int i = pmt :: to_long (P_int); pmt :: pmt_t P_double = pmt :: from_double (0,2); double d = pmt :: to_double (P_double); pmt :: pmt_t P_double = pmt :: mp (0,2);
La dernière ligne montre la fonction sténographique pmt :: mp () . Il enregistre essentiellement une certaine frappe, car il déduit la from_
fonction correcte du type donné.
Les types de chaînes jouent un peu un rôle spécial dans les PMT (où ils sont appelés symboles ), comme nous le verrons plus tard, et ont leur propre convertisseur:
pmt :: pmt_t P_str = pmt :: string_to_symbol ("spam"); pmt :: pmt_t P_str = pmt :: intern ("spam"); // Alias pour string_to_symbol () std :: string str = pmt :: symbol_to_string (P_str);
Voir les documents PMT et le fichier d’en-tête pmt.h pour une liste complète des fonctions de conversion. En Python, nous pouvons utiliser le typage faible, et il existe en fait une fonction d’assistance pour effectuer ces conversions (la fonction d’assistance de C ++ pmt::mp()
ne fonctionne que vers PMT, et est moins puissante):
P_int = pmt.to_pmt (42) i = pmt.to_python (P_int) P_double = pmt.to_pmt (0,2) d = pmt.to_double (P_double)
En passant, il existe trois constantes PMT utiles, qui peuvent être utilisées dans les domaines Python et C ++. En C ++, ceux-ci peuvent être utilisés tels quels:
pmt :: pmt_t P_true = pmt :: PMT_T; pmt :: pmt_t P_false = pmt :: PMT_F; pmt :: pmt_t P_nil = pmt :: PMT_NIL;
En Python:
P_true = pmt.PMT_T P_false = pmt.PMT_F P_nil = pmt.PMT_NIL
pmt.PMT_T
et pmt.PMT_F
sont des types booléens PMT représentant respectivement True et False. Le PMT_NIL est comme un NULL ou Aucun et peut être utilisé pour les arguments par défaut ou les valeurs de retour, indiquant souvent qu’une erreur s’est produite.
Pour pouvoir revenir aux types de données C ++, nous devons être en mesure de trouver le type à partir d’un PMT. La famille de is_*
méthodes nous y aide:
double d; if (pmt :: is_integer (P)) { d = (double) pmt :: to_long (P); } else if (pmt :: is_real (P)) { d = pmt :: to_double (P); } else { // Nous attendions vraiment un entier ou un double ici, donc nous ne savons pas quoi faire throw std :: runtime_error ("attendu un entier!"); }
Nous pouvons comparer les PMT sans connaître leur type en utilisant la pmt::equal()
fonction:
if (pmt :: eq (P_int, P_double)) { std :: cout << "Equal!" << std :: endl; // Cette ligne ne sera jamais atteinte }
Il y a plus de fonctions d’égalité, qui comparent différentes choses: pmt::eq()
et pmt::eqv()
. Nous n’en aurons pas besoin pour ce didacticiel.
5.1.2 Types plus complexes
Les PMT peuvent contenir une variété de types. En utilisant la méthode Python pmt.to_pmt()
, nous pouvons convertir la plupart des types standard Pythons prêts à l’emploi:
P_tuple = pmt.to_pmt ((1, 2, 3, 'spam', 'eggs')) P_dict = pmt.to_pmt ({'spam': 42, 'eggs': 23})
Mais qu’est-ce que cela signifie dans le domaine C ++? Eh bien, il existe des types PMT qui définissent les tuples et les dictionnaires , les clés et les valeurs étant des PMT, encore une fois.
Ainsi, pour créer le tuple à partir de l’exemple Python, le code C ++ ressemblerait à ceci:
pmt :: pmt_t P_tuple = pmt :: make_tuple (pmt :: from_long (1), pmt :: from_long (2), pmt :: from_long (3), pmt :: string_to_symbol ("spam"), pmt :: string_to_symbol ( "des œufs"))
Pour le dictionnaire, c’est un peu plus complexe:
pmt :: pmt_t P_dict = pmt :: make_dict (); P_dict = pmt :: dict_add (P_dict, pmt :: string_to_symbol ("spam"), pmt :: from_long (42)); P_dict = pmt :: dict_add (P_dict, pmt :: string_to_symbol ("eggs"), pmt :: from_long (23));
Comme vous pouvez le voir, nous devons d’abord créer un dictionnaire, puis attribuer chaque paire clé / valeur individuellement.
Une variante des tuples sont des vecteurs . Comme les tuples et les listes de Python, les vecteurs PMT sont mutables, contrairement aux tuples PMT. En fait, les vecteurs PMT sont les seuls types de données PMT qui sont mutables. Lorsque vous modifiez la valeur ou ajoutez un élément à un dictionnaire, nous créons en fait un nouveau PMT.
Pour créer un vecteur, nous pouvons l’initialiser à certaines longueurs et remplir tous les éléments avec une valeur initiale. Nous pouvons alors modifier des éléments ou les référencer:
pmt :: pmt_t P_vector = pmt :: make_vector (5, pmt :: from_long (23)); // Crée un vecteur avec 5 23 comme PMT pmt :: vector_set (P_vector, 0, pmt :: from_long (42)); // Change le premier élément en 42 std :: cout << pmt :: vector_ref (P_vector, 0); // Imprime 42
En Python, nous pouvons effectuer toutes ces étapes (en utilisant pmt.make_vector()
etc.), ou convertir une liste:
P_vector = pmt.to_pmt ([42, 23, 23, 23, 23])
Les vecteurs sont également différents des tuples dans le sens où nous pouvons directement charger des types de données dans les éléments, qui ne doivent pas nécessairement être des PMT.
Supposons que nous voulons passer une série de 8 valeurs flottantes à un autre bloc (il peut s’agir par exemple des caractéristiques d’un filtre). Il serait fastidieux de convertir chaque élément vers et depuis les PMT, car tous les éléments du vecteur sont du même type.
Nous pouvons utiliser des types de vecteurs spéciaux dans ce cas:
pmt :: pmt_t P_f32vector = pmt :: make_f32vector (8, 5.0); // Crée un vecteur avec 8 5.0s comme flotteurs pmt :: f32vector_set (P_f32vector, 0, 2.0); // Change le premier élément en un flottant 2.0 f = f32vector_ref (P_f32vector, 0); std :: cout << f << std :: endl; // Imprime 2.0 size_t len; float * fp = pmt :: f32vector_elements (P_f32vector, len); pour (size_t i = 0; i <len; i ++) std :: cout << fp [i] << std :: endl; // Imprime tous les éléments de P_f32vector, l'un après l'autre
Python a un concept similaire: les tableaux Numpy . Comme d’habitude, la bibliothèque PMT comprend cela et convertit comme prévu:
P_f32vector = pmt.to_pmt (numpy.array ([2.0, 5.0, 5.0, 5.0, 5.0], dtype = numpy.float32)) print pmt.is_f32vector (P_f32vector) # Imprime 'True'
Ici, «f32» signifie «float, 32 bits». Les PMT connaissent la plupart des types de données à largeur fixe typiques, tels que ‘u8’ (caractère 8 bits non signé) ou ‘c32’ (complexe avec des flottants 32 bits pour chaque I et Q). Consultez le manuel pour une liste complète des types.
Le type PMT le plus générique est probablement le blob (grand objet binaire). Utilisez-le avec précaution – cela nous permet de faire circuler tout ce qui peut être représenté en mémoire.
5.1.3 Paires, cons-es et car
Un concept qui trouve son origine dans les dialectes lispiens est le couple et le contre . L’explication la plus simple est juste que: si vous combinez deux PMT, ils forment un nouveau PMT, qui est une paire (ou contre) de ces deux PMT (ne vous inquiétez pas du nom étrange, beaucoup de choses originaires de Lisp ont bizarre Pensez à une «construction»).
De la même manière que les vecteurs ou les tuples, les paires sont un moyen utile de regrouper plusieurs composants d’un message dans un seul PMT. En utilisant les PMT générés dans la section précédente, nous pouvons combiner deux d’entre eux pour former une paire, ici en Python:
P_pair = pmt.cons (pmt.string_to_symbol ("taps"), P_f32vector) print pmt.is_pair (P_pair) # Imprime 'true'
Vous pouvez combiner des PMT sous forme de tuples, de dictionnaires, de vecteurs ou de paires, c’est juste une question de goût. Cette construction est cependant bien établie et utilisée en tant que telle dans GNU Radio assez souvent.
Alors comment déconstruire une paire? C’est ce que font les fonctions car
et cdr
. Déconstruisons cette paire précédente en C ++:
pmt :: pmt_t P_key = pmt :: car (P_pair); pmt :: pmt_t P_f32vector2 = pmt :: cdr (P_pair); std :: cout << P_key << std :: endl; // Imprime des 'taps' en utilisant la conversion automatique PMT en chaînes
Pour une manipulation plus avancée des paires, reportez-vous à la documentation et à la page Wikipedia pour car et cdr .
OK, maintenant nous savons tout sur la création de messages – mais comment les envoyer de bloc en bloc?
5.2 Balises de flux
Lorsque deux blocs communiquent déjà via une connexion de flux (par exemple, des échantillons circulent d’un bloc à un autre), nous pouvons utiliser des balises de flux . Une balise de flux est un message qui est connecté à un élément spécifique. De cette façon, nous pouvons envoyer des messages de manière synchrone au flux.
Outre la position fixe dans le flux, les balises de flux ont trois propriétés:
- Une clé , qui peut être utilisée pour identifier une certaine balise
- Une valeur , qui peut être n’importe quel PMT
- (Facultatif) Un ID source , qui permet d’identifier l’origine de cette balise spécifique.
Le choix des blocs d’utiliser ces informations dépend d’eux. La plupart des blocs propageront les balises de manière transparente, ce qui signifie que la balise est attachée au même élément (ou à un élément correspondant) sur la sortie. Les blocs qui utilisent réellement des balises recherchent généralement des balises avec des clés spécifiques et ne les traitent que.
Jetons un coup d’œil à un exemple simple:

Dans ce graphique de flux, nous avons deux sources: une sinusoïde et un stroboscope d’étiquette. Un stroboscope de balise est un bloc qui produira une balise constante, dans ce cas, sur chaque 1000e élément (la valeur réelle des éléments est toujours nulle). Ces sources s’additionnent. Le signal après l’additionneur est identique à l’onde sinusoïdale que nous avons produite, car nous ajoutons toujours des zéros. Cependant, les balises restent attachées à la même position qu’elles provenaient du stroboscope des balises! Cela signifie que chaque millième échantillon de la sinusoïde a maintenant une étiquette. La portée QT peut afficher des balises et même les déclencher.

Nous avons maintenant un mécanisme pour attacher aléatoirement toutes les métadonnées à des éléments spécifiques. Il existe plusieurs blocs qui utilisent des balises. L’un d’eux est le bloc UHD Sink, le pilote utilisé pour la transmission avec les appareils USRP. Il réagira aux balises avec certaines touches, l’une d’entre elles tx_freq
, qui peut être utilisée pour définir la fréquence de transmission d’un USRP pendant la diffusion.
5.2.1 Ajout de balises au démodulateur QPSK
Pour revenir à notre exemple de démodulation QPSK, nous pourrions vouloir ajouter une fonctionnalité pour indiquer aux blocs en aval que la démodulation ne se déroule pas bien. N’oubliez pas que la sortie de notre bloc est toujours une décision difficile, et nous devons sortir quelque chose. Nous pourrions donc utiliser des balises pour signaler que l’entrée n’est pas bien formée et que la sortie n’est pas fiable.
Comme critère d’échec, nous discutons le cas où l’amplitude d’entrée est trop petite, disons inférieure à 0,01. Lorsque l’amplitude tombe en dessous de cette valeur, nous sortons une balise. Une autre balise n’est envoyée que lorsque l’amplitude s’est rétablie et retombe en dessous du seuil. Nous étendons notre fonction de travail comme ceci:
if (std :: abs (in [i]) <0,01 et non d_low_ampl_state) { add_item_tag (0, // Numéro de port nitems_written (0) + i, // Offset pmt :: mp ("amplitude_warning"), // Key pmt :: from_double (std :: abs (in [i])) // Value ); d_low_ampl_state = true; } else if (std :: abs (in [i])> = 0.01 and d_low_ampl_state) { add_item_tag (0, // Numéro de port nitems_written (0) + i, // Offset pmt :: mp ("amplitude_recovered"), / / Key pmt :: PMT_T // Value ); d_low_ampl_state = false; // Réinitialiser l'état }
En Python, le code ressemblerait à ceci (en supposant que nous avons un membre de notre classe de bloc appelé d_low_ampl_state
):
# Le vecteur 'in' est appelé 'in0' ici parce que 'in' est un mot - clé Python si abs (in0 [i]) <0,01 et non d_low_ampl_state: self.add_item_tag (0, # Numéro de port self.nitems_written (0) + i, # Offset pmt.intern ("amplitude_warning"), # Key pmt.from_double (numpy.double (abs (in0 [i]))) # Value # Remarque: Nous devons créer explicitement un "double" ici, # parce que in0 [i] est un flottant explicite de 32 bits ici ) self.d_low_ampl_state = True elif abs (in0 [i])> = 0,01 et d_low_ampl_state: self.add_item_tag (0, # Numéro de port self.nitems_written (0) + i, # Décalage pmt.intern ("amplitude_recovered"),# Clé pmt.PMT_T # Value ) self.d_low_ampl_state = False; // Réinitialiser l'état
Nous pouvons également créer un type de données de tag tag_t et le transmettre directement:
if (std :: abs (in [i]) <0,01 et non d_low_ampl_state) { tag_t tag; tag.offset = nitems_written (0) + i; tag.key = pmt :: mp ("amplitude_warning"); tag.value = pmt :: from_double (std :: abs (dans [i])); add_item_tag (0, tag); d_low_ampl_state = true; }
Voici un graphique de flux qui utilise la version balisée du démodulateur. Nous saisissons 20 symboles QPSK valides, puis 10 zéros. Puisque la sortie de ce bloc est toujours 0, 1, 2 ou 3, nous n’avons normalement aucun moyen de voir si l’entrée n’était pas clairement l’une de ces valeurs.

Voici la sortie. Vous pouvez voir que nous avons des balises sur ces valeurs qui ne proviennent pas d’un symbole QPSK valide, mais de quelque chose de peu fiable.

5.2.2 Décalages de balises et noutput_items
Avant d’expliquer davantage les balises, il est important de comprendre les décalages. Le décalage de balise est une valeur absolue, et spécifique à un certain port! Le premier élément qui passe à travers un bloc a le décalage 0, et il n’y aura qu’un seul échantillon avec ce décalage (OK, avant que les nitpickers ne sortent leurs fourches: le décalage est une valeur 64 bits non signée, donc ils s’enrouleront une fois 2 ^ 64 éléments sont passés. Si vous faites le calcul, vous comprendrez que vous devez laisser un graphique de flux s’exécuter pendant longtemps jusqu’à ce que cela se produise).
La plupart des blocs, cependant, fonctionnent sur des indices d’échantillon relatifs (par exemple, si vous avez une boucle du formulaire for (int i = 0; i < noutput_items; i++)
, ce i
serait un indice d’échantillon relatif, car vous ne vous souciez pas nécessairement de la position absolue de l’échantillon). Si vous savez combien d’échantillons ont été consommés ou produits avant de commencer cette boucle, vous pouvez facilement convertir des indices d’échantillons relatifs en indices absolus en utilisant les méthodes nitems_read(port_num)
et nitems_written(port_num)
(note pour les experts et ceux qui ne veulent pas faire d’erreurs de débogage difficiles: L’appel des fonctions consume()
et produce()
modifiera ces valeurs).
Dans l’exemple suivant, nous allons diffuser en continu des symboles et des zéros QPSK valides. Découvrez comment les balises de flux sont ajoutées aux éléments:
(Faire une image)
Un bloc en aval peut désormais utiliser ces balises. Pour ce faire, nous avons besoin de l’ get_tags_in_range()
appel, qui lit les balises dans une plage donnée de décalages absolus. Pour lire une balise de l’un des cent premiers éléments d’entrée, ce serait un appel valide:
balises std :: vector; get_tags_in_range ( balises, // Les balises seront enregistrées ici 0, // Port 0 nitems_read (0), // Début de la plage nitems_read (0) + 100, // Fin de la plage pmt :: mp ("my_tag_key") // Facultatif: recherchez uniquement les balises avec la clé "my_tag_key" );
Les balises seront toutes enregistrées dans le vecteur tags
. Tout tag connecté aux 100 premiers éléments sera placé dans ce vecteur, mais pas dans un ordre spécifique. Le cinquième argument facultatif nous permet de filtrer les balises avec une clé spécifique, au cas où nous ne rechercherions qu’un seul type de balise.
Il existe un raccourci vers cet appel, appelé get_tags_in_window , qui recherche des balises sur les indices relatifs. Le code suivant trouverait les mêmes balises qu’avant:
balises std :: vector; get_tags_in_window (// Notez les différentes balises de nom de méthode , // Les balises seront enregistrées ici 0, // Port 0 0, // Début de la plage (par rapport à nitems_read (0)) 100, // Fin de la plage relative pmt :: mp ("my_tag_key") // Facultatif: Rechercher uniquement les balises avec la clé "my_tag_key" );
5.2.3 Tag propagation
Nous savons maintenant comment ajouter des balises aux flux et comment les lire. Mais qu’advient-il des tags après leur lecture? Et qu’arrive-t-il aux tags qui ne sont pas utilisés? Après tout, il existe de nombreux blocs qui ne se soucient pas du tout des balises.
La réponse est: cela dépend de la politique de propagation des balises d’un bloc ce qui arrive aux balises qui y entrent.
Vous avez le choix entre trois politiques :
- TPP_ALL_TO_ALL: toute balise qui entre sur un port est propagée automatiquement à tous les ports de sortie (c’est le paramètre par défaut)
- TPP_ONE_TO_ONE: les balises entrant sur le port N sont propagées au port de sortie N. Cela ne fonctionne que pour les blocs avec le même nombre de ports d’entrée et de sortie.
- TPP_DONT: les balises entrant dans le bloc ne sont pas propagées automatiquement. Seules les balises créées dans ce bloc (à l’aide de
add_item_tag()
) apparaissent sur les flux de sortie.
Nous définissons généralement la politique de propagation des balises dans le constructeur du bloc en utilisant @set_tag_propagation_policy
Lorsque la politique de propagation des balises est définie sur TPP_ALL_TO_ALL ou TPP_ONE_TO_ONE, le programmateur GNU Radio utilise toutes les informations disponibles pour déterminer quel élément de sortie correspond à quel élément d’entrée. Le bloc peut les lire et ajouter de nouvelles balises, mais les balises existantes sont automatiquement déplacées en aval d’une manière jugée appropriée.
À titre d’exemple, considérons un bloc d’interpolation. Voir le graphique de flux suivant:


Comme vous pouvez le constater, nous produisons des balises sur chaque 10e échantillon, puis les passons à travers un bloc qui répète chaque échantillon 100 fois. Les balises ne sont pas répétées avec les politiques de propagation de balises standard (après tout, ce ne sont pas des politiques de manipulation de balises ), donc le planificateur veille à ce que chaque balise soit placée sur l’élément de sortie qui correspond à l’élément d’entrée sur lequel elle se trouvait auparavant. Ici, l’ordonnanceur fait une supposition éclairée et place la balise sur le premier des 100 éléments.
Remarque: Nous ne pouvons pas utiliser ici un seul récepteur temporel d’interface graphique QT pour les deux signaux, car ils fonctionnent à un rythme différent. Notez la différence de temps sur l’axe des x!
Sur les blocs de décimation, le comportement est similaire. Considérez ce graphique de flux très simple et la position des échantillons:


Nous pouvons voir qu’aucune étiquette n’est détruite, et les étiquettes sont en effet espacées d’un dixième de l’espacement d’origine de 100 articles. Bien sûr, l’élément réel qui a été passé à travers le bloc peut être détruit ou modifié (pensez à un filtre FIR décimant).
En fait, cela fonctionne avec n’importe quel bloc à taux variable. Notez qu’il y a des cas où la relation entre les positions des balises d’entrée et de sortie est ambiguë, le programmateur GNU Radio essaiera alors de se rapprocher le plus possible.
Voici un autre exemple intéressant: considérons ce graphique de flux, qui a un bloc de retard, et la position des balises après:


Avant le bloc de retard, des balises étaient positionnées au début de la rampe. Après le retard, ils sont toujours dans la même position! Si nous inspections le code source du bloc de retard, nous constaterions qu’il n’y a absolument aucun code de gestion des balises. Au lieu de cela, le bloc déclare un retard au planificateur, qui propage ensuite les balises avec ce retard.
En utilisant ces mécanismes, nous pouvons laisser GNU Radio gérer la propagation des balises pour un grand nombre de cas. Pour les cas spécialisés ou de coin, il n’y a pas d’autre option que de définir la politique de TPP_DONT
propagation des balises et de propager manuellement les balises (en fait, il existe une autre façon: supposons que la plupart des balises se propagent normalement, mais quelques-unes devraient être traitées différemment. Nous pouvons utiliser remove_item_tag()
(DEPRECATED. Sera supprimé en 3.8.) Pour supprimer ces balises de l’entrée; elles ne seront alors plus propagées même si la politique de propagation des balises est définie sur autre chose que TPP_DONT
. Mais c’est une utilisation plus avancée et ne sera pas élaborée sur ici).
Cas d’utilisation: filtres FIR
(Remarque: cette section nécessite une connaissance du traitement du signal numérique, vous pouvez passer à la section suivante si vous ne comprenez pas certaines des expressions).
Supposons que nous ayons un bloc qui est en fait un filtre FIR. Nous voulons laisser GNU Radio gérer la propagation des balises. Comment configurer le bloc?
Maintenant, un filtre FIR a une entrée et une sortie. Donc, peu importe si nous définissons la politique de propagation sur TPP_ALL_TO_ALL
ou TPP_ONE_TO_ONE
, et nous pouvons la laisser par défaut. Les éléments entrant et sortant sont différents, alors comment faire correspondre l’entrée à la sortie? Puisque nous voulons conserver le timing de la position de la balise, nous devons utiliser le retard de groupe du filtre comme retard pour les balises (ce qui correspond à ce filtre FIR symétrique (N-1)/2
, où N
est le nombre de prises de filtre). Enfin, nous pouvons interpoler, décimer ou les deux (par exemple, pour les changements de fréquence d’échantillonnage) et nous devons également en informer l’ordonnanceur.
5.3 Message Passing
L’ interface de transmission des messages fonctionne complètement différemment des balises de flux. Les messages sont de purs PMT et ils n’ont pas de décalage (cela n’aurait aucun sens de toute façon, car ils ne sont pas connectés à un élément, qui à son tour a un décalage) et pas de clés (bien que nous puissions créer des paires clé / valeur en créant le PMT une paire ou un dictionnaire). Une autre différence est que nous avons un type de port différent pour les messages, qui ne peut pas être envoyé aux ports de streaming réguliers. Nous appelons ces nouveaux types de ports des ports de messages , par opposition au type que nous avons utilisé jusqu’à présent, que nous appellerons ports de streaming si nous devons être explicites.
Les messages ne sont généralement pas utilisés pour les échantillons, mais plutôt pour les paquets ou les données qui ont du sens à être déplacés de manière en paquets. Bien sûr, il existe de nombreux cas où les interfaces de transmission de messages et de streaming peuvent être utilisées, et c’est à nous de choisir celle que nous préférons.
Voici un exemple simple d’un graphique de flux utilisant à la fois le streaming et les messages:

Il y a plusieurs choses intéressantes à souligner. Premièrement, il existe deux blocs source, qui produisent tous les deux des éléments à intervalles réguliers, un tous les 1000 et un tous les 750 millisecondes. Les lignes pointillées indiquent les ports de messages connectés, contrairement aux lignes pleines, qui désignent les ports de streaming connectés. Dans la moitié supérieure du graphique de flux, nous pouvons voir qu’il est en fait possible de basculer entre les ports de transmission de messages et de streaming, mais uniquement si le type des PMT correspond au type des ports de streaming (dans cet exemple, le la couleur rose des ports de streaming indique des octets, ce qui signifie que le PMT doit être un vecteur u8 si nous voulons diffuser les mêmes données que nous avons envoyées en tant que PMT).
Un autre fait intéressant est que nous pouvons connecter plus d’un port de sortie de message à un seul port d’entrée de message, ce qui n’est pas possible avec les ports de streaming. Cela est dû à la nature asynchrone des messages: le bloc de réception traitera tous les messages chaque fois qu’il en aura la possibilité, et pas nécessairement dans un ordre spécifique. La réception de messages provenant de plusieurs blocs signifie simplement qu’il pourrait y avoir plus de messages à traiter.
Qu’advient-il d’un message une fois qu’il a été publié dans un bloc? Cela dépend de l’implémentation réelle du bloc, mais il existe deux possibilités:
1) Un gestionnaire de messages est appelé, qui traite le message immédiatement.
2) Le message est écrit dans une mémoire tampon FIFO, et le bloc peut l’utiliser à tout moment, généralement dans la fonction de travail.
Pour un bloc qui possède à la fois des ports de messages et des ports de diffusion en continu, l’une de ces deux options est OK, selon l’application. Cependant, nous déconseillons fortement le traitement des messages à l’intérieur d’une fonction de travail et recommandons plutôt l’utilisation de gestionnaires de messages. L’utilisation de messages dans la fonction de travail nous encourage à bloquer le travail en attendant l’arrivée d’un message. C’est un mauvais comportement pour une fonction de travail, qui ne doit jamais se bloquer. Si un bloc dépend d’un message pour fonctionner, utilisez le concept de gestionnaire de messages pour recevoir le message, qui peut ensuite être utilisé pour informer les actions du bloc lorsque la fonction de travail est appelée. Ce n’est que dans des occasions spécialement bien identifiées que nous devons utiliser la méthode 2 ci-dessus dans un bloc.
Avec une interface de transmission de messages, nous pouvons écrire des blocs qui n’ont pas de ports de streaming, puis la fonction de travail devient inutile, car c’est une fonction conçue pour fonctionner sur des éléments en streaming. En fait, les blocs qui n’ont pas de ports de streaming n’ont généralement pas de fonction de travail.
5.3.1 PDU
Dans le graphique de flux précédent, nous avons un bloc appelé PDU to Tagged Stream . Une PDU (unité de données de protocole) dans GNU Radio a un type PMT spécial, c’est une paire de dictionnaire (sur CAR) et un type de vecteur uniforme. Ainsi, cela donnerait une PDU valide, sans métadonnées et 10 zéros en tant que données de flux:
pdu = pmt.cons (pmt.make_dict (), pmt.make_u8vector (10, 0))
Les paires clé / valeur du dictionnaire sont alors interprétées comme des paires clé / valeur de balises de flux.
5.3.2 Ajout d’un message passant au code
Contrairement aux ports de streaming, les ports de message ne sont pas définis dans la signature d’E / S, mais sont déclarés avec les message_port_register_*
fonctions. Les ports de messages ont également des identifiants au lieu de numéros de port; ces identifiants sont également des PMT. Pour ajouter des ports d’entrée ou de sortie pour les messages à un bloc, le constructeur doit être modifié par des lignes comme celles-ci:
// Mettez ceci dans le constructeur pour créer des ports de message message_port_register_in (pmt :: mp ("in_port_name")); message_port_register_out (pmt :: mp ("out_port_name"));
Pour configurer une fonction comme gestionnaire de messages pour un port de message d’entrée qui est appelé à chaque fois que nous recevons un message, nous ajoutons une ligne comme celle-ci:
// Mettez ceci dans le constructeur après la définition du port d'entrée: set_msg_handler ( pmt :: mp ("in_port_name"), // Ceci est l'identifiant du port boost :: bind (& block_class_name :: msg_handler_method, this, _1) // [FIXME nom de classe] Liez la méthode de classe );
Nous utilisons labind
technique de Boost pour informer GNU Radio de la fonction que nous voulons appeler pour les messages sur ce port d’entrée. Si vous ne savez pas boost::bind
, ne vous inquiétez pas, la syntaxe est toujours la même: le premier argument est un pointeur vers la méthode qui est appelée, le second est this
donc la méthode est appelée à partir de la classe correcte et le troisième argument est _1
, ce qui signifie que la fonction prend un argument, qui est le message PMT.
Tous les gestionnaires de messages, comme la fonction «msg_handler_method» utilisée ci-dessus, ont le même prototype de fonction et sont membres de la classe d’implémentation du bloc:
void msg_handler_method (pmt :: pmt_t msg);
Dans un bloc Python, ces lignes ressembleraient à ceci:
# Mettez ceci dans le constructeur pour créer des ports de messages self.message_port_register_in (pmt.intern ("in_port_name")) self.message_port_register_out (pmt.intern ("out_port_name")) self.set_msg_handler (pmt.intern ("in_port_name"), self .msg_handler_method) # Aucune liaison nécessaire, nous pouvons passer la fonction directement
Les entrées et sorties de message peuvent être connectées de manière similaire aux ports de streaming uniquement en utilisant msg_connect
au lieu de connect
:
tb = gr.top_block () # Envoyer la chaîne "message" une fois toutes les 1000 ms src = blocks.message_strobe (pmt.to_pmt ("message"), 1000) dbg = blocks.message_debug () tb.msg_connect (src, "pdus ", dbg," print ")
Notez l’ msg_connect()
appel au lieu de la connect()
fonction que nous utilisons pour les ports de streaming.
5.3.3 Exemple: application de conversation
Construisons une application qui utilise la transmission de messages. Un programme de chat est un cas d’utilisation idéal, car il attend que l’utilisateur tape un message, puis l’envoie.
Nous allons créer deux blocs: un pour l’envoi du message et un pour la réception. Ce dernier est assez simple: lorsqu’il reçoit un message, il l’imprime sur la console. Le premier est un peu plus fantaisiste: il va d’abord ajouter une chaîne, que nous utilisons pour identifier le nom d’utilisateur de la personne qui transmet. En outre, il supprimera tous les caractères non imprimables de la chaîne (nous ferons de même dans le récepteur, juste pour nous assurer que personne ne fout notre console avec des caractères amusants).
Enfin, précisons que nous transmettrons les données sous forme de vecteurs u8. Cela rend nos blocs compatibles avec les autres blocs de GNU Radio.
Jetons un coup d’œil aux blocs: [1]
Le premier bloc,, chat_sanitizer
a une fonction, post_message()
qui peut être appelée de l’extérieur pour initier un transfert de message. Il fait tout le filtrage de chaîne que nous avons mentionné précédemment, puis convertit la chaîne en u8vector.
Le deuxième bloc chat_receiver
,, possède également une fonction ( handle_msg()
) qui est toujours appelée lorsqu’un message est publié sur ce bloc. Ceci est activé en enregistrant cette fonction en tant que gestionnaire de messages.
Notez que nous ne transmettons pas simplement le u8vector, mais plutôt une paire du u8vector et un autre PMT, que nous pouvons utiliser pour les métadonnées. Par exemple, un bloc qui convertit un flux en PDU produirait les données du flux en tant que données et les balises en tant que métadonnées dans cette paire PMT.
Ce fichier contient les deux blocs que nous pouvons utiliser dans les graphiques de flux, mais il peut également être exécuté directement. Dans ce cas, il connectera directement ces deux blocs et fournira une invite à l’utilisateur afin que nous puissions tester ces blocs.
Un exemple plus élaboré est donné dans [2] . Ici, nous pouvons voir comment ces deux blocs sont utilisés pour créer un graphique de flux qui utilise réellement le mappage QPSK pour transférer les données.
5.4 Quiz
- Si vous souhaitez créer un PMT avec 100 échantillons complexes, comment l’initialiser?
- Supposons que vous avez créé un PMT en utilisant `pmt :: to_double ()`, mais que vous souhaitez affecter la valeur à un type de données entier (par exemple `int pmt_val`). Comment tu fais ça?
- Qu’est-ce qu’un gestionnaire de messages?
- Supposons que vous ayez écrit un bloc TCP, qui transmet les données à un bloc IP. Souhaitez-vous utiliser la transmission de messages ou l’interface de streaming?
- Supposons que vous disposez d’un bloc de détection de rafale qui diffuse des échantillons vers un bloc de démodulation en aval. Vous souhaitez maintenant également transmettre des métadonnées au bloc en aval. Souhaitez-vous utiliser des balises de flux ou une interface de message?