Tous les textes

~/textes/wsrepeater-sensor-calibration

Systèmes embarqués
5 min de lecture

Ma station météo ment au sujet du soleil

Une station Ecowitt bon marché rapporte des valeurs UV et de rayonnement solaire qui lisent trop haut et qui sautillent. Le répéteur qui les transmet à Weather Underground lisse et met ces canaux à l'échelle d'abord, et ne laisse jamais une API lente en amont bloquer la station.

J'opère une station météo Ecowitt WS2320. Elle POST ses lectures à une cadence fixe, et un petit service Go que j'ai écrit se place au milieu : il traduit le format d'Ecowitt vers celui de Weather Underground et le transmet, et il sert un tableau de bord local pour que je ne dépende pas du site de quelqu'un d'autre pour voir ma propre cour. Deux choses devaient être réglées avant que les données valent la peine d'être publiées.

Le soleil n'est pas si brillant

La température, l'humidité, la pression et le vent de cette station sont corrects. Les deux canaux qui ne le sont pas, c'est l'indice UV et le rayonnement solaire. La photodiode bon marché lit trop haut, et elle fluctue : d'une minute à l'autre elle varie plus que le ciel réel. Transmets les chiffres bruts et tu dis à Weather Underground qu'il fait une journée plus claire qu'en réalité, avec du bruit par-dessus.

Deux corrections, dans l'ordre. D'abord une moyenne mobile sur les derniers échantillons pour enlever le sautillement, ensuite un facteur d'échelle constant pour ramener l'amplitude à ce que le ciel faisait vraiment.

repeater.go
const movingAverageWindow = 5
 
smoothedUV := utils.SmoothValue(uvValue, &uvValues, &uvMutex)
smoothedSolarRadiation := utils.SmoothValue(solarRadiationValue, &solarValues, &solarMutex)
 
// The sensor reads high. 0.94 was tuned until the numbers matched reality.
correctedUV := math.Round(smoothedUV * 0.94)
correctedSolarRadiation := smoothedSolarRadiation * 0.94

SmoothValue est la moitié plate : garder les cinq dernières lectures par canal, retourner leur moyenne, jeter la plus vieille à mesure que les nouvelles arrivent. Le 0.94 est la partie qui n'existe que parce que j'ai comparé la station à la réalité et qu'elle surlisait d'environ six pour cent sur toute la plage. Ce n'est pas une courbe de calibration, c'est une seule constante empirique, et c'est la description exacte de la chose : un nombre que j'ai mesuré pour le mettre en place, pas un que j'ai dérivé. L'UV est arrondi parce que Weather Underground veut un indice entier ; le rayonnement solaire reste fractionnaire.

L'ordre compte. Lisser d'abord, mettre à l'échelle ensuite. Mets à l'échelle une valeur bruitée et tu as mis le bruit à l'échelle aussi ; lisser en premier veut dire que la constante corrige un nombre stable.

Ne fais pas attendre la station

Le second problème, ce n'est pas les données, c'est la plomberie. La station POST selon son propre horaire et s'attend à une réponse rapide. Si je transmets à Weather Underground de façon synchrone à l'intérieur de ce gestionnaire de requête, alors le POST de la station n'est aussi rapide que l'API de Weather Underground dans sa pire journée. Un amont lent, ou un blocage momentané, et la station reste là à attendre, bloquée sur quelque chose qui n'a rien à voir avec elle.

Donc le gestionnaire fait seulement le travail local et bon marché : analyser, lisser, corriger, traduire. Ensuite il remet la charge utile finie à une file tamponnée et retourne. Des workers vident la file et s'occupent du POST distant lent à leur propre rythme.

repeater.go
const workerCount = 5
var jobQueue = make(chan url.Values, 100)
 
// ...inside the request handler, after correcting the values:
jobQueue <- wundergroundData   // returns immediately; never blocks on WU
 
// started once at boot:
for i := 0; i < workerCount; i++ {
    go worker()
}
 
func worker() {
    for job := range jobQueue {
        // the slow part: POST to Weather Underground, retry, log
    }
}

Le tampon a une profondeur de cent, ce qui est bien plus que ce que la station mettra jamais en file à sa cadence de publication, donc un hoquet de Weather Underground est absorbé au lieu de remonter dans la station. La station obtient toujours son 200 rapide ; le chemin lent vit derrière le canal.

Fais le travail bon marché sur le chemin chaud, diffère le lent

Un gestionnaire de requête qui appelle une API tierce en ligne hérite de la latence de cette API et de ses mauvaises journées. Sépare le travail : ce qui est local et rapide (l'analyse, le lissage, la mise à l'échelle) se passe sur la requête ; ce qui est distant et lent va sur une file avec des workers derrière. L'appelant qui t'intéresse vraiment, ici une station météo qui veut juste être acquittée, ne paie jamais pour la panne de quelqu'un d'autre.

Le tableau de bord emprunte, il ne martèle pas

Le tableau de bord local montre plus que ce que la station connaît : phase de la lune, lever et coucher du soleil, un fil RSS, l'historique récent ramené de Weather Underground. Chacun de ceux-là est l'API de quelqu'un d'autre avec ses propres limites de débit, donc chacun est mis en cache sur un TTL qui correspond à la vitesse à laquelle il change vraiment. La phase de la lune se rafraîchit à l'heure, le fil RSS aux quinze minutes, le lever et le coucher du soleil une fois puis retenus jusqu'à minuit. Rien ne refait de requête au chargement d'une page. Le tableau de bord lit dans le cache et le cache se remplit à son propre rythme.

Rien de tout ça n'est exotique. C'est un traducteur avec deux corrections mesurées en avant et une file qui tient une API lente hors du chemin critique. Mais le résultat, c'est une station dont les chiffres publiés correspondent au ciel, et un hub qui reste réactif peu importe ce que font les services sur lesquels il s'appuie.

Le code est sur GitHub.