Going Blue is a weather app designed specifically for satellite messengers. It was built for a Denali ski expedition with one goal: to get you all the weather information you would have at home, wherever you are. Going Blue uses a custom compression codec and decoder app to pack hundreds of forecast data points into a single message that can be sent over SMS, Garmin inReach, or iPhone satellite messaging. Going Blue is deployed at going.blue and is available on the App Store.
- Build a forecast request in the app. Choose the location, model, and variables that you care about.
- Send the forecast request to Going Blue via the internet, SMS, Garmin inReach, or iPhone satellite messaging.
- Receive an encoded message from Going Blue. Paste it into the app to see a detailed forecast.
- Works via the internet, SMS, Garmin inReach, and iPhone satellite messaging.
- Uses a custom compression codec optimized for weather data that packs hundreds of data points into a single message. Choose between hourly detail and extended range up to 13 days.
- Temperature, snow, rain, wind, and cloud cover included by default. Optional variables include pressure-level winds for high-altitude mountaineering, AQI for planning around wildfire smoke, low/mid/high cloud cover, and freezing level.
- Weather forecasts from over 30 models including HRRR (3km), HRDPS (2.5km), ICON-D2 (2km), and MET Norway (1km). Automatically chooses the best model for your location.
- Compare forecasts from American, Canadian, and European forecast centers.
- All forecasts are saved on your device for comparing multiple models and past forecasts.
There are a few components to the system:
- Forecast source: Open-Meteo which provides all forecast data.
- Going Blue service: handles incoming SMS, fetches forecasts from Open-Meteo, and replies with encoded forecasts.
- Going Blue app: mobile app for creating forecast requests and decoding/visualizing forecast responses.
The service is written in TypeScript. There are four packages:
packages/protocol— shared TypeScript binary encoding/decoding used by both the server and the mobile app.packages/server— Hono/Node.js server; receives inbound messages, fetches forecasts, and sends replies.packages/codec-server— Codec server for encoding messages. This is separate from the main server so that old codecs can be kept deployed as-is while the codec is modified.packages/mobile— Expo React Native app for building requests and decoding forecasts.
Going Blue uses a Markov model of weather combined with a rANS entropy coder.
To see how this works, let's step through an example of encoding the weathercode, which is a general summary of weather conditions in a single symbol. There are 30 different weathercodes, so encoding weathercode without compression would take 5 bits. We can take advantage of the fact that the current weather is a good predictor of future weather. For example, if it is currently sunny, this is the probability distribution of the next hour's weather:
| Next hour | Probability |
|---|---|
| ☀️ clear sky | 85.40% |
| 🌤️ mainly clear | 8.68% |
| ⛅ partly cloudy | 2.67% |
| ☁️ overcast | 2.58% |
| 🌦️ light drizzle | 0.458% |
| … everything else | 0.192% combined |
We can then represent a forecast as a series of state transitions with different probabilities i.e. a Markov chain: ☀️ -> ☀️ -> ⛅ -> ⛅ -> 🌦️.
We can then feed this probability distribution into an entropy coder like a Huffman coder. In Huffman coding, each symbol is assigned a code based on its probability. The more likely a symbol is, the shorter its code:
| conditions | P | bits | code |
|---|---|---|---|
| ☀️ clear sky | 85.40% | 1 | 0 |
| 🌤️ mainly clear | 8.68% | 2 | 10 |
| ⛅ partly cloudy | 2.67% | 3 | 110 |
| ☁️ overcast | 2.58% | 4 | 1110 |
| 🌦️ light drizzle | 0.458% | 5 | 11110 |
| … everything else | 0.192% combined | 6+ | 111110… |
In this example, the clear -> clear transition is very likely so it gets a 1-bit code: 0. The clear -> light drizzle transition is unlikely, so it gets a 5-bit code: 11110. The expected length of the encoded forecast is only 1.248 bits/symbol, far below the 5 bits/symbol that would be required to encode any of the 30 different weathercodes. The actual encoded length may vary depending on the forecast. If it's completely clear for the entire forecast period, we will just use 1 bit per period. In more variable conditions, we will need more bits for each forecast period.
The actual entropy coder that Going Blue uses is rANS which removes the 1-bit floor of Huffman coding by encoding the entire forecast into a single large number instead of going symbol by symbol. See this post for a great explanation of asymmetric numeral systems. With the Huffman coder, we can reach 1.248 bits/symbol. rANS brings us much closer to the actual entropy of the data, which is 0.833 bits/symbol.
The same technique can be applied to other weather variables. Correlation between variables can also be used. For example, weathercodes are split into classes that give a general weather bucket: rainy, snowy, dry, etc. Weathercode is always included so this data can be used to condition other variables for free. Snow, rain, and precipitation probability are keyed off of weathercode class.
For variables with large ranges, like temperature, we encode the starting temperature and then the delta of each forecast point. This avoids having a separate codebook for every possible temperature. It also allows the codec to more easily capture trends. For example, if the temperature rose 2°C in the last hour, it is likely still rising in the next hour. The delta provides more information about the next hour's temperature than the absolute temperature does.
For variables like snow and rain which are sparse but have large variability, we use a sqrt scale. This provides detail at small amounts while preserving range for larger values. With rain, we might have an hour with 0.1mm rain and a 12 hour period with 100mm of rain. A sqrt scale allows us to represent both extremes on a scale with only 64 values. Rain values range from 0.036mm to 144mm and snow from 0.05cm to 200cm in a single time period.
| Code | 0 | 1 | 2 | 3 | … | 16 | 32 | 48 | … | 62 | 63 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Rain (mm) | 0 | 0.036 | 0.145 | 0.327 | … | 9.29 | 37.15 | 83.59 | … | 139.47 | 144.00 |
| Step | — | 0.036 | 0.109 | 0.181 | … | 1.13 | 2.29 | 3.45 | … | 4.46 | 4.54 |
Going Blue uses an entropy coder which means that the forecast length is not predictable. It depends on the entropy of the forecast, with stable (low-entropy) conditions taking few bits to encode and variable (high-entropy) conditions taking many bits to encode. In practice, forecasts with the default variable set range between 40 and 225 time periods, with an average near 100.
We can't promise a 3 day hourly forecast or a 10 day forecast at 3h resolution. At the minimum of 40 data points, we can choose between almost 2 days of hourly data or a 10 day forecast at 6h resolution. The app allows the user to select a fill priority: detail, auto, or range. The server fetches the forecast and then tries to fit as much data into the message as possible. At each step, it can either extend the range of the forecast (up to 13 days) or increase the detail (12h/6h/3h/1h resolution). The fill priority determines which it tries to do. These fill ladders are pre-defined and shared between the server and client. The server sends back a sequence number so that the client can derive the resolution of each forecast point. The server does a binary search of the sequence number to find the largest forecast that can fit within the character budget.
To keep the message small, the server never sends the client information that it already has. When the client creates a request, it stores request metadata like forecast location, model, variables, priority mode, UTC offset, and request time in a local cache. The client sends a request index to the server and the server sends that index back in the response. The client can then recover all of the forecast metadata from that index. Using this, the entire header can be packed into just 5 characters:
| Field | Bits | Meaning |
|---|---|---|
| version | 7 | base-85 index = protocol version; read before anything else |
index |
7 | message index the client stores its request context under |
seq |
8 | fill sequence number to derive forecast length and layout |
elev |
7 | elevation in 100 m steps |
class |
3 | which of the 8 codebook classes the body used |
Each variable has a different quantization method and codebook strategy. The variables that are always included are:
- Weathercode
- Temperature
- Snow
- Rain
- Wind gust
- Wind speed
- Wind direction
| Variable | Model | States (the symbol alphabet) | Codebook keyed by | Quantization |
|---|---|---|---|---|
| weathercode | value | 30 WMO codes | previous code | — |
| temperature | delta | Δ°C −7…+7, plus an escape symbol + raw 6-bit (−32…+31) | resolution × time-of-day (8 × 3h local buckets) × previous-delta bucket (≤−2 | −1 | 0 | +1 | ≥+2) | 1 °C steps, −100…+155 °C; 8-bit anchor |
| precipitation prob. | value | eighths 0…7 | resolution × previous value × same-period weathercode class | 0–100% in eighths |
| snow | value | 64 companded steps | resolution × previous-value bucket (0 | 1–3 | 4–9 | 10–20 | 21+) × same-period weathercode class | sqrt-companded, 0–200 cm |
| rain | value | 64 companded steps | resolution × previous-value bucket (same) × same-period weathercode class | sqrt-companded, 0–144 mm |
| freezing level | delta | Δ −31…+31 | resolution × same-period temperature Δ bucket (≤−2 | −1 | 0 | +1 | ≥+2); resolution alone when temp is absent | 1000 ft steps, 0–31000 ft; 5-bit anchor |
| cloud (high/mid/low) | delta | Δ −7…+7 | one shared table per level | 0–100% in eighths; 3-bit anchor |
| wind gust | delta | Δ −17…+17 | resolution (encodes first, no context of its own) | extended Beaufort force 0…17, km/h bands; 5-bit anchor |
| wind speed | delta | Δ −17…+17 | resolution × level; surface by the gust column's Δ bucket; 600/700 hPa by the upper level's Δ bucket | extended Beaufort force 0…17, km/h bands (midpoint decode); 5-bit anchor |
| wind direction | value | 8 cardinals | resolution × previous direction (× upper direction for 600/700 hPa); calm periods emit no symbol | 45° points |
SMS is the transport layer for Going Blue. Satellite messengers like Garmin inReach, iPhone, and ZOLEO can all send and receive messages via SMS. To reach Going Blue, a message travels through several intermediaries. For example, the path an inReach message takes looks something like this:
inReach -Iridium Short Burst Data (SBD)-> Garmin -SMS-> Twilio -HTTP-> Going Blue server
Each part of the chain has a different character set and message length limit. In the case of Garmin, these are:
- Iridium SBD: 270-340 bytes
- SMS: 160 GSM-7 basic septets
- Garmin: 160 characters of printable ASCII
The alphabet that Going Blue can use is the intersection of all of these: 160 characters of GSM-7 basic ∩ printable ASCII (minus space), or base-85. This provides log₂(85) ≈ 6.409 bits per character and approximately 1025 bits per message.
Each device has a different character set and message length. Going Blue chooses how to encode a message based on the device:
| Device | Network | Alphabet1 | Message length | Bits per message |
|---|---|---|---|---|
| SMS | Cellular | base-124 (GSM-7 basic) | 160 chars | ~1110 |
| Garmin inReach | Iridium Short Burst Data | base-85 (GSM-7 basic ∩ printable ASCII) | 160 chars | ~1025 |
| ZOLEO | Iridium Short Burst Data | base-85 (GSM-7 basic ∩ printable ASCII) | 240 chars | ~1538 |
| iPhone satellite messaging | Globalstar | base-32768 | 50 chars2 | ~707 |
| Internet | Internet | base-94 (printable ASCII) | — | — |
Entropy coding requires having accurate statistics about the distribution of each symbol since sequences that aren't represented in the training data will be very expensive to encode. For example, if we trained the codebooks only on tropical weather forecasts, the encoder would assign very long symbols to snow and a forecast in the arctic would be very expensive.
The encoder is trained on over 100k historical forecasts collected from the Open-Meteo Historical Forecast API. These forecasts are sampled from 8,500 locations across the world. Forecast locations are not uniformly sampled across the globe since that would bias the forecasts strongly towards the ocean. Instead, the forecast points are allocated based on 30 Köppen climate classes based on the square-root of the area of the climate class. This ensures that rare climate classes have enough training data while still allocating more share to more common climate types.
Ocean locations are not included in Köppen but they are included in the training data with an 85/15 land/ocean split. This gives the ocean a similar weight to a high-level Köppen climate class (tropical, arid, temperate, continental, polar). Ocean locations are sampled from 6 30° latitude bands with the same sqrt(area) allocation as climate classes.
Training data is pulled from the two year window July 2024 - July 2026. 12 14-day forecasts are collected for each location for an average of 1 forecast every 2 months. This ensures coverage of all seasons while also reducing forecast duplication.
1,500 forecast locations are held out for evaluation and never used to train the encoder. 137 of my Windy favorites are also used as an evaluation set since these are the places I actually want weather forecasts for. There is also a small set of 150 peaks used in evaluation to make sure that Going Blue works well in the mountains. There is a custom page for exploring the evaluation results at going.blue/benchmark.
Fill percentage is the main codec performance metric. 100% represents a forecast filled to maximum range and resolution (13 days, hourly data). Encoding improvements should increase this percentage.
Some interesting findings from the evaluation:
- The median forecast with auto priority has a 13 day range with 2 days at hourly resolution, 5 days at 3h, 3 days at 6h, and the last 3 days at 12h. The 1st-percentile forecast still has 10 days of data with 1 day hourly, 4 days at 3h, and 6 days of 12h.
- Forecasts in polar climates (Köppen class E and ocean at 60°-90°N) are the cheapest to encode. Probably because of the polar high and lack of diurnal temperature swings.
- Forecasts in tropical climates (Köppen class A) are the most expensive to encode. Probably because of frequent afternoon precipitation, strong diurnal temperature swings, etc. There's a lot more weather happening in the tropics than there is in the arctic.
- Ocean forecasts are cheaper than every climate class except the arctic. There are no diurnal temperature swings over open water and winds are more consistent than they are on land.
- Wind is the most expensive variable (steady, gust, direction combined) taking an average of 40.1% of the message. Temperature is the second most expensive at 24.6% followed by weathercode at 19%. Since snow and rain are sparse, they only take up an average of 10.8% combined.
- A 1st-percentile forecast containing all optional variables (detailed clouds, high altitude winds, freezing level, and precip chance) still delivers 7 days of forecast data at 6h resolution for 3 days and 12h resolution for the next 4 days.
- Rain -> snow remapping based on temperature. Temperature is already adjusted for elevation by Open-Meteo. The rain -> snow remapping makes precip type consistent with temperature.
- Mixed rain/snow weathercodes during mixed precip periods.
- Pressure-level cloud extrapolation from low/mid/high clouds based on relative humidity.
- Weathercode summarization. Weathercodes are aggregated from hourly data for longer periods. Showery codes are used to indicate mixed conditions.
Open-Meteo accepts an elevation parameter for forecasts and adjusts temperature from the model's grid cell elevation using temperature lapse rate. It does not adjust other variables like precipitation type. This can lead to contradictory forecasts in the mountains. For example, a forecast for the summit of Denali may show very low temperatures and rain if it is raining at the grid cell elevation (~3000m for GFS).
To fix this, rain is remapped to snow if the forecast elevation is above the freezing level. It is also remapped to snow if the temperature is below -2°C to handle inversions and forecast centers that do not support freezing level (GEM, ECMWF). Snow is never remapped to rain. Rain is translated to snow at a 7:1 SWE ratio for parity with Open-Meteo. More accurate snow:liquid mapping may be added in the future.
The following weathercodes are remapped:
- 51/53/55 (drizzle) → 71/73/75 (snow)
- 61/63/65 (rain) → 71/73/75 (snow)
- 80/81/82 (rain showers) → 85/85/86 (snow showers)
Freezing drizzle (56/57) and freezing rain (66/67) are not transformed.
Requirements:
- Docker
- tmux
Everything needed to run locally is bundled into one tmux session:
pnpm install
./dev.sh
./dev.sh kill
The services run on the following ports by default:
- Postgres: 5432
- Gateway server: 8080
- Codec server: 8082
- Expo (mobile app server): 8081
To run the iOS app:
cd packages/mobile
eas build -p ios --profile development
Then install the app on your iOS device using the QR code in the step above.
The app can also be run in the simulator:
cd packages/mobile
npx expo run:ios
For a non-development build:
eas build --platform ios --profile preview
pnpm testThe Going Blue codec relies on the client and server having identical codebooks. Since clients may be out of service and unable to update for long periods of time, the service maintains support for old versions. Each forecast request starts with a version number e.g. v1. The gateway server routes each message to the appropriate codec service. Each version of the codec is a separate container running from main tagged at a specific version. Golden messages are kept for each codec version so that changes to the codec service can be made (for example patching security vulnerabilities) while ensuring that the message format does not change. This approach allows the codec to evolve quickly without sacrificing support for older clients in the field.
- Added air quality variables: AQI, PM2.5, PM10, ozone, nitrogen dioxide, sulfur dioxide. Supports both American and European scales.
- Added support for iPhone satellite messaging with multi-part messages.
- Corrected precipitation type for elevation. Open-Meteo already adjusts temperature from grid cell elevation to forecast elevation using a temperature lapse rate formula. This change also remaps rain to snow when the forecast elevation is above the freezing level or the temperature is less than -2°C. Uses a 7:1 snow:liquid ratio. Weathercode is also remapped.
- Improved weathercode aggregation to better summarize mixed conditions.
- Added model attribution to the meteogram so that the switch between a high-resolution local model and a low-resolution global model is clear.
- Expanded SMS alphabet from 85 to 124 characters by using almost all of GSM-7 instead of the intersection of GSM-7 and ASCII.
- Added ZOLEO support with 240 character messages.
- Expanded detailed cloud cover with several more cloud levels and better cloud rendering.
- Added a legend to the meteogram.
- Split out rain, snow, and precip chance in the meteogram to improve legibility.
- Report mixed rain/snow weathercode when there is a substantial amount of each precip type.
- Optimized meteogram rendering.
Copyright 2025-2026 Lane Aasen
Licensed under the Apache License, Version 2.0. You may use, modify, and distribute this software, including commercially, provided you retain the copyright and license notices and state any significant changes you make. The license includes an express patent grant. See NOTICE for the required attribution notice.
Forecast data comes from Open-Meteo via its public API and is subject to Open-Meteo's own terms; no Open-Meteo source code is included here.
