11. Fichiers IQ et SigMF¶
Dans tous nos exemples Python prĂ©cĂ©dents, nous avons stockĂ© les signaux sous forme de tableaux NumPy 1D de type âflottants complexesâ. Dans ce chapitre, nous apprenons comment les signaux peuvent ĂȘtre stockĂ©s dans un fichier, puis relus dans Python, et nous prĂ©sentons la norme SigMF. Le stockage des donnĂ©es de signaux dans un fichier est extrĂȘmement utile ; vous pouvez souhaiter enregistrer un signal dans un fichier afin de lâanalyser manuellement hors ligne, de le partager avec un collĂšgue ou de constituer un ensemble de donnĂ©es complet.
Fichiers binaires¶
Rappelons quâun signal numĂ©rique en bande de base est une sĂ©quence de nombres complexes.
Exemple : [0.123 + j0.512, 0.0312 + j0.4123, 0.1423 + j0.06512, âŠ]
Ces nombres correspondent Ă [I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, I+jQ, âŠ].
Lorsque nous voulons enregistrer des nombres complexes dans un fichier, nous les enregistrons au format IQIQIQIQIQIQIQIQ. Câest-Ă -dire que nous stockons un tas de flottants dans une rangĂ©e, et lorsque nous les relisons, nous devons les sĂ©parer en [I+jQ, I+jQ, âŠ].
Bien quâil soit possible de stocker les nombres complexes dans un fichier texte ou un fichier csv, nous prĂ©fĂ©rons les enregistrer dans ce que lâon appelle un âfichier binaireâ pour gagner de lâespace. Ă des taux dâĂ©chantillonnage Ă©levĂ©s, vos enregistrements de signaux peuvent facilement atteindre plusieurs Go, et nous voulons ĂȘtre aussi Ă©conomes en mĂ©moire que possible. Si vous avez dĂ©jĂ ouvert un fichier dans un Ă©diteur de texte et quâil semblait incomprĂ©hensible comme la capture dâĂ©cran ci-dessous, il sâagissait probablement dâun fichier binaire. Les fichiers binaires contiennent une sĂ©rie dâoctets, et vous devez garder la trace du format vous-mĂȘme. Les fichiers binaires sont le moyen le plus efficace de stocker des donnĂ©es, en supposant que toutes les compressions possibles ont Ă©tĂ© effectuĂ©es. Ătant donnĂ© que nos signaux ressemblent gĂ©nĂ©ralement Ă une sĂ©quence alĂ©atoire de flottants, nous nâessayons gĂ©nĂ©ralement pas de compresser les donnĂ©es. Les fichiers binaires sont utilisĂ©s pour beaucoup dâautres choses, par exemple pour les programmes compilĂ©s (appelĂ©s âbinairesâ). Lorsquâils sont utilisĂ©s pour enregistrer des signaux, nous les appelons âfichiers IQâ binaires, en utilisant lâextension de fichier .iq.
En Python, le type complexe par dĂ©faut est np.complex128, qui utilise deux flottants de 64 bits par Ă©chantillon. Mais en DSP/SDR, nous avons tendance Ă utiliser des flottants de 32 bits Ă la place, car les ADC de nos SDR nâont pas tant de prĂ©cision que cela pour justifier des flottants de 64 bits. En Python, nous utiliserons np.complex64, qui utilise deux flottants de 32 bits. Lorsque vous traitez simplement un signal en Python, cela nâa pas vraiment dâimportance, mais lorsque vous allez enregistrer le tableau 1d dans un fichier, vous voulez dâabord vous assurer quâil sâagit dâun tableau de np.complex64.
Exemples Python¶
En Python, et spécifiquement en numpy, nous utilisons la fonction tofile()
pour enregistrer un tableau numpy dans un fichier. Voici un court exemple de crĂ©ation dâun simple signal BPSK plus du bruit et de son enregistrement dans un fichier dans le mĂȘme rĂ©pertoire que celui Ă partir duquel nous avons exĂ©cutĂ© notre script :
.. code-block:: python
import numpy as np import matplotlib.pyplot as plt
num_symbols = 10000
x_symbols = np.random.randint(0, 2, num_symbols)*2-1 # -1 et 1 n = (np.random.randn(num_symbols) + 1j*np.random.randn(num_symbols))/np.sqrt(2) # AWGN de puissance unitaire r = x_symbols + n * np.sqrt(0.01) # puissance du bruit de 0.01 print(r) plt.plot(np.real(r), np.imag(r), â.â) plt.grid(True) plt.show()
# Sauvegarder le fichier IQ print(type(r[0])) # VĂ©rifier le type de donnĂ©es. Oups, câest 128 et non 64 ! r = r.astype(np.complex64) # Convertir en64 print(type(r[0])) # Verifier que câest bien 64 r.tofile(âbpsk_in_noise.iqâ) # Sauvegarder le fichier
Maintenant, examinez les dĂ©tails du fichier produit et vĂ©rifiez combien dâoctets il contient. Ce devrait ĂȘtre num_symbols * 8 parce que nous avons utilisĂ© np.complex64, ce qui reprĂ©sente 8 octets par Ă©chantillon, 4 octets par flottant (2 flottants par Ă©chantillon).
En utilisant un nouveau script Python, nous pouvons lire ce fichier en utilisant np.fromfile()
, comme ceci :
import numpy as np
import matplotlib.pyplot as plt
samples = np.fromfile('bpsk_in_noise.iq', np.complex64) # Lire dans le fichier. Nous devons lui dire quel est son format
print(samples)
# Tracez la constellation pour vous assurer qu'elle est correcte
plt.plot(np.real(samples), np.imag(samples), '.')
plt.grid(True)
plt.show()
Une grosse erreur est dâoublier dâindiquer Ă np.fromfile() le format du fichier. Les fichiers binaires nâincluent aucune information sur leur format. Par dĂ©faut, np.fromfile() suppose quâil lit un tableau de float64s.
La plupart des autres langages ont des mĂ©thodes pour lire les fichiers binaires, par exemple, dans MATLAB vous pouvez utiliser fread(). Pour lâanalyse visuelle dâun fichier RF, voir la section ci-dessous.
Si vous vous retrouvez un jour Ă traiter des int16 (alias ints courts), ou tout autre type de donnĂ©es pour lequel numpy nâa pas dâĂ©quivalent complexe, vous serez obligĂ© de lire les Ă©chantillons en tant que rĂ©els, mĂȘme sâils sont en fait complexes. Lâastuce est de les lire en tant que rĂ©els, mais ensuite de les entrelacer dans le format IQIQIQ⊠vous-mĂȘme, quelques maniĂšres diffĂ©rentes de le faire sont montrĂ©es ci-dessous :
samples = np.fromfile('iq_samples_as_int16.iq', np.int16).astype(np.float32).view(np.complex64)
or
samples = np.fromfile('iq_samples_as_int16.iq', np.int16)
samples /= 32768 # convertir en -1 en +1 (facultatif)
samples = samples[::2] + 1j*samples[1::2] # convertir en IQIQIQ...
Analyse visuelle dâun fichier RF¶
Bien que nous ayons appris Ă crĂ©er notre propre tracĂ© de spectrogramme dans le chapitre Domaine frĂ©quentiel, rien ne vaut lâutilisation dâun logiciel dĂ©jĂ crĂ©Ă©, et quand il sâagit dâanalyser un long enregistrement RF, je recommande dâutiliser inspectrum. Inspectrum est un outil graphique assez simple mais puissant pour balayer visuellement un fichier RF, avec un contrĂŽle fin sur la gamme de cartes de couleurs et la taille de la FFT (quantitĂ© de zoom). Vous pouvez maintenir la touche alt et utiliser la molette de dĂ©filement pour vous dĂ©placer dans le temps. Il dispose de curseurs optionnels pour mesurer le delta-temps entre deux rafales dâĂ©nergie, et la possibilitĂ© dâexporter une tranche du fichier RF dans un nouveau fichier. Pour lâinstallation sur des plateformes basĂ©es sur Debian comme Ubuntu, utilisez les commandes suivantes :
sudo apt-get install qt5-default libfftw3-dev cmake pkg-config libliquid-dev
git clone https://github.com/miek/inspectrum.git
cd inspectrum
mkdir build
cd build
cmake ..
make
sudo make install
inspectrum
Valeurs maximales et saturation¶
Lorsque vous recevez des Ă©chantillons dâun SDR, il est important de connaĂźtre la valeur maximale de lâĂ©chantillon. De nombreux SDR Ă©mettent les Ă©chantillons sous forme de flottants avec une valeur maximale de 1.0 et une valeur minimale de -1.0. Dâautres SDR vous donneront des Ă©chantillons sous forme dâentiers, gĂ©nĂ©ralement 16 bits, auquel cas les valeurs max et min seront +32767 et -32768 (sauf indication contraire), et vous pouvez choisir de diviser par 32 768 pour les convertir en flottants de -1,0 Ă 1,0. La raison pour laquelle il faut connaĂźtre la valeur maximale de votre SDR est due Ă la saturation : lors de la rĂ©ception dâun signal extrĂȘmement fort (ou si le gain est rĂ©glĂ© trop haut), le rĂ©cepteur va âsaturerâ et il va tronquer les valeurs Ă©levĂ©es Ă la valeur maximale de lâĂ©chantillon. Les ADCs de nos SDRs ont un nombre limitĂ© de bits. Lorsque vous crĂ©ez une application SDR, il est sage de toujours vĂ©rifier la saturation, et lorsque cela se produit, vous devez lâindiquer dâune maniĂšre ou dâune autre. Un signal qui est saturĂ© aura lâair perturbĂ© dans le domaine temporel, comme ceci :
En raison des changements soudains dans le domaine temporel, dus Ă la troncature, le domaine frĂ©quentiel peut sembler Ă©talĂ©. En dâautres termes, le domaine des frĂ©quences comprendra de fausses caractĂ©ristiques, des caractĂ©ristiques rĂ©sultant de la saturation et ne faisant pas rĂ©ellement partie du signal, ce qui peut dĂ©concerter les gens lors de lâanalyse dâun signal.
SigMF et lâannotation des fichiers IQ¶
Comme le fichier IQ lui-mĂȘme nâest associĂ© Ă aucune mĂ©tadonnĂ©e, il est courant dâavoir un second fichier contenant des informations sur le signal, portant le mĂȘme nom de fichier mais une extension .txt ou autre. Ces informations devraient au minimum inclure la frĂ©quence dâĂ©chantillonnage utilisĂ©e pour collecter le signal, et la frĂ©quence sur laquelle le SDR Ă©tait accordĂ©. AprĂšs lâanalyse du signal, le fichier de mĂ©tadonnĂ©es peut inclure des informations sur les plages dâĂ©chantillonnage des caractĂ©ristiques intĂ©ressantes, telles que les rafales dâĂ©nergie. Lâindex dâĂ©chantillon est simplement un nombre entier qui commence Ă 0 et sâincrĂ©mente Ă chaque Ă©chantillon complexe. Si vous savez quâil y a de lâĂ©nergie entre les Ă©chantillons 492342 et 528492, vous pouvez lire le fichier et extraire cette partie du tableau : samples[492342:528493]
.
Heureusement, il existe dĂ©sormais une norme ouverte qui spĂ©cifie un format de mĂ©tadonnĂ©es utilisĂ© pour dĂ©crire les enregistrements de signaux, connue sous le nom de SigMF. En utilisant une norme ouverte comme SigMF, de multiples parties peuvent partager des enregistrements RF plus facilement, et utiliser diffĂ©rents outils pour opĂ©rer sur les mĂȘmes ensembles de donnĂ©es. Cela permet Ă©galement dâĂ©viter le âbitrotâ des ensembles de donnĂ©es RF oĂč les dĂ©tails de la capture sont perdus au fil du temps en raison de dĂ©tails de lâenregistrement qui ne sont pas colocalisĂ©s avec lâenregistrement lui-mĂȘme. La façon la plus simple (et minimale) dâutiliser le standard SigMF pour dĂ©crire un fichier IQ binaire que vous avez crĂ©Ă© est de renommer le fichier .iq en .sigmf-data et de crĂ©er un nouveau fichier avec le mĂȘme nom mais lâextension .sigmf-meta, et de sâassurer que le champ datatype dans le mĂ©ta-fichier correspond au format binaire de votre fichier de donnĂ©es. Ce fichier mĂ©ta est un fichier en texte clair rempli de json, vous pouvez donc simplement lâouvrir avec un Ă©diteur de texte et le remplir manuellement (nous verrons plus tard comment le faire de maniĂšre automatique). Voici un exemple de fichier .sigmf-meta que vous pouvez utiliser comme modĂšle :
{
"global": {
"core:datatype": "cf32_le",
"core:sample_rate": 1000000,
"core:hw": "PlutoSDR with 915 MHz whip antenna",
"core:author": "Art Vandelay",
"core:version": "1.0.0"
},
"captures": [
{
"core:sample_start": 0,
"core:frequency": 915000000
}
],
"annotations": []
}
Notez que core:cf32_le
indique que votre fichier .sigmf-data est de type IQIQIQIQ⊠avec des flottants 32 bits, câest-Ă -dire np.complex64 comme nous lâavons utilisĂ© prĂ©cĂ©demment. RĂ©fĂ©rez-vous aux spĂ©cifications pour les autres types de donnĂ©es disponibles, par exemple si vous avez des donnĂ©es rĂ©elles au lieu de complexes, ou si vous utilisez des entiers 16 bits au lieu de flottants pour gagner de la place.
En dehors du type de données, les lignes les plus importantes à remplir sont core:sample_rate
et core:frequency
. Il est bon de saisir également des informations sur le matériel (core:hw
) utilisĂ© pour capturer lâenregistrement, comme le type de SDR et lâantenne, ainsi quâune description de ce que lâon sait du ou des signaux dans lâenregistrement dans core:description
. Le core:version
est simplement la version de la norme SigMF utilisée au moment de la création du fichier de métadonnées.
Si vous capturez votre enregistrement RF Ă partir de Python, par exemple en utilisant lâAPI Python pour votre SDR, vous pouvez Ă©viter de devoir crĂ©er manuellement ces fichiers de mĂ©tadonnĂ©es en utilisant le paquetage SigMF Python. Celui-ci peut ĂȘtre installĂ© sur un systĂšme dâexploitation basĂ© sur Ubuntu/Debian comme suit :
pip install sigmf
Le code Python permettant dâĂ©crire le fichier .sigmf-meta pour lâexemple du dĂ©but de ce chapitre, oĂč nous avons enregistrĂ© bpsk_in_noise.iq, est prĂ©sentĂ© ci-dessous :
import datetime as dt
import numpy as np
import sigmf
from sigmf import SigMFFile
# <code pour exemple>
# r.tofile('bpsk_in_noise.iq')
r.tofile('bpsk_in_noise.sigmf-data') # remplacer la ligne ci-dessus par celle-ci
# crérer les metadata
meta = SigMFFile(
data_file='bpsk_in_noise.sigmf-data', # extension optionalle
global_info = {
SigMFFile.DATATYPE_KEY: 'cf32_le',
SigMFFile.SAMPLE_RATE_KEY: 8000000,
SigMFFile.AUTHOR_KEY: 'Your name and/or email',
SigMFFile.DESCRIPTION_KEY: 'Simulation of BPSK with noise',
SigMFFile.VERSION_KEY: sigmf.__version__,
}
)
# créer une clé de capture à l'index temporel 0
meta.add_capture(0, metadata={
SigMFFile.FREQUENCY_KEY: 915000000,
SigMFFile.DATETIME_KEY: dt.datetime.now(dt.timezone.utc).isoformat(),
})
# vérifier les erreurs et écrire sur le disque
assert meta.validate()
meta.tofile('bpsk_in_noise.sigmf-meta') # extension optionalle
Remplacez simplement 8000000
et 915000000
par les variables que vous avez utilisĂ©es pour stocker respectivement la frĂ©quence dâĂ©chantillonnage et la frĂ©quence centrale.
Pour lire un enregistrement SigMF dans Python, utilisez le code suivant. Dans cet exemple, les deux fichiers SigMF doivent ĂȘtre nommĂ©s bpsk_in_noise.sigmf-meta
et bpsk_in_noise.sigmf-data
.
from sigmf import SigMFFile, sigmffile
# charger les données
filename = 'bpsk_in_noise'
signal = sigmffile.fromfile(filename)
samples = signal.read_samples().view(np.complex64).flatten()
print(samples[0:10]) # examinons les 10 premiers Ă©chantillons
# Obtenir certaines métadonnées et toutes les annotations
sample_rate = signal.get_global_field(SigMFFile.SAMPLE_RATE_KEY)
sample_count = signal.sample_count
signal_duration = sample_count / sample_rate
Pour plus de détails, voir la référence the SigMF Python documentation.
Un petit bonus pour ceux qui ont lu jusquâici: le logo SigMF est en fait stockĂ© comme un enregistrement SigMF lui-mĂȘme, et quand le signal est tracĂ© comme une constellation (IQ plot) dans le temps, il produit lâanimation suivante :
Le code Python utilisé pour lire le fichier du logo (situé ici) et produire le gif animé ci-dessus est présenté ci-dessous, pour les curieux :
from pathlib import Path
from tempfile import TemporaryDirectory
import numpy as np
import matplotlib.pyplot as plt
import imageio.v3 as iio
from sigmf import SigMFFile, sigmffile
# charger les données
filename = 'sigmf_logo' # supposez qu'il se trouve dans le mĂȘme rĂ©pertoire que ce script
signal = sigmffile.fromfile(filename)
samples = signal.read_samples().view(np.complex64).flatten()
# Ajoutez des zéros à la fin pour que ce soit clair lorsque l'animation se répÚte.
samples = np.concatenate((samples, np.zeros(50000)))
sample_count = len(samples)
samples_per_frame = 5000
num_frames = int(sample_count/samples_per_frame)
with TemporaryDirectory() as temp_dir:
filenames = []
output_dir = Path(temp_dir)
for i in range(num_frames):
print(f"frame {i} out of {num_frames}")
# tracer le cadre
fig, ax = plt.subplots(figsize=(5, 5))
samples_frame = samples[i*samples_per_frame:(i+1)*samples_per_frame]
ax.plot(np.real(samples_frame), np.imag(samples_frame), color="cyan", marker=".", linestyle="None", markersize=1)
ax.axis([-0.35,0.35,-0.35,0.35]) # garder les axes existants
ax.set_facecolor('black') # couleur d'arriĂšre plan
# Enregister la figure dans un fichier
filename = output_dir.joinpath(f"sigmf_logo_{i}.png")
fig.savefig(filename, bbox_inches='tight')
plt.close()
filenames.append(filename)
# Créer un gif animé
images = [iio.imread(f) for f in filenames]
iio.imwrite('sigmf_logo.gif', images, fps=20)