Messages de vitesse en vol
Javions – étape 6
1. Introduction
Le but principal de cette sixième étape est d'écrire la classe représentant le dernier type de message ADS-B traité dans ce projet, qui permet aux aéronefs de communiquer leur vitesse de déplacement lorsqu'ils sont en vol.
Notez que cette étape devra être rendue deux fois :
- pour le rendu testé habituel (délai : 31/3 18h00),
- pour le rendu intermédiaire (délai : 7/4 18h00).
Le deuxième de ces rendus sera corrigé par lecture du code de vos étapes 1 à 6, et il vous faudra donc soigner sa qualité et sa documentation. Il est fortement conseillé de lire notre guide à ce sujet.
2. Concepts
2.1. Messages de vitesse en vol
Les messages de vitesse en vol (airborne velocity) permettent aux aéronefs de communiquer leur vitesse et direction de déplacement lorsqu'ils sont en vol.
Ces messages ont un code de type valant 19, stocké pour mémoire dans les 5 bits de poids fort de l'attribut ME
du message, qui en fait 56. Les parties utiles à ce projet des 51 bits restants sont décrites dans la table ci-dessous.
Nom | Début | Bits | Contenu |
---|---|---|---|
ST |
48 | 3 | Sous-type |
IC |
47 | 1 | inutilisé dans ce projet |
IFR |
46 | 1 | inutilisé dans ce projet |
NUC |
43 | 3 | inutilisé dans ce projet |
21 | 22 | dépend du sous-type | |
VR |
10 | 11 | inutilisé dans ce projet |
8 | 2 | réservé | |
dALT |
0 | 8 | inutilisé dans ce projet |
L'attribut ST
donne le sous-type du message, qui permet de savoir comment interpréter les 22 bits commençant au bit 21. Dans un message valide, interprété comme un entier non signé, le sous-type — qu'il ne faut pas confondre avec le code de type — ne peut valoir que 1, 2, 3 ou 4.
La raison pour laquelle il existe 4 sous-types différents est d'une part que la vitesse peut être mesurée dans deux unités différentes, en fonction de si l'aéronef est en vol subsonique ou supersonique, et d'autre part que la vitesse et la direction de déplacement peuvent être mesurées de deux manières différentes, comme décrit ci-après.
2.1.1. Mesure de la vitesse et de la direction de déplacement
Une première manière de mesurer la vitesse d'un aéronef consiste à la mesurer par rapport au sol, pour obtenir ce que l'on nomme la vitesse sol (ground speed). Cela revient à mesurer la vitesse de l'ombre que l'aéronef projette sur le sol, en admettant que le soleil se trouve exactement au-dessus de lui.
Une seconde manière de mesurer la vitesse consiste à le faire par rapport à la masse d'air dans laquelle se trouve l'aéronef, par exemple en lui attachant un anémomètre, pour obtenir ce que l'on nomme la vitesse air (air speed).
Ces deux manières de mesurer la vitesse d'un aéronef diffèrent, car la masse d'air dans laquelle il se trouve n'est généralement pas stationnaire. Ainsi, un ballon à air chaud — qui n'a aucun moyen propre de se déplacer horizontalement — a toujours une vitesse air nulle. Par contre, sa vitesse sol ne l'est souvent pas, car il se trouve dans une masse d'air qui se déplace.
La vitesse sol est celle mesurée par les systèmes de positionnement par satellite — GPS ou autres — et est généralement celle qui est communiquée par les aéronefs, car c'est la plus utile des deux pour le trafic aérien. La vitesse air est mesurée quant à elle au moyen d'un anémomètre situé sur l'aéronef.
La direction de déplacement peut également être mesurée de différentes manières. La première consiste à déterminer — conceptuellement en tout cas — la direction dans laquelle se déplace l'ombre de l'aéronef sur le sol, que l'on nomme sa route (track). La seconde consiste à mesurer, au moyen d'une boussole, la direction dans laquelle pointe le nez de l'aéronef, que l'on nomme son cap (heading). Là aussi, ces deux types de mesure produisent généralement des résultats différents en raison du mouvement de la masse d'air dans laquelle évolue l'aéronef.
2.1.2. Déplacement par rapport au sol
Lorsqu'un message de vitesse en vol a un sous-type valant 1 ou 2, il communique la vitesse sol de l'aéronef, ainsi que sa route. Dans ce cas, le contenu des 22 bits qui dépendent du sous-type est donné par la table suivante :
Nom | Début | Bits | Contenu |
---|---|---|---|
Dew |
21 | 1 | Sens de la composante est-ouest de la vitesse |
Vew |
11 | 10 | Composante est-ouest de la vitesse (+1) |
Dns |
10 | 1 | Sens de la composante nord-sud de la vitesse |
Vns |
0 | 10 | Composante nord-sud de la vitesse (+1) |
Chacune des paires d'attributs — Dns/Vns
d'un côté et Dew/Vew
de l'autre — représente une composante du vecteur vitesse. Les attributs Dns
et Dew
indiquent le sens de la composante, tandis que les attributs Vns
et Vew
indiquent sa valeur absolue plus un. En effet, lorsque Vns
ou Vew
valent 0, la vitesse est inconnue.
Lorsque Dns
vaut 0, l'aéronef se dirige du sud au nord, et lorsqu'il vaut 1 il se dirige du nord au sud ; lorsque Dew
vaut 0, l'aéronef se dirige de l'ouest vers l'est, et lorsqu'il vaut 1 il se dirige de l'est vers l'ouest.
Une fois les composantes du vecteur vitesse obtenues, on peut en calculer la norme pour obtenir la vitesse, et l'angle pour obtenir la direction de déplacement.
Le sous-type 1 est destiné à communiquer la vitesse des avions volant à vitesse subsonique, et le vecteur vitesse est alors exprimé en nœuds. Le sous-type 2 est destiné aux avions supersoniques, et le vecteur vitesse et alors exprimé en une unité qui ne porte pas de nom mais qui correspond à 4 nœuds.
2.1.3. Déplacement dans l'air
Lorsqu'un message de vitesse en vol a un sous-type valant 3 ou 4, il communique la vitesse air de l'aéronef, ainsi que son cap. Dans ce cas, le contenu des 22 bits qui dépendent du sous-type est donné par la table suivante :
Nom | Début | Bits | Contenu |
---|---|---|---|
SH |
21 | 1 | Disponibilité du cap |
HDG |
11 | 10 | Cap |
T |
10 | 1 | inutilisé dans ce projet |
AS |
0 | 10 | Vitesse air (+1) |
Si le bit SH
vaut 1, alors l'attribut HDG
contient le cap de l'aéronef, c.-à-d. la direction dans laquelle son nez pointe. En interprétant cet attribut comme un entier non signé puis en le divisant par 210, on obtient le cap exprimé en tours. L'angle ainsi obtenu est celui séparant le nord et la direction dans laquelle pointe le nez de l'aéronef, mesuré dans le sens horaire.
Ainsi, le cap d'un aéronef vaut 0° s'il pointe son nez vers le nord, 90° s'il le pointe vers l'est, 180° s'il le pointe vers le sud, et 270° s'il le pointe vers l'ouest.
Quand le sous-type du message vaut 3, la vitesse air contenue dans l'attribut AS
est exprimée en nœuds, alors qu'elle est exprimée en « 4 nœuds » si le sous-type vaut 4. Dans les deux cas, la valeur stockée dans l'attribut AS
est la vitesse plus un, et lorsque cet attribut contient 0, la vitesse est inconnue.
2.1.4. Unification
Même si les deux concepts de vitesse et de direction de déplacement décrits ci-dessus sont différents, nous ne les distinguerons pas dans ce projet, dans un soucis de simplicité.
Nous considérerons donc que la vitesse d'un aéronef sera celle qu'il nous aura transmise dans un message de vitesse en vol, sans faire de distinction entre vitesse sol et vitesse air. De même, nous ne ferons pas de distinction entre route et cap.
Bien entendu, nous devrons distinguer les deux sous-types de messages décrits ci-dessus afin de correctement interpréter leur contenu, mais une fois qu'une vitesse et une direction auront été extraits d'un tel message, les deux sous-types seront traités de manière identique.
3. Mise en œuvre Java
3.1. Enregistrement AirborneVelocityMessage
L'enregistrement AirborneVelocityMessage
du sous-paquetage adsb
, public, représente un message de vitesse en vol du type décrit à la §2.1. Il implémente l'interface Message
et ses attributs sont :
long timeStampNs
, l'horodatage du message, en nanosecondes,IcaoAddres icaoAddress
, l'adresse OACI de l'expéditeur du message,double speed
, la vitesse de l'aéronef, en m/s,double trackOrHeading
, la direction de déplacement de l'aéronef, en radians.
Comme son nom l'indique, l'attribut trackOrHeading
contient soit la route de l'aéronef, soit son cap. Dans les deux cas, cette valeur est représentée par l'angle — positif et mesuré dans le sens des aiguilles d'une montre — entre le nord et la direction de déplacement de l'aéronef.
AirborneVelocityMessage
possède un constructeur compact qui lève :
NullPointerException
siicaoAddress
est nul, etIllegalArgumentException
sitimeStampNs
,speed
outrackOrHeading
sont strictement négatifs.
Comme les enregistrements représentant les autres types de message, celui-ci offre une unique méthode statique nommée of
permettant de construire un message de vitesse en vol à partir d'un message brut :
AirborneVelocityMessage of(RawMessage rawMessage)
, qui retourne le message de vitesse en vol correspondant au message brut donné, ounull
si le sous-type est invalide, ou si la vitesse ou la direction de déplacement ne peuvent pas être déterminés.
3.1.1. Conseils de programmation
Pour déterminer la direction du vecteur vitesse — l'angle qu'il fait avec le nord —, vous pouvez utiliser la méthode atan2
de Math
. Lisez bien sa documentation afin de comprendre comment elle fonctionne avant de l'utiliser !
Pour déterminer la norme de ce même vecteur, vous pouvez utiliser la méthode hypot
de Math.
3.2. Classe MessageParser
La classe MessageParser
du sous-paquetage adsb
, publique et non instanciable, a pour but de transformer les messages ADS-B bruts en messages d'un des trois types décrits précédemment — identification, position en vol, vitesse en vol. Elle n'offre qu'une seule méthode publique (et statique) :
Message parse(RawMessage rawMessage)
, qui retourne l'instance deAircraftIdentificationMessage
, deAirbornePositionMessage
ou deAirborneVelocityMessage
correspondant au message brut donné, ounull
si le code de type de ce dernier ne correspond à aucun de ces trois types de messages, ou si il est invalide.
Un message brut est invalide si la méthode of
de la classe correspondant à son code de type retourne null
.
3.3. Classe AircraftStateAccumulator
La classe AircraftStateAccumulator
du sous-paquetage adsb
, publique, représente un « accumulateur d'état d'aéronef », c.-à-d. un objet accumulant les messages ADS-B provenant d'un seul aéronef afin de déterminer son état au cours du temps.
Une instance de AircraftStateAccumulator
est associée à un objet représentant l'état modifiable d'un aéronef, de type AircraftStateSetter
, qui est passée à son constructeur. Le rôle principal de l'accumulateur est d'appeler les méthodes de modification de cet état afin de le maintenir à jour au fur et à mesure de l'arrivée des messages envoyés par l'aéronef. Par exemple, lorsqu'un message d'identification est envoyé par l'aéronef et transmis à l'accumulateur, il appelle les méthodes setCategory
et setCallSign
de l'état afin de lui communiquer ces informations, extraites du message.
AircraftStateAccumulator
mémorise de plus toujours le dernier message pair et le dernier message impair reçu de l'aéronef, dans le but de pouvoir déterminer sa position.
AircraftStateAccumulator
est générique, et son paramètre de type, nommé T
ci-dessous, est borné par AircraftStateSetter
. Elle possède un unique constructeur public :
AircraftStateAccumulator(T stateSetter)
, qui retourne un accumulateur d'état d'aéronef associé à l'état modifiable donné, ou lèveNullPointerException
si celui-ci est nul.
En plus de ce constructeur, AircraftStateAccumulator
offre les deux méthodes suivantes :
T stateSetter()
, qui retourne l'état modifiable de l'aéronef passé à son constructeur,void update(Message message)
, qui met à jour l'état modifiable en fonction du message donné, en appelant, pour tous les types de message, sa méthodesetLastMessageTimeStampNs
, ainsi que :- s'il s'agit d'un message d'identification et de catégorie,
setCategory
etsetCallSign
, - s'il s'agit d'un message de positionnement en vol,
setAltitude
et, si la position peut être déterminée,setPosition
, - s'il s'agit d'un message de vitesse en vol,
setVelocity
etsetTrackOrHeading
.
- s'il s'agit d'un message d'identification et de catégorie,
Pour que la position puisse être déterminée, il faut que la différence entre l'horodatage du message passé à update
et celui du dernier message de parité opposée reçu de l'aéronef soit inférieur ou égal à 10 secondes.
3.3.1. Conseils de programmation
La méthode update
doit déterminer, d'une manière ou d'une autre, le type exact du message qu'on lui passe en argument pour savoir s'il s'agit d'une instance de AircraftIdentificationMessage
, de AirbornePositionMessage
ou de AirborneVelocityMessage
. Plusieurs techniques existent pour faire cela.
La première consiste à utiliser une séquence de tests au moyen de l'opérateur instanceof
. Cette solution n'est toutefois ni très propre ni très efficace.
La seconde consiste à utiliser le patron de conception Visiteur (Visitor), que la plupart d'entre vous a examiné au premier semestre, mais cela est relativement lourd et implique l'existence d'une infrastructure qui n'a pas été mise en place pour les classes implémentant Message
.
La troisième consiste à utiliser le filtrage de motif (pattern matching), un concept déjà décrit partiellement à l'étape 1. Il s'agit de la solution la plus propre et la plus efficace, et nous vous conseillons donc fortement son emploi. Elle consiste à utiliser une variante de l'énoncé switch
qui permet de déterminer de quel classe une valeur est une instance.
L'extrait de programme ci-dessous illustre cette variante de l'énoncé switch
au moyen d'une méthode qui prend en argument une valeur de type Message
, détermine s'il s'agit d'une instance de AircraftIdentificationMessage
, et dans ce cas, affiche l'indicatif qu'il contient à l'écran ; sinon, un texte par défaut est affiché.
void printCallSign(Message m) { switch (m) { case AircraftIdentificationMessage aim -> System.out.println("Indicatif : " + aim.callSign()); default -> System.out.println("Autre type de message."); }
Notez bien que la ligne
case AircraftIdentificationMessage aim ->
teste d'une part si m
est une instance de AircraftIdentificationMessage
, et, si c'est le cas, définit la variable nommée aim
qui contient ce message et a le type AircraftIdentificationMessage
. C'est ce qui rend valide l'appel à la méthode callSign
de aim
à la ligne suivante.
Pour pouvoir utiliser cette variante du switch
dans votre projet, il vous faut changer légèrement sa configuration. Pour cela, ouvrez le menu File, puis sélectionnez Project Structure…. Sélectionnez Project sur la gauche, puis cliquez sur le menu déroulant à droite de Language level et choisissez 17 (Preview) - Pattern matching for switch.
3.4. Tests
Comme d'habitude, nous ne vous fournissons plus de tests mais un fichier de vérification de signatures à importer dans votre projet.
Pour tester les classes de cette étape, vous pouvez commencer par écrire une classe implémentant l'interface AircraftStateSetter
et qui affiche simplement les informations reçues à l'écran. Un squelette d'une telle classe pourrait ressembler à ceci :
class AircraftState implements AircraftStateSetter { @Override public void setCallSign(CallSign callSign) { System.out.println("indicatif : " + callSign); } @Override public void setPosition(GeoPos position) { System.out.println("position : " + position); } // … autres méthodes }
Cela fait, vous pouvez exécuter une version améliorée du programme de test donné à la fin de l'étape 4, qui démodule les 384 messages du fichier d'échantillons fourni et utilise un accumulateur pour suivre l'état de l'aéronef dont l'adresse OACI est 4D2228 :
public static void main(String[] args) throws IOException { String f = "samples_20230304_1442.bin"; IcaoAddress expectedAddress = new IcaoAddress("4D2228"); try (InputStream s = new FileInputStream(f)) { AdsbDemodulator d = new AdsbDemodulator(s); RawMessage m; AircraftStateAccumulator<AircraftState> a = new AircraftStateAccumulator<>(new AircraftState()); while ((m = d.nextMessage()) != null) { if (!m.icaoAddress().equals(expectedAddress)) continue; Message pm = MessageParser.parse(m); if (pm != null) a.update(pm); } } }
qui devrait afficher les mises à jour de l'état suivantes à l'écran :
position : (5.620176717638969°, 45.71530147455633°) position : (5.621292097494006°, 45.715926848351955°) indicatif : CallSign[string=RYR7JD] position : (5.62225341796875°, 45.71644593961537°) position : (5.623420681804419°, 45.71704415604472°) position : (5.624397089704871°, 45.71759032085538°) position : (5.625617997720838°, 45.71820789948106°) position : (5.626741759479046°, 45.718826316297054°) position : (5.627952609211206°, 45.71946484968066°) position : (5.629119873046875°, 45.72007002308965°) position : (5.630081193521619°, 45.7205820735544°) position : (5.631163045763969°, 45.72120669297874°) indicatif : CallSign[string=RYR7JD] position : (5.633909627795219°, 45.722671514377°) position : (5.634819064289331°, 45.72314249351621°)
4. Résumé
Pour cette étape, vous devez :
- écrire les classes
AirborneVelocityMessage
,MessageParser
etAircraftStateAccumulator
selon les indications données ci-dessus, - tester votre code,
- documenter la totalité des entités publiques que vous avez définies,
- rendre votre code au plus tard le 31 mars 2023 à 18h00, au moyen du programme
Submit.java
fourni et des jetons disponibles sur votre page privée.
Ce rendu est un rendu testé, auquel 18 points sont attribués, au prorata des tests unitaires passés avec succès.
N'attendez surtout pas le dernier moment pour effectuer votre rendu, car vous n'êtes pas à l'abri d'imprévus. Souvenez-vous qu'aucun retard, aussi insignifiant soit-il, ne sera toléré !