Marshall Frith

Projects

Winds Aloft to 103,800 Feet, Without a GRIB Decoder

One wind service from the surface to a balloon burst, pure standard library, served straight into ATAK. The data gaps are output, not silence.

Windmaster answers one question: what is the wind doing at this point, at this altitude, at this time, and how much should I believe it. It covers the surface to roughly 103,800 feet anywhere on Earth, and it serves the answer to a stock ATAK client as a GeoChat reply, a KML barb column, CoT markers, or a one-tap mission package. Parachute drift, UAV wind triangles, balloon trajectories, and a wind curtain along an imported route all come from the same profile.

It is pure Python standard library. No pip install, no FastAPI, and no GRIB decoder, which is the part that needs explaining.

Measure the API, don't read its docs

Open-Meteo's documentation lists pressure levels topping out at 30 hPa. The live API, probed on 2026-08-06, serves 21 levels topping out at 10 hPa, which is about 31,600 metres. That one measurement is the whole reason this project could stay stdlib-only: 10 hPa covers a stratospheric balloon burst, so the entire GRIB dependency chain that NOMADS would have required was never needed.

The probe is a permanent subcommand, not a one-off, because vendors change things:

bash
python3 -m windmaster probe

Three more things the probe found that the docs never said:

  • Ask for a level above the ceiling and you get HTTP 200 with a well-formed array of nulls. No error. Code that trusts the status code returns an empty profile and calls it success.
  • Levels below the terrain still return plausible wind. At Denver the 1000 hPa level sits 1.6 kilometres underground and reports 12.8 km/h anyway. Those are extrapolations, not forecasts, and they get filtered against surface pressure.
  • The *_seamless model ids are pre-blended by the vendor. At Denver, gfs_seamless returns HRRR values verbatim. If provenance matters, request explicit models, because the blended id will happily lie about whose forecast you are holding.

Two models, one seam

A capability registry, every entry measured rather than transcribed, drives a two-model ladder: the finest convection-allowing regional model owns the boundary layer, a global backbone owns everything above, and the two are cross-faded over an 8,000 metre crossover with a 2,000 metre taper so no derived trajectory kinks at the seam.

The backbone is picked by rank, not by grid spacing. Sorting by kilometres hands the upper column to UKMO Global at 10 km over GFS at 25 km, which trades a 16-day horizon and the densest level ladder for a finer grid that resolves nothing at 60,000 feet. The same trap exists regionally: AROME France has the finest grid in the registry at 1.5 km, zero pressure levels, and a bounding box that reaches London and Frankfurt. Pick regional models by grid spacing alone and it silently displaces UKV and ICON-D2 across two countries it barely covers. There is a test whose only job is to stop that.

Wind is stored as u/v components and nothing else. Average 350 and 10 degrees as degrees and you get 180, which is exactly backwards. Speed and direction are derived on the way out, and direction enters through a single function.

The hole nobody advertises

Between the hub-height wind variables and the first above-ground pressure level there is a gap from roughly 262 to 1,266 feet AGL, which is most of the airspace a small UAV can legally use. Windmaster returns that as an explicit gap object with the two bracketing levels, instead of interpolating across it and pretending. Gaps are first-class output everywhere: the ceiling, spans wider than 3 km, and levels dropped as subsurface all say so in the response.

The sign bug that shipped

The wind correction angle is the negative arcsine of the crosswind component. The version with the sign inverted crabs the nose downwind, doubling drift instead of cancelling it, and it shipped in the UAV endpoint. It survived because ground speed is an even function of the angle, so every other number in the response stayed plausible. A hand-worked pure-crosswind case caught it; no amount of staring at outputs did.

When touching wind-triangle code, check the crab direction against "nose points into the wind", never against whether the ground speed looks right.

Validation against a real radiosonde at 83,000 feet came out at RMS 12.8 knots. Usable but drifting, and the tool says that rather than rounding it to confidence.

Run it yourself

bash
cd winds && python3 -m windmaster serve --host 127.0.0.1 --port 8788

No dependencies to install. The dashboard has a Leaflet map where a click drops a profile point, and a drag-rotate barb column where every barb points into the wind, the pilot convention, on purpose. ATAK users ping it over GeoChat and get products back over the wire; there is no plugin, because a native ATAK plugin needs a TAK.gov SDK account with manual approval and the wire needs nothing.

Sixty-three offline tests, no network. The free tier allows roughly 10,000 calls a day and a full profile costs four or five, so the disk cache under ~/.windmaster/cache is doing real work; offline it serves stale data with a warning instead of nothing.

The thing I would tell you before you start: probe every API you depend on, and keep the probe. The gap between what the docs said and what the endpoint did was the difference between a stdlib weekend and a GRIB toolchain.

Comments

Plain text only. Held for review before it appears.