diff --git a/runtimes/c/include/ads_helpers.h b/runtimes/c/include/ads_helpers.h index c71f3bb..85e6b91 100644 --- a/runtimes/c/include/ads_helpers.h +++ b/runtimes/c/include/ads_helpers.h @@ -60,6 +60,9 @@ uint32_t ads_bitslice(uint32_t byte, uint8_t bit_start, uint8_t bit_end); uint32_t ads_concat_bits(const uint32_t *values, size_t count); /* ─── Formatter helpers (push items onto result.formatted.items) ──────── */ +/* Every ads_fmt_* takes ownership of the ads_value_t pointers passed in and + * frees them (matches ads_result_raw_set's transfer semantics). Pass NULL + * to no-op (matches the TS pattern of guarding before calling). */ void ads_fmt_position(ads_decode_result_t *r, ads_value_t *lat, ads_value_t *lon); void ads_fmt_position_value(ads_decode_result_t *r, ads_value_t *position); @@ -75,6 +78,71 @@ void ads_fmt_arrival_airport(ads_decode_result_t *r, ads_value_t *v); void ads_fmt_fuel(ads_decode_result_t *r, ads_value_t *v); void ads_fmt_unknown_arr(ads_decode_result_t *r, const char *const *values, size_t count); +/* Time-of-day formatters (value in seconds since midnight). */ +void ads_fmt_eta(ads_decode_result_t *r, ads_value_t *v); +void ads_fmt_off(ads_decode_result_t *r, ads_value_t *v); +void ads_fmt_on(ads_decode_result_t *r, ads_value_t *v); +void ads_fmt_in(ads_decode_result_t *r, ads_value_t *v); +void ads_fmt_out(ads_decode_result_t *r, ads_value_t *v); + +/* Calendar formatters. */ +void ads_fmt_day(ads_decode_result_t *r, ads_value_t *v); +void ads_fmt_departure_day(ads_decode_result_t *r, ads_value_t *v); +void ads_fmt_arrival_day(ads_decode_result_t *r, ads_value_t *v); +void ads_fmt_month(ads_decode_result_t *r, ads_value_t *v); + +/* Velocity / atmosphere formatters. */ +void ads_fmt_mach(ads_decode_result_t *r, ads_value_t *v); +void ads_fmt_groundspeed(ads_decode_result_t *r, ads_value_t *v); +void ads_fmt_airspeed(ads_decode_result_t *r, ads_value_t *v); +/* TS signature: takes the raw STRING and converts M→- / P→+ before + * numeric parse; no-ops on empty input. */ +void ads_fmt_temperature(ads_decode_result_t *r, const char *value); +void ads_fmt_total_air_temp(ads_decode_result_t *r, const char *value); + +/* Fuel formatters. */ +void ads_fmt_current_fuel(ads_decode_result_t *r, ads_value_t *v); +void ads_fmt_remaining_fuel(ads_decode_result_t *r, ads_value_t *v); + +/* Routing formatters. */ +void ads_fmt_alternate_airport(ads_decode_result_t *r, ads_value_t *v); +void ads_fmt_arrival_runway(ads_decode_result_t *r, ads_value_t *v); +void ads_fmt_alternate_runway(ads_decode_result_t *r, ads_value_t *v); + +/* Event formatters. */ +void ads_fmt_state_change(ads_decode_result_t *r, const char *from, const char *to); +void ads_fmt_door_event(ads_decode_result_t *r, const char *door, const char *state); + +/* Free-text formatters. */ +void ads_fmt_text(ads_decode_result_t *r, const char *value); +void ads_fmt_unknown(ads_decode_result_t *r, const char *value); +void ads_fmt_unknown_sep(ads_decode_result_t *r, const char *value, const char *sep); +void ads_fmt_unknown_arr_sep(ads_decode_result_t *r, const char *const *values, size_t count, const char *sep); + +/* Diagnostic formatters. */ +void ads_fmt_checksum(ads_decode_result_t *r, ads_value_t *v); +void ads_fmt_checksum_algorithm(ads_decode_result_t *r, const char *value); + +/* Generic structured-item push for plugins that emit custom item shapes + * (OHMA, WRN, SQ, ATIS, etc.). Stores no raw field; just pushes the item. */ +void ads_fmt_push_item(ads_decode_result_t *r, const char *type, const char *code, + const char *label, const char *value); + +/* Time-of-day helper: seconds since midnight → "HH:MM:SS" (for < 86400); + * larger values stringify as-is. Caller frees the returned malloc'd string. */ +char *ads_fmt_time_of_day_str(int64_t seconds); + +/* ─── Result mutators ────────────────────────────────────────────────────── */ +/* For escape hatches that need to override fields the ads_result_new() call + * already set up (e.g. variant-dependent description, custom remaining text). */ + +void ads_result_set_description(ads_decode_result_t *r, const char *description); +void ads_result_set_remaining(ads_decode_result_t *r, const char *text); +void ads_result_append_remaining(ads_decode_result_t *r, const char *text, const char *sep); +const char *ads_result_get_remaining(const ads_decode_result_t *r); +void ads_result_set_decode_level(ads_decode_result_t *r, ads_decode_level_t level); +void ads_result_clear_items(ads_decode_result_t *r); + #ifdef __cplusplus } #endif diff --git a/runtimes/c/src/result_formatter.c b/runtimes/c/src/result_formatter.c index e3acbaa..02c9937 100644 --- a/runtimes/c/src/result_formatter.c +++ b/runtimes/c/src/result_formatter.c @@ -41,10 +41,11 @@ void ads_fmt_position(ads_decode_result_t *r, ads_value_t *lat, ads_value_t *lon cJSON_AddNumberToObject(pos, "longitude", lo); cJSON_AddItemToObject(r->raw, "position", pos); char buf[64]; + /* TS CoordinateUtils.coordinateString: strictly > 0 → N/E; 0 → S/W. */ snprintf(buf, sizeof(buf), "%.3f %c, %.3f %c", - la < 0 ? -la : la, la < 0 ? 'S' : 'N', - lo < 0 ? -lo : lo, lo < 0 ? 'W' : 'E'); - push_item(r, "aircraft_position", "ARP", "Aircraft Position", buf); + la < 0 ? -la : la, la > 0 ? 'N' : 'S', + lo < 0 ? -lo : lo, lo > 0 ? 'E' : 'W'); + push_item(r, "aircraft_position", "POS", "Aircraft Position", buf); ads_value_free(lat); ads_value_free(lon); } @@ -74,9 +75,11 @@ static void push_numeric(ads_decode_result_t *r, ads_value_t *v, const char *raw void ads_fmt_altitude(ads_decode_result_t *r, ads_value_t *v) { push_numeric(r, v, "altitude", "altitude", "ALT", "Altitude", "feet"); } void ads_fmt_speed(ads_decode_result_t *r, ads_value_t *v) { push_numeric(r, v, "speed", "speed", "SPD", "Speed", "knots"); } -void ads_fmt_heading(ads_decode_result_t *r, ads_value_t *v) { push_numeric(r, v, "heading", "heading", "HDG", "Heading", "deg"); } -void ads_fmt_timestamp(ads_decode_result_t *r, ads_value_t *v) { push_numeric(r, v, "message_timestamp", "timestamp", "TS", "Timestamp", ""); } -void ads_fmt_fuel(ads_decode_result_t *r, ads_value_t *v) { push_numeric(r, v, "fuel_on_board", "fuel", "FUEL", "Fuel", ""); } +void ads_fmt_heading(ads_decode_result_t *r, ads_value_t *v) { push_numeric(r, v, "heading", "heading", "HDG", "Heading", ""); } +/* ads_fmt_timestamp lives after push_tod below — TS shape is + * time/TIMESTAMP/"Message Timestamp" with timestampToString display. */ +/* TS has no plain `fuel`; alias of currentFuel (fuel_on_board/FOB). */ +void ads_fmt_fuel(ads_decode_result_t *r, ads_value_t *v) { push_numeric(r, v, "fuel_on_board", "fuel_on_board", "FOB", "Fuel On Board", ""); } static void push_string(ads_decode_result_t *r, ads_value_t *v, const char *raw_key, const char *kind, const char *code, const char *label) { @@ -86,30 +89,295 @@ static void push_string(ads_decode_result_t *r, ads_value_t *v, const char *raw_ ads_value_free(v); } -void ads_fmt_callsign(ads_decode_result_t *r, ads_value_t *v) { push_string(r, v, "callsign", "callsign", "CS", "Callsign"); } -void ads_fmt_flight_number(ads_decode_result_t *r, ads_value_t *v) { push_string(r, v, "flight_number", "flight_number", "FLT", "Flight Number"); } -void ads_fmt_tail(ads_decode_result_t *r, ads_value_t *v) { push_string(r, v, "tail", "tail", "TAIL", "Tail Number"); } -void ads_fmt_departure_airport(ads_decode_result_t *r, ads_value_t *v) { push_string(r, v, "departure_icao", "airport_origin", "DEP", "Origin"); } -void ads_fmt_arrival_airport(ads_decode_result_t *r, ads_value_t *v) { push_string(r, v, "arrival_icao", "airport_destination", "ARR", "Destination"); } +void ads_fmt_callsign(ads_decode_result_t *r, ads_value_t *v) { push_string(r, v, "callsign", "callsign", "CALLSIGN", "Callsign"); } +void ads_fmt_flight_number(ads_decode_result_t *r, ads_value_t *v) { + /* TS no-ops on empty flight numbers. */ + const char *s = ads_value_as_string(v); + if (!s || !*s) { ads_value_free(v); return; } + push_string(r, v, "flight_number", "flight_number", "FLIGHT", "Flight Number"); +} +void ads_fmt_tail(ads_decode_result_t *r, ads_value_t *v) { push_string(r, v, "tail", "tail", "TAIL", "Tail"); } +void ads_fmt_departure_airport(ads_decode_result_t *r, ads_value_t *v) { push_string(r, v, "departure_icao", "icao", "ORG", "Origin"); } +void ads_fmt_arrival_airport(ads_decode_result_t *r, ads_value_t *v) { push_string(r, v, "arrival_icao", "icao", "DST", "Destination"); } void ads_fmt_unknown_arr(ads_decode_result_t *r, const char *const *values, size_t count) { + ads_fmt_unknown_arr_sep(r, values, count, ","); +} + +void ads_fmt_unknown_arr_sep(ads_decode_result_t *r, const char *const *values, size_t count, const char *sep) { + if (!r || !values || count == 0) return; + if (!sep) sep = ","; + size_t sep_len = strlen(sep); size_t total = 1; - for (size_t i = 0; i < count; i++) total += strlen(values[i] ? values[i] : "") + 1; + for (size_t i = 0; i < count; i++) total += strlen(values[i] ? values[i] : "") + sep_len; char *joined = malloc(total); if (!joined) return; joined[0] = '\0'; for (size_t i = 0; i < count; i++) { - if (i > 0) strcat(joined, ","); + if (i > 0) strcat(joined, sep); strcat(joined, values[i] ? values[i] : ""); } + ads_result_append_remaining(r, joined, sep); + free(joined); +} + +void ads_fmt_unknown(ads_decode_result_t *r, const char *value) { + ads_result_append_remaining(r, value, ","); +} + +void ads_fmt_unknown_sep(ads_decode_result_t *r, const char *value, const char *sep) { + ads_result_append_remaining(r, value, sep); +} + +/* ─── Time-of-day helper ─────────────────────────────────────────────────── */ + +/* TS DateTimeUtils.timestampToString: + * < 86400 → "HH:MM:SS" (time of day) + * < 2678400 → "YYYY-MM-DDTHH:MM:SS" (day known, year/month masked) + * otherwise → full ISO-8601 without millis + "Z" + * Caller frees. */ +char *ads_fmt_time_of_day_str(int64_t seconds) { + char *out = malloc(32); + if (!out) return NULL; + if (seconds >= 0 && seconds < 86400) { + int h = (int)(seconds / 3600); + int m = (int)((seconds % 3600) / 60); + int s = (int)(seconds % 60); + snprintf(out, 32, "%02d:%02d:%02d", h, m, s); + return out; + } + /* Civil-from-days (Howard Hinnant) to render ISO without time.h UTC pain. */ + int64_t days = seconds / 86400; + int64_t rem = seconds % 86400; + if (rem < 0) { rem += 86400; days -= 1; } + int h = (int)(rem / 3600), mi = (int)((rem % 3600) / 60), s = (int)(rem % 60); + int64_t z = days + 719468; + int64_t era = (z >= 0 ? z : z - 146096) / 146097; + int64_t doe = z - era * 146097; + int64_t yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + int64_t y = yoe + era * 400; + int64_t doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + int64_t mp = (5 * doy + 2) / 153; + int64_t d = doy - (153 * mp + 2) / 5 + 1; + int64_t mo = mp < 10 ? mp + 3 : mp - 9; + if (mo <= 2) y += 1; + if (seconds < 2678400) { + snprintf(out, 32, "YYYY-MM-%02lldT%02d:%02d:%02d", (long long)d, h, mi, s); + } else { + snprintf(out, 32, "%04lld-%02lld-%02lldT%02d:%02d:%02dZ", + (long long)y, (long long)mo, (long long)d, h, mi, s); + } + return out; +} + +/* ─── Time-of-day formatters (value in seconds since midnight) ───────────── */ + +static void push_tod(ads_decode_result_t *r, ads_value_t *v, const char *raw_key, + const char *kind, const char *code, const char *label) { + if (!v) return; + int64_t n = 0; + ads_value_as_int(v, &n); + cJSON_AddNumberToObject(r->raw, raw_key, (double)n); + char *display = ads_fmt_time_of_day_str(n); + push_item(r, kind, code, label, display ? display : ""); + free(display); + ads_value_free(v); +} + +void ads_fmt_eta(ads_decode_result_t *r, ads_value_t *v) { + push_tod(r, v, "eta_time", "time", "ETA", "Estimated Time of Arrival"); +} +void ads_fmt_off(ads_decode_result_t *r, ads_value_t *v) { + push_tod(r, v, "off_time", "time", "OFF", "Takeoff Time"); +} +void ads_fmt_on(ads_decode_result_t *r, ads_value_t *v) { + push_tod(r, v, "on_time", "time", "ON", "Landing Time"); +} +void ads_fmt_in(ads_decode_result_t *r, ads_value_t *v) { + push_tod(r, v, "in_time", "time", "IN", "In Gate Time"); +} +void ads_fmt_out(ads_decode_result_t *r, ads_value_t *v) { + push_tod(r, v, "out_time", "time", "OUT", "Out of Gate Time"); +} +void ads_fmt_timestamp(ads_decode_result_t *r, ads_value_t *v) { + push_tod(r, v, "message_timestamp", "time", "TIMESTAMP", "Message Timestamp"); +} + +/* ─── Calendar formatters ────────────────────────────────────────────────── */ + +static void push_int_item(ads_decode_result_t *r, ads_value_t *v, const char *raw_key, + const char *kind, const char *code, const char *label) { + if (!v) return; + int64_t n = 0; + ads_value_as_int(v, &n); + cJSON_AddNumberToObject(r->raw, raw_key, (double)n); + char buf[32]; + snprintf(buf, sizeof(buf), "%lld", (long long)n); + push_item(r, kind, code, label, buf); + ads_value_free(v); +} + +void ads_fmt_day(ads_decode_result_t *r, ads_value_t *v) { push_int_item(r, v, "day", "day", "MSG_DAY", "Day of Month"); } +void ads_fmt_departure_day(ads_decode_result_t *r, ads_value_t *v) { push_int_item(r, v, "departure_day", "day", "DEP_DAY", "Departure Day"); } +void ads_fmt_arrival_day(ads_decode_result_t *r, ads_value_t *v) { push_int_item(r, v, "arrival_day", "day", "ARR_DAY", "Arrival Day"); } +void ads_fmt_month(ads_decode_result_t *r, ads_value_t *v) { push_int_item(r, v, "month", "month", "MSG_MON", "Month of Year"); } + +/* ─── Velocity / atmosphere formatters ───────────────────────────────────── */ + +void ads_fmt_mach(ads_decode_result_t *r, ads_value_t *v) { push_numeric(r, v, "mach", "mach", "MACH", "Mach Number", "mach"); } +void ads_fmt_groundspeed(ads_decode_result_t *r, ads_value_t *v) { push_numeric(r, v, "groundspeed", "aircraft_groundspeed", "GSPD", "Aircraft Groundspeed", "knots"); } +void ads_fmt_airspeed(ads_decode_result_t *r, ads_value_t *v) { push_numeric(r, v, "airspeed", "airspeed", "ASPD", "True Airspeed", "knots"); } +/* TS temperature/totalAirTemp take a STRING and convert M→- / P→+ before + * Number(); no-op on empty input. Mirrored here. */ +void ads_fmt_temperature(ads_decode_result_t *r, const char *value) { + if (!r || !value || !*value) return; + char buf[32]; + size_t j = 0; + for (size_t i = 0; value[i] && j < sizeof(buf) - 1; i++) { + buf[j++] = value[i] == 'M' ? '-' : (value[i] == 'P' ? '+' : value[i]); + } + buf[j] = '\0'; + push_numeric(r, ads_value_from_double(atof(buf)), + "outside_air_temperature", "outside_air_temperature", + "OATEMP", "Outside Air Temperature (C)", "degrees"); +} +void ads_fmt_total_air_temp(ads_decode_result_t *r, const char *value) { + if (!r || !value || !*value) return; + char buf[32]; + size_t j = 0; + for (size_t i = 0; value[i] && j < sizeof(buf) - 1; i++) { + buf[j++] = value[i] == 'M' ? '-' : (value[i] == 'P' ? '+' : value[i]); + } + buf[j] = '\0'; + /* TS uses item type 'temperature' (not 'total_air_temperature') here. */ + push_numeric(r, ads_value_from_double(atof(buf)), + "total_air_temperature", "temperature", + "TATEMP", "Total Air Temperature (C)", "degrees"); +} + +/* ─── Fuel formatters ────────────────────────────────────────────────────── */ + +void ads_fmt_current_fuel(ads_decode_result_t *r, ads_value_t *v) { push_numeric(r, v, "fuel_on_board", "fuel_on_board", "FOB", "Fuel On Board", ""); } +/* The leading space in " FUEL_REM" is present in the TS source; preserved + * for byte parity until fixed upstream. */ +void ads_fmt_remaining_fuel(ads_decode_result_t *r, ads_value_t *v) { push_numeric(r, v, "fuel_remaining", "fuel_remaining", " FUEL_REM", "Fuel Remaining", ""); } + +/* ─── Routing formatters ─────────────────────────────────────────────────── */ + +void ads_fmt_alternate_airport(ads_decode_result_t *r, ads_value_t *v) { push_string(r, v, "alternate_icao", "icao", "ALT_DST", "Alternate Destination"); } +void ads_fmt_arrival_runway(ads_decode_result_t *r, ads_value_t *v) { push_string(r, v, "arrival_runway", "runway", "ARWY", "Arrival Runway"); } +void ads_fmt_alternate_runway(ads_decode_result_t *r, ads_value_t *v) { push_string(r, v, "alternate_runway", "runway", "ALT_ARWY", "Alternate Runway"); } + +/* ─── Event formatters ───────────────────────────────────────────────────── */ + +void ads_fmt_state_change(ads_decode_result_t *r, const char *from, const char *to) { + if (!r || !from || !to) return; + cJSON *obj = cJSON_CreateObject(); + cJSON_AddStringToObject(obj, "from", from); + cJSON_AddStringToObject(obj, "to", to); + cJSON_AddItemToObject(r->raw, "state_change", obj); + char buf[128]; + snprintf(buf, sizeof(buf), "%s -> %s", from, to); + push_item(r, "state_change", "STATE_CHANGE", "State Change", buf); +} + +void ads_fmt_door_event(ads_decode_result_t *r, const char *door, const char *state) { + if (!r || !door || !state) return; + cJSON *obj = cJSON_CreateObject(); + cJSON_AddStringToObject(obj, "door", door); + cJSON_AddStringToObject(obj, "state", state); + cJSON_AddItemToObject(r->raw, "door_event", obj); + char buf[128]; + snprintf(buf, sizeof(buf), "%s %s", door, state); + push_item(r, "door_event", "DOOR", "Door Event", buf); +} + +/* ─── Free-text formatters ───────────────────────────────────────────────── */ + +void ads_fmt_text(ads_decode_result_t *r, const char *value) { + if (!r || !value) return; + cJSON_AddStringToObject(r->raw, "text", value); + push_item(r, "text", "TEXT", "Text Message", value); +} + +/* ─── Diagnostic formatters ──────────────────────────────────────────────── */ + +void ads_fmt_checksum(ads_decode_result_t *r, ads_value_t *v) { + if (!v) return; + int64_t n = 0; + ads_value_as_int(v, &n); + cJSON_AddNumberToObject(r->raw, "checksum", (double)n); + /* TS: '0x' + ('0000' + hex).slice(-4) — always exactly the last 4 + * lowercase hex chars, zero-padded. */ + char hex[24]; + snprintf(hex, sizeof(hex), "%04llx", (unsigned long long)n); + size_t len = strlen(hex); + const char *tail4 = len > 4 ? hex + (len - 4) : hex; + char buf[16]; + snprintf(buf, sizeof(buf), "0x%s", tail4); + push_item(r, "message_checksum", "CHECKSUM", "Message Checksum", buf); + ads_value_free(v); +} + +/* TS stores the raw field but pushes NO formatted item (the item block is + * commented out in the reference). Mirrored exactly. */ +void ads_fmt_checksum_algorithm(ads_decode_result_t *r, const char *value) { + if (!r || !value) return; + cJSON_AddStringToObject(r->raw, "checksum_algorithm", value); +} + +/* ─── Generic structured-item push ───────────────────────────────────────── */ + +void ads_fmt_push_item(ads_decode_result_t *r, const char *type, const char *code, + const char *label, const char *value) { + if (!r) return; + push_item(r, + type ? type : "unknown", + code ? code : "", + label ? label : "", + value ? value : ""); +} + +/* ─── Result mutators (declared in ads_helpers.h) ────────────────────────── */ + +void ads_result_set_description(ads_decode_result_t *r, const char *description) { + if (!r) return; + free(r->description); + r->description = description ? strdup(description) : NULL; +} + +void ads_result_set_remaining(ads_decode_result_t *r, const char *text) { + if (!r) return; + free(r->remaining); + r->remaining = text ? strdup(text) : NULL; +} + +void ads_result_append_remaining(ads_decode_result_t *r, const char *text, const char *sep) { + if (!r || !text) return; + if (!sep) sep = ","; if (r->remaining) { - size_t newlen = strlen(r->remaining) + 1 + strlen(joined) + 1; + size_t newlen = strlen(r->remaining) + strlen(sep) + strlen(text) + 1; char *grown = malloc(newlen); - snprintf(grown, newlen, "%s,%s", r->remaining, joined); + if (!grown) return; + snprintf(grown, newlen, "%s%s%s", r->remaining, sep, text); free(r->remaining); r->remaining = grown; } else { - r->remaining = strdup(joined); + r->remaining = strdup(text); } - free(joined); +} + +const char *ads_result_get_remaining(const ads_decode_result_t *r) { + return r ? r->remaining : NULL; +} + +void ads_result_set_decode_level(ads_decode_result_t *r, ads_decode_level_t level) { + if (!r) return; + r->decode_level = level; +} + +void ads_result_clear_items(ads_decode_result_t *r) { + if (!r) return; + cJSON_Delete(r->items); + r->items = cJSON_CreateArray(); }