New AsosAdjustmentWorker runs every 10 minutes:
- Fetches latest ASOS observations from all ~2900 US stations via
IEM bulk currents API (parallel fetch across 51 state networks)
- For each grid point within 75km of a reporting station, re-scores
using fresh ASOS data (temp, dewpoint, wind, sky, pressure, precip)
with HRRR refractivity gradient from the last hourly computation
- Pushes updated scores to the map via PubSub
Also stores HRRR profiles in the database during grid computation
so the data persists for reference and ASOS blending.
The JS hook sends map bounds on load and on pan/zoom. The server
queries only scores within those bounds, dramatically reducing the
payload for band switches and map updates. At zoom 7 (DFW area)
this sends ~2k scores instead of ~95k.
Add a separate wgrib2-builder stage that compiles wgrib2 from source.
Docker layer caching means this only builds once — subsequent deploys
reuse the cached layer. The binary is copied into the final runtime
image. Also fixed wgrib2 path lookup to be runtime instead of
compile-time so it works in the Docker build pipeline.
Replace circle markers with a canvas tile layer that renders smooth,
flowing colored regions using bilinear interpolation between grid
points. Colors interpolate between tiers for gradients. ~95k grid
points at 0.125 degree resolution with wgrib2 extraction.
Add Wgrib2 module that shells out to wgrib2 binary for fast GRIB2
grid extraction using -lola (nearest-neighbor to regular lat-lon grid).
Falls back to pure-Elixir decoder if wgrib2 is not installed.
Also: parallel GRIB2 range downloads, merge adjacent byte ranges,
skip corrupt messages instead of failing, pressure fetch is optional.
Both extract_points and extract_grid now skip messages with missing
data sections instead of halting. Pressure product fetch is now
optional for both grid and single-point modes — if pressure data
has corrupt messages, surface-only profiles are still stored. Fixes
historical HRRR backfill failing on old data with incomplete files.
95k points at 0.125 degree resolution caused the GRIB2 extraction to
take too long. 0.5 degree (~55 km) resolution gives 6k points which
completes in under a minute. Can increase resolution later once the
extraction is optimized.
The hrrr/weather/iemre/terrain backfill queues (20+ concurrent jobs)
were competing for bandwidth with the propagation grid HRRR download.
Now pauses those queues before the grid fetch and resumes them after,
ensuring the hourly propagation update completes reliably.
QSO enrichment now groups all path points by HRRR hour and creates
one batch job per hour instead of one job per point. The batch job
downloads the GRIB2 data once and extracts all needed points from
the same binary. Legacy single-point jobs are still supported for
backward compatibility.
Vendor Leaflet 1.9.4 (JS, CSS, marker images) and wire it into
the esbuild/Tailwind asset pipeline. Create MapLive with band
selector buttons, auto-refresh, and a colocated JS hook that
renders propagation scores as color-coded circle markers with a
legend. Stub Propagation context and BandConfig modules provide
the data interface for the scoring pipeline.
Add nav bar with links to Map, QSOs, and Submit pages.
Add fetch_grid/3 for batch HRRR data retrieval across multiple lat/lon
points in a single download pass. Expand surface messages to include
wind (UGRD/VGRD), cloud cover (TCDC), and precipitation (APCP). Add
extract_grid/2 to the GRIB2 extractor for multi-point extraction from
a single GRIB binary, and add GRIB2 variable identifiers for the new
surface fields.
Define 0.125-degree CONUS grid (25-50N, 125-66W) for propagation
scoring and create propagation_scores table with composite unique
index on lat/lon/valid_time/band_mhz for upsert support.
Add extract_values/3 to SimplePacking and ComplexPacking for batch
index extraction from a single GRIB2 message. Add extract_grid/2 to
Extractor which takes a list of {lat, lon} points and returns all
variable values for each point, skipping points outside the grid.
This enables extracting weather data for many grid points from a
single HRRR download instead of re-parsing per point.
Single source of truth for all scoring parameters: weights, thresholds,
seasonal tables, and per-band coefficients for 8 microwave bands
(10G through 241G). Includes ITU-R P.838-3 rain attenuation
coefficients, humidity effects, refractivity scoring thresholds,
and sunrise/tier definitions.
Enqueue worker now gathers atmospheric data at pos1, midpoint, and pos2
along each QSO path instead of only pos1. Existing has_* guards prevent
duplicate fetches at each grid point.
- Add Radio.qso_path_points/1 for path point extraction
- Update hrrr_job_for_qso, iemre_job_for_qso, jobs_for_qso to iterate path points
- Refactor Weather into find_nearest_hrrr/3 and find_nearest_iemre/3
- Add hrrr_profiles_for_path/1 and iemre_for_path/1 query functions
- Add mix reset_enrichment task to trigger re-processing
System.cmd :timeout option doesn't exist in Elixir 1.19, causing every
poll to crash silently. Also translate SNMPv2-SMI::enterprises prefix
to numeric OID so the lookup maps match correctly.
Weather job fan-out can exceed PostgreSQL's 65535 parameter limit
when many QSOs produce thousands of jobs in a single batch. Chunk
insert_all into batches of 1000 jobs.
- Add precip_1h_in and wx_codes fields to ASOS surface observations
- Add IEMRE reanalysis schema for radar-derived hourly precipitation
- Add IemreFetchWorker with exponential backoff and idempotency
- Integrate IEMRE enqueue into cron weather backfill pipeline
- All existing QSOs marked iemre_queued=false for automatic backfill
Poll UBNT AirFiber radios (AF11X + AF60-LR) every 5 minutes via
net-snmp CLI, storing signal metrics in commercial_samples. Fetches
ASOS weather alongside each cycle for propagation correlation.
Includes 7 seeded link definitions, Oban cron worker, and net-snmp
in the Docker image.
Previously each run only enqueued 500 QSOs (the query limit).
Now weather, HRRR, and terrain enqueue functions recurse until
no unprocessed QSOs remain.
extract_n_values_array used Enum.take(count) on a reversed list
before reversing it, which included padding values from byte
alignment and dropped actual values. When group count * bits
wasn't a multiple of 8, the extra padding bits produced a
phantom value that shifted the entire array by one position.
This caused cascading errors in spatial differencing — values
started correct but diverged exponentially (DPT decoded as
38 billion K instead of 275 K).
Fix: reverse the list first, then take count, so padding values
at the end are discarded instead of actual values at the start.
AWS S3 doesn't support multi-range HTTP requests. When given
Range: bytes=0-999, 2000-2999 it ignores the header and returns
the full file (200 instead of 206). Download each range separately.
Instead of listing every permanent failure string, only retry on known
transient errors (5xx, 429, network exceptions). Everything else —
GRIB decode errors, 404s, range request failures, index out of range —
is cancelled immediately.
S3 returns 200 with the full ~116MB file instead of 206 for some older
HRRR data. Treat this as a permanent failure instead of trying to parse
the entire file.
Instead of returning "malformed section" on trailing bytes or truncated
sections, attempt to return parsed results. This handles older HRRR
files (2019) that have padding bytes after the last section.
When a local .hgt tile is missing, download it from the public AWS S3
skadi bucket, decompress with zlib, and write to the tiles directory
before retrying the lookup. Falls back to Open-Meteo/OpenTopo APIs if
the download fails.
HTTP 404 means the HRRR data doesn't exist on NOAA S3 (pre-2014
dates, etc.) and will never succeed. Return {:cancel, reason}
instead of {:error, reason} so Oban stops retrying immediately.