~/textes/jankgps-ts2phc-gpsd-demux
ts2phc et gpsd ne peuvent pas partager un port série
Un seul port série USB, deux programmes qui veulent tous les deux un accès exclusif. ts2phc veut uniquement du RMC pour discipliner l'horloge PTP; gpsd veut tout le reste. J'ai écrit un démultiplexeur qui donne à chacun un port synthétique façonné exactement comme il l'attend.
Un u-blox NEO-M9N se présente sous Linux comme un seul périphérique série USB, /dev/ttyACM0. Ce port unique, c'est tout le problème. Deux programmes le veulent en entier, en exclusivité, au même moment.
ts2phc est l'outil linuxptp qui discipline l'horloge matérielle PTP d'une carte réseau à partir d'un GPS. Il lit le NMEA sur le port série pour connaître l'heure de la seconde, puis horodate l'impulsion PPS pour l'instant exact. gpsd, je le veux pour tout le reste: nombre de satellites, qualité du fix, dilution de la précision, l'alimentation d'un refclock SHM de chrony et d'un exportateur Prometheus. Les deux ouvrent le port en O_EXCL. Lance l'un ou l'autre. Pas les deux.
Il y a une deuxième contrainte, plus tranchante, qui tue les contournements évidents. ts2phc veut que le port série transporte des phrases RMC et rien d'autre. Pointe-le vers le lot complet de NMEA que gpsd aime (GGA, GSV, VTG, ZDA) et il devient grognon. Un simple tee du port brut ne règle rien; ça donnerait à ts2phc un flux qu'il ne veut pas.
[global]
ts2phc.nmea_serialport /dev/ttyACM0
ts2phc.nmea_baudrate 115200
# ts2phc wants RMC and nothing else on this port.
[enp2s0]
ts2phc.master 0
ts2phc.pin_index 0
ts2phc.extts_polarity risingArrête de partager le port, mets-toi à le posséder
Un seul processus possède /dev/ttyACM0 en entier et remet à chaque consommateur un port synthétique façonné comme il l'attend. ts2phc reçoit un port série virtuel avec uniquement du RMC. gpsd reçoit le lot complet de NMEA. Ni l'un ni l'autre ne touche au vrai périphérique, alors la bataille d'exclusivité disparaît. Il y a un seul ouvreur, et c'est le mien.
Le u-blox parle deux protocoles sur ce port unique: NMEA (ASCII délimité par des virgules) et UBX, le protocole binaire avec des données plus riches et une trame plus serrée. Ils sont entrelacés dans le même flux d'octets. Les démultiplexer est plus simple qu'il n'y paraît; les deux cadrages commencent par des octets distinguables. Les trames UBX commencent par la paire de synchronisation 0xB5 0x62; les phrases NMEA commencent par $. Le lecteur est une petite machine à états qui parcourt le flux d'octets:
b := d.buf[d.pos]
switch {
case b == ubx.SyncA: // 0xB5, start of a UBX frame
if err := d.readUBX(); err != nil {
return err
}
case b == '$': // start of an NMEA sentence
if err := d.readNMEA(); err != nil {
return err
}
default:
d.pos++ // junk byte between frames, skip it
}Les trames UBX vont vers un gestionnaire qui décode NAV-PVT (position, vitesse, heure dans un seul message) et le reste des messages de navigation. Le NMEA passe au travers. À partir de là, le démon distribue les données aux deux consommateurs, chacun dans sa forme préférée.
Un PTY qui ne dit jamais que du RMC
Pour ts2phc, le port synthétique est un PTY. Ouvre une paire, fais un lien symbolique du côté esclave vers un chemin stable, et pointe la config de ts2phc vers ce chemin au lieu du vrai périphérique. ts2phc ouvre le lien symbolique, obtient l'esclave, et le lit comme un port série ordinaire. Le démon écrit dans le maître.
master, slave, err := pty.Open()
// ...
os.Remove(linkPath) // clear any stale link
os.Symlink(slave.Name(), linkPath) // /run/jankgps/ts2phc -> /dev/pts/NLe changement de config tient en une ligne: le port série est maintenant le lien symbolique, pas le périphérique.
ts2phc.nmea_serialport /run/jankgps/ts2phcLe démon écrit exactement un seul type de phrase dans ce PTY. NAV-PVT arrive, il synthétise un RMC et l'écrit dans le maître. Du vrai NMEA arrive, il ne le transmet au PTY que si c'est du RMC. Tout le reste est jeté sur ce chemin. ts2phc voit un port propre, uniquement du RMC. C'est la diète qu'il a demandée.
// TCP (gpsd) gets everything; the PTY (ts2phc) gets only RMC.
if h.tcp != nil {
h.tcp.Broadcast(sentence)
}
if h.pty != nil && isRMC(sentence) {
_ = h.pty.Write(sentence)
}gpsd reçoit le jet complet par TCP
gpsd n'a pas besoin d'un périphérique série. Il peut lire le NMEA à partir d'une source TCP. Alors l'autre export est un simple écouteur TCP sur :2948 qui diffuse chaque phrase à chaque client connecté et qui largue les clients qui arrêtent de lire. gpsd se connecte comme source, et à partir de là la chaîne habituelle fonctionne sans changement: gpsd vers un refclock SHM de chrony, gpsd vers un exportateur Prometheus.
Synthétise, ne te contente pas de transmettre
Parce que le démon décode le NAV-PVT d'UBX, il peut construire les phrases NMEA lui-même au lieu de transmettre ce que le module émet. Le NMEA que le module envoie et le NMEA que chaque consommateur reçoit sont découplés. Je peux faire tourner le module dans une config majoritairement UBX pour des données plus riches et quand même remettre à ts2phc et à gpsd du NMEA bien formé dérivé des messages binaires. Le port série cesse d'être un tuyau fixe et devient quelque chose que je façonne par consommateur.
Métriques, dont ts2phc qui se note lui-même
Comme le démon décode déjà chaque message de navigation, exposer les métriques GPS coûte presque rien: satellites suivis, type de fix, dilution de la précision, tous les champs NAV-SAT et NAV-PVT, sur un point d'accès Prometheus aux côtés de node_exporter et de l'exportateur chrony.
Ce que j'aime, c'est que le démon surveille aussi ts2phc en train de se noter lui-même. ts2phc consigne l'état de son servo dans le journal: décalage d'horloge, correction de fréquence, délai NMEA. Plutôt que d'analyser les rouages internes de PTP, le démon suit journalctl -u ts2phc -f et extrait ces lignes en métriques:
offsetRegex = regexp.MustCompile(`([^ ]+) offset\s+(-?\d+)\s+s\d\s+freq\s+(-?\d+)`)
nmeaDelayRegex = regexp.MustCompile(`nmea delay: (\d+) ns`)Donc le même démon qui alimente ts2phc en heure rapporte aussi, en ns, à quel point ts2phc pense bien se débrouiller. Ce qui produit l'entrée et ce qui mesure la sortie sont le même processus. Toute la chaîne de timing est observable depuis une seule cible de scrape.
Le délai du câble d'antenne (le temps de propagation fixe de l'antenne le long du coax jusqu'au récepteur) est un décalage configurable sur le chemin PPS, à 38 ns par défaut. Tu le mesures une fois pour ton installation et tu l'oublies.
Ce que c'est et ce que ce n'est pas
Le README appelle ça "a pretty crappy ublox demuxer," et il y a du vrai là-dedans. La nouvelle interface de config u-blox (CFG-VALSET) est peu documentée et m'a demandé du tâtonnement. Mais la forme est bonne. Quand deux programmes exigent tous les deux la propriété exclusive d'un seul périphérique, et que l'un d'eux est difficile sur le format exact qu'on lui sert, tu ne choisis pas un gagnant et tu ne fais pas un tee des octets bruts. Tu possèdes le périphérique une fois et tu donnes à chaque consommateur une vue sur mesure: un PTY qui ne parle que RMC pour le démon de timing, un jet complet TCP pour gpsd, un point d'accès de métriques qui surveille toute la chaîne depuis l'extérieur.
Le code est sur GitHub.