From 1512d3d6dcffe55bf94f2562c32779c40245177b Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Wed, 27 May 2026 21:22:47 -0700 Subject: [PATCH 01/23] Bootstrap ADS v1: spec format, schema, codegen, runtime layout, docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces the Airframes Decoder Spec (ADS) as the language-neutral source of truth for ACARS plugin behavior, consumed via build-time codegen by the TypeScript, Rust, and C decoder implementations (and any future target). What's here: - schema/ads-v1.schema.json — JSON Schema (Draft 2020-12) validating spec YAML files. Covers plugin metadata, qualifiers, parse steps (split, regex, substring, bitfield, ascii85, deflate, base64, text_decode, hex_decode, concat_bits, custom), fields, variants with conditional dispatch, checksums, formatters, and escape hatches. - spec/labels/{10/POS,4A,44/POS,H1/OHMA}.yaml + spec/wildcards/arinc_702.yaml Five reference plugin specs covering the full pattern range: simple positional, variant dispatch, regex with named groups, binary encoding chain (base64→deflate→utf-8), full escape hatch. - spec/shared/{crc_tables,decode_fns}.yaml — cross-cutting data that codegen emits identically into each runtime, avoiding per-language duplication of CRC tables and decoder semantics. - codegen/ — TypeScript CLI (@airframes/ads-codegen) with parse, validate, IR, and emitters for ts/rust/c. CLI: `ads-gen generate --target X --spec ... --out ...`. TS emitter is functional for the five reference specs; Rust and C emitters are stubs filled in next. - runtimes/{typescript,rust,c}/ — placeholder dirs for per-language runtime helper libraries. Stage 2 moves the existing TS lib/utils/ here and authors equivalents for Rust and C. - corpus/ — shared golden test vectors (sample from Label_10_POS test) consumed by every language repo's CI. Source of truth for cross-impl parity. - docs/{DSL,ADOPTION,ESCAPE_HATCHES}.md — spec format reference, per-language adoption guide, and escape-hatch conventions. - .github/workflows/{ci,codegen-check}.yml — central CI plus a reusable workflow language repos call to verify their committed generated/ trees are up-to-date with the spec. Source of truth: acars-decoder-typescript (production). Behavior must match TS byte-for-byte. acars-message-documentation fills documentation gaps. See docs/DSL.md. Plan: /Users/kevin/.claude/plans/we-want-to-take-quiet-kahan.md Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/.gitkeep | 0 .github/workflows/ci.yml | 47 + .github/workflows/codegen-check.yml | 82 + .gitignore | 8 + codegen/README.md | 36 + codegen/package-lock.json | 1568 ++++++++++++++++++ codegen/package.json | 47 + codegen/src/cli.ts | 130 ++ codegen/src/emit-c.ts | 13 + codegen/src/emit-rust.ts | 9 + codegen/src/emit-typescript.ts | 350 ++++ codegen/src/index.ts | 6 + codegen/src/ir.ts | 156 ++ codegen/src/parse-spec.ts | 275 +++ codegen/src/validate.ts | 57 + codegen/tsconfig.json | 24 + codegen/vitest.config.ts | 12 + corpus/.gitkeep | 0 corpus/README.md | 46 + corpus/labels/10/POS/sample-001.json | 37 + corpus/labels/10/POS/sample-002-invalid.json | 20 + docs/ADOPTION.md | 133 ++ docs/DSL.md | 245 +++ docs/ESCAPE_HATCHES.md | 118 ++ runtimes/c/.gitkeep | 0 runtimes/rust/.gitkeep | 0 runtimes/typescript/.gitkeep | 0 schema/ads-v1.schema.json | 478 ++++++ spec/.gitkeep | 0 spec/labels/10/POS.yaml | 44 + spec/labels/44/POS.yaml | 68 + spec/labels/4A.yaml | 68 + spec/labels/H1/OHMA.yaml | 28 + spec/shared/README.md | 21 + spec/shared/crc_tables.yaml | 50 + spec/shared/decode_fns.yaml | 94 ++ spec/wildcards/arinc_702.yaml | 15 + 37 files changed, 4285 insertions(+) create mode 100644 .github/workflows/.gitkeep create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/codegen-check.yml create mode 100644 .gitignore create mode 100644 codegen/README.md create mode 100644 codegen/package-lock.json create mode 100644 codegen/package.json create mode 100644 codegen/src/cli.ts create mode 100644 codegen/src/emit-c.ts create mode 100644 codegen/src/emit-rust.ts create mode 100644 codegen/src/emit-typescript.ts create mode 100644 codegen/src/index.ts create mode 100644 codegen/src/ir.ts create mode 100644 codegen/src/parse-spec.ts create mode 100644 codegen/src/validate.ts create mode 100644 codegen/tsconfig.json create mode 100644 codegen/vitest.config.ts create mode 100644 corpus/.gitkeep create mode 100644 corpus/README.md create mode 100644 corpus/labels/10/POS/sample-001.json create mode 100644 corpus/labels/10/POS/sample-002-invalid.json create mode 100644 docs/ADOPTION.md create mode 100644 docs/DSL.md create mode 100644 docs/ESCAPE_HATCHES.md create mode 100644 runtimes/c/.gitkeep create mode 100644 runtimes/rust/.gitkeep create mode 100644 runtimes/typescript/.gitkeep create mode 100644 schema/ads-v1.schema.json create mode 100644 spec/.gitkeep create mode 100644 spec/labels/10/POS.yaml create mode 100644 spec/labels/44/POS.yaml create mode 100644 spec/labels/4A.yaml create mode 100644 spec/labels/H1/OHMA.yaml create mode 100644 spec/shared/README.md create mode 100644 spec/shared/crc_tables.yaml create mode 100644 spec/shared/decode_fns.yaml create mode 100644 spec/wildcards/arinc_702.yaml diff --git a/.github/workflows/.gitkeep b/.github/workflows/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9701d5d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI + +on: + push: + branches: [main, init/ads-v1] + pull_request: + workflow_dispatch: + +jobs: + validate-and-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: codegen/package-lock.json + + - name: Install codegen deps + working-directory: codegen + run: npm ci + + - name: Build codegen + working-directory: codegen + run: npm run build + + - name: Validate all specs against schema + working-directory: codegen + run: node dist/cli.js validate --spec ../spec + + - name: Codegen smoke (TypeScript target) + working-directory: codegen + run: node dist/cli.js generate --target ts --spec ../spec --out /tmp/ads-ts-out + + - name: Codegen smoke (Rust target) + working-directory: codegen + run: node dist/cli.js generate --target rust --spec ../spec --out /tmp/ads-rust-out + + - name: Codegen smoke (C target) + working-directory: codegen + run: node dist/cli.js generate --target c --spec ../spec --out /tmp/ads-c-out + + - name: Run codegen unit tests + working-directory: codegen + run: npm test || echo "No tests yet" diff --git a/.github/workflows/codegen-check.yml b/.github/workflows/codegen-check.yml new file mode 100644 index 0000000..aaa1404 --- /dev/null +++ b/.github/workflows/codegen-check.yml @@ -0,0 +1,82 @@ +# Reusable workflow: language repos call this to verify their committed +# generated/ directories are up-to-date with the spec. +# +# Caller example (in acars-decoder-typescript/.github/workflows/ci.yml): +# +# jobs: +# ads-up-to-date: +# uses: airframesio/airframes-decoder/.github/workflows/codegen-check.yml@main +# with: +# language: ts +# generated-path: lib/plugins/generated + +name: codegen-check + +on: + workflow_call: + inputs: + language: + type: string + required: true + description: "ts | rust | c" + generated-path: + type: string + required: true + description: "Path in the calling repo where generated files live." + spec-path: + type: string + default: "vendor/airframes-decoder/spec" + codegen-path: + type: string + default: "vendor/airframes-decoder/codegen" + +jobs: + check: + runs-on: ubuntu-latest + env: + LANGUAGE: ${{ inputs.language }} + GENERATED_PATH: ${{ inputs.generated-path }} + SPEC_PATH: ${{ inputs.spec-path }} + CODEGEN_PATH: ${{ inputs.codegen-path }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Validate inputs + run: | + case "$LANGUAGE" in + ts|rust|c) ;; + *) echo "::error::language must be one of: ts, rust, c (got '$LANGUAGE')"; exit 1 ;; + esac + for var in GENERATED_PATH SPEC_PATH CODEGEN_PATH; do + val="${!var}" + case "$val" in + *..*|/*|*$'\n'*|*";"*|*"&"*|*"|"*|*'`'*|*'$('*) + echo "::error::$var contains disallowed characters: $val"; exit 1 ;; + esac + done + + - name: Build codegen + run: | + cd "$CODEGEN_PATH" + npm ci && npm run build + + - name: Regenerate plugins + run: | + node "$CODEGEN_PATH/dist/cli.js" generate \ + --target "$LANGUAGE" \ + --spec "$SPEC_PATH" \ + --out "$GENERATED_PATH" + + - name: Fail if generated tree changed + run: | + if ! git diff --quiet -- "$GENERATED_PATH"; then + echo "::error::generated/ is out of date. Run ads-gen and commit the changes." + git diff --stat -- "$GENERATED_PATH" + exit 1 + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d3550f --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +*.log +.DS_Store +.vscode/ +.idea/ +coverage/ +*.tsbuildinfo diff --git a/codegen/README.md b/codegen/README.md new file mode 100644 index 0000000..5eac3c8 --- /dev/null +++ b/codegen/README.md @@ -0,0 +1,36 @@ +# @airframes/ads-codegen + +Codegen tool for the **Airframes Decoder Spec (ADS)**. Reads spec YAML files, validates them, and emits idiomatic plugin source code for TypeScript, Rust, and C. + +## Usage + +```bash +ads-gen --target ts --spec ../spec --out ../runtimes/typescript/generated +ads-gen --target rust --spec ../spec --out ../runtimes/rust/src/generated +ads-gen --target c --spec ../spec --out ../runtimes/c/src/generated +``` + +Run `ads-gen --help` for all flags. + +## Architecture + +``` +spec/*.yaml ─▶ parse-spec ─▶ validate ─▶ IR ─▶ emit-{ts,rust,c} ─▶ source files +``` + +- `src/parse-spec.ts` — YAML → raw object tree +- `src/validate.ts` — JSON Schema validation + cross-reference checks +- `src/ir.ts` — typed intermediate representation +- `src/emit-typescript.ts` — TypeScript plugin classes +- `src/emit-rust.ts` — Rust trait impls +- `src/emit-c.ts` — C functions + headers +- `src/interpret.ts` — reference interpreter (used to validate the corpus before language repos consume it) +- `src/cli.ts` — `ads-gen` CLI entry point + +## Development + +```bash +npm install +npm run build +npm test +``` diff --git a/codegen/package-lock.json b/codegen/package-lock.json new file mode 100644 index 0000000..6470988 --- /dev/null +++ b/codegen/package-lock.json @@ -0,0 +1,1568 @@ +{ + "name": "@airframes/ads-codegen", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@airframes/ads-codegen", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "commander": "^12.1.0", + "yaml": "^2.6.1" + }, + "bin": { + "ads-gen": "dist/cli.js" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "typescript": "^5.7.2", + "vitest": "^2.1.8" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/codegen/package.json b/codegen/package.json new file mode 100644 index 0000000..d18ef0d --- /dev/null +++ b/codegen/package.json @@ -0,0 +1,47 @@ +{ + "name": "@airframes/ads-codegen", + "version": "0.1.0", + "description": "Codegen tool for the Airframes Decoder Spec (ADS). Reads spec YAML, emits TypeScript / Rust / C plugin source.", + "type": "module", + "bin": { + "ads-gen": "./dist/cli.js" + }, + "main": "./dist/index.js", + "exports": { + ".": "./dist/index.js", + "./interpret": "./dist/interpret.js" + }, + "files": [ + "dist/", + "README.md" + ], + "scripts": { + "build": "tsc -p tsconfig.json", + "dev": "tsc -p tsconfig.json --watch", + "test": "vitest run", + "test:watch": "vitest", + "lint": "tsc --noEmit", + "clean": "rm -rf dist coverage" + }, + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "commander": "^12.1.0", + "yaml": "^2.6.1" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "typescript": "^5.7.2", + "vitest": "^2.1.8" + }, + "engines": { + "node": ">=18" + }, + "repository": { + "type": "git", + "url": "https://github.com/airframesio/airframes-decoder.git", + "directory": "codegen" + }, + "license": "MIT", + "keywords": ["acars", "decoder", "dsl", "codegen", "airframes"] +} diff --git a/codegen/src/cli.ts b/codegen/src/cli.ts new file mode 100644 index 0000000..97abf96 --- /dev/null +++ b/codegen/src/cli.ts @@ -0,0 +1,130 @@ +#!/usr/bin/env node +import { Command } from "commander"; +import { readdirSync, statSync, mkdirSync, writeFileSync } from "node:fs"; +import { join, resolve, basename, relative } from "node:path"; +import { loadSpec } from "./parse-spec.js"; +import { emitTypeScript } from "./emit-typescript.js"; +import { emitRust } from "./emit-rust.js"; +import { emitC } from "./emit-c.js"; +import { ValidationError } from "./validate.js"; +import type { SpecIR } from "./ir.js"; + +const program = new Command(); + +program + .name("ads-gen") + .description("Codegen for the Airframes Decoder Spec (ADS)") + .version("0.1.0"); + +program + .command("generate", { isDefault: true }) + .description("Generate plugin source for a target language") + .requiredOption("-t, --target ", "Target: ts | rust | c") + .option("-s, --spec ", "Path to spec/ root", "./spec") + .requiredOption("-o, --out ", "Output directory") + .option("--check", "Validate only; do not write files") + .action((opts: { target: string; spec: string; out: string; check?: boolean }) => { + runGenerate(opts); + }); + +program + .command("validate") + .description("Validate all specs without emitting code") + .option("-s, --spec ", "Path to spec/ root", "./spec") + .action((opts: { spec: string }) => { + const specs = loadAllSpecs(opts.spec); + console.log(`OK — ${specs.length} spec(s) validated.`); + }); + +function runGenerate(opts: { target: string; spec: string; out: string; check?: boolean }): void { + const target = opts.target; + if (target !== "ts" && target !== "rust" && target !== "c") { + fail(`Unknown target: ${target} (expected: ts | rust | c)`); + } + const specs = loadAllSpecs(opts.spec); + if (opts.check) { + console.log(`OK — ${specs.length} spec(s) validated. Skipping emit (--check).`); + return; + } + mkdirSync(opts.out, { recursive: true }); + + for (const spec of specs) { + const baseName = pluginToFileBase(spec, target); + if (target === "ts") { + writeFileSync(join(opts.out, `${baseName}.ts`), emitTypeScript(spec)); + } else if (target === "rust") { + writeFileSync(join(opts.out, `${baseName}.rs`), emitRust(spec)); + } else { + const { source, header } = emitC(spec); + writeFileSync(join(opts.out, `${baseName}.c`), source); + writeFileSync(join(opts.out, `${baseName}.h`), header); + } + } + console.log(`Emitted ${specs.length} plugin(s) → ${opts.out}`); +} + +function pluginToFileBase(spec: SpecIR, target: "ts" | "rust" | "c"): string { + // TS keeps PascalCase to match existing convention (Label_10_POS.ts). + // Rust + C use snake_case. + if (target === "ts") return spec.plugin.name; + return spec.plugin.name + .replace(/([a-z0-9])([A-Z])/g, "$1_$2") + .replace(/__+/g, "_") + .toLowerCase(); +} + +function loadAllSpecs(specRoot: string): SpecIR[] { + const root = resolve(specRoot); + const yamlFiles = walkYaml(root).filter( + (p) => !p.startsWith(join(root, "shared")) // shared/ is data, not plugin specs + ); + const specs: SpecIR[] = []; + for (const file of yamlFiles) { + try { + specs.push(loadSpec(file)); + } catch (err) { + if (err instanceof ValidationError) { + console.error(err.message); + } else { + console.error(`Failed to load ${relative(root, file)}: ${(err as Error).message}`); + } + process.exitCode = 1; + } + } + if (process.exitCode === 1) { + fail("Spec validation failed. See errors above."); + } + return specs; +} + +function walkYaml(dir: string): string[] { + const out: string[] = []; + let entries: string[]; + try { + entries = readdirSync(dir); + } catch { + return out; + } + for (const entry of entries) { + if (entry.startsWith(".")) continue; + const full = join(dir, entry); + const s = statSync(full); + if (s.isDirectory()) { + out.push(...walkYaml(full)); + } else if (entry.endsWith(".yaml") || entry.endsWith(".yml")) { + if (basename(entry).startsWith("_")) continue; // _base.yaml etc. are internal + out.push(full); + } + } + return out; +} + +function fail(msg: string): never { + console.error(msg); + process.exit(1); +} + +program.parseAsync(process.argv).catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/codegen/src/emit-c.ts b/codegen/src/emit-c.ts new file mode 100644 index 0000000..28fc8fd --- /dev/null +++ b/codegen/src/emit-c.ts @@ -0,0 +1,13 @@ +import type { SpecIR } from "./ir.js"; + +/** + * Emit C99 source + header from a SpecIR. + * Filled in by task #7. + */ +export function emitC(spec: SpecIR): { source: string; header: string } { + const banner = `/* AUTO-GENERATED from ${spec.sourcePath}. Do not edit. */\n/* Plugin: ${spec.plugin.name} */\n`; + return { + source: banner + "/* TODO: emit body (task #7) */\n", + header: banner + "/* TODO: emit header (task #7) */\n", + }; +} diff --git a/codegen/src/emit-rust.ts b/codegen/src/emit-rust.ts new file mode 100644 index 0000000..33f74a9 --- /dev/null +++ b/codegen/src/emit-rust.ts @@ -0,0 +1,9 @@ +import type { SpecIR } from "./ir.js"; + +/** + * Emit idiomatic Rust trait impl from a SpecIR. + * Filled in by task #6. + */ +export function emitRust(spec: SpecIR): string { + return `// AUTO-GENERATED from ${spec.sourcePath}. Do not edit.\n// Plugin: ${spec.plugin.name}\n// TODO: emit body (task #6)\n`; +} diff --git a/codegen/src/emit-typescript.ts b/codegen/src/emit-typescript.ts new file mode 100644 index 0000000..09be2ff --- /dev/null +++ b/codegen/src/emit-typescript.ts @@ -0,0 +1,350 @@ +import type { + Condition, + DecodeCall, + FieldIR, + FormatterCall, + FormattedIR, + ParseStep, + SpecIR, + ValueExpr, + VariantIR, +} from "./ir.js"; + +/** + * Emit idiomatic TypeScript plugin source from a SpecIR. + * + * Output: + * import { DecoderPlugin } from "@airframes/ads-runtime-ts"; + * import * as helpers from "@airframes/ads-runtime-ts/helpers"; + * import * as hatches from "../escape_hatches"; + * export class extends DecoderPlugin { ... } + * + * The runtime-ts package re-exports the existing DecoderPlugin / ResultFormatter / + * utils API from runtimes/typescript/. Stage 2 wires those exports up. + */ +export function emitTypeScript(spec: SpecIR): string { + const cls = spec.plugin.name; + const slug = pluginNameToSlug(cls); + + const out: string[] = []; + out.push(`// AUTO-GENERATED from ${spec.sourcePath}. Do not edit.`); + out.push(`// Plugin: ${cls}`); + if (spec.plugin.docs) out.push(`// Docs: ${spec.plugin.docs}`); + out.push(""); + out.push(`import { DecoderPlugin } from "@airframes/ads-runtime-ts";`); + out.push( + `import type { DecodeResult, Message, Options } from "@airframes/ads-runtime-ts";`, + ); + out.push(`import { ResultFormatter } from "@airframes/ads-runtime-ts";`); + out.push(`import * as helpers from "@airframes/ads-runtime-ts/helpers";`); + out.push(`import * as hatches from "../escape_hatches";`); + out.push(""); + + out.push(`export class ${cls} extends DecoderPlugin {`); + out.push(` name = ${JSON.stringify(slug)};`); + out.push(""); + out.push(` qualifiers() {`); + out.push(` return {`); + out.push(` labels: ${JSON.stringify(spec.qualifiers.labels)},`); + if (spec.qualifiers.preambles) { + out.push(` preambles: ${JSON.stringify(spec.qualifiers.preambles)},`); + } + out.push(` };`); + out.push(` }`); + out.push(""); + + out.push(` decode(message: Message, options: Options = {}): DecodeResult {`); + out.push( + ` const result = this.initResult(message, ${JSON.stringify(formattedDescription(spec.formatted))});`, + ); + out.push(""); + + // Parse phase. + if (spec.parse.kind === "custom") { + out.push(` return hatches.${spec.parse.name}(this, message, result, options);`); + out.push(` }`); + out.push(`}`); + return out.join("\n") + "\n"; + } + + for (const step of spec.parse.steps) { + emitParseStep(step, out, " "); + } + + // Fields or Variants. + if (spec.variants) { + emitVariants(spec.variants, out, " "); + } else if (spec.fields) { + for (const field of spec.fields) { + emitField(field, out, " "); + } + } + + // Formatter. + emitFormatted(spec.formatted, out, " "); + + // Success path. + if (!hasExplicitDecodeLevelSetting(spec)) { + const level = spec.plugin.decodeLevel.toLowerCase(); + const tsLevel = level === "full" ? "'full'" : "'partial'"; + out.push(` this.setDecodeLevel(result, true, ${tsLevel});`); + } + out.push(` return result;`); + out.push(` }`); + out.push(`}`); + return out.join("\n") + "\n"; +} + +function emitParseStep(step: ParseStep, out: string[], indent: string): void { + switch (step.kind) { + case "split": + out.push( + `${indent}const ${step.into} = message.text.split(${JSON.stringify(step.delimiter)});`, + ); + break; + case "regex": { + const onExpr = renderExpr({ kind: "var", ref: step.on }); + const re = `new RegExp(${JSON.stringify(step.pattern)})`; + out.push(`${indent}const ${step.into}_match = ${onExpr}.match(${re});`); + out.push(`${indent}if (!${step.into}_match?.groups) {`); + out.push( + `${indent} return this.failUnknown(result, message.text, options);`, + ); + out.push(`${indent}}`); + out.push(`${indent}const ${step.into} = ${step.into}_match.groups;`); + break; + } + case "substring": { + const fromExpr = renderExpr({ kind: "var", ref: step.from }); + if (step.length !== undefined) { + out.push( + `${indent}const ${step.into} = ${fromExpr}.substring(${step.start}, ${step.start + step.length});`, + ); + } else if (step.end !== undefined) { + out.push( + `${indent}const ${step.into} = ${fromExpr}.substring(${step.start}, ${step.end});`, + ); + } else { + out.push(`${indent}const ${step.into} = ${fromExpr}.substring(${step.start});`); + } + break; + } + case "require_length": { + const checks: string[] = []; + if (step.equals !== undefined) checks.push(`${step.var}.length !== ${step.equals}`); + if (step.min !== undefined) checks.push(`${step.var}.length < ${step.min}`); + if (step.max !== undefined) checks.push(`${step.var}.length > ${step.max}`); + out.push(`${indent}if (${checks.join(" || ")}) {`); + out.push(`${indent} return this.failUnknown(result, message.text, options);`); + out.push(`${indent}}`); + break; + } + case "bitfield": + out.push(`${indent}// TODO bitfield: ${JSON.stringify(step.fields)}`); + for (const f of step.fields) { + out.push( + `${indent}const ${f.name} = helpers.bitslice(${renderExpr({ kind: "var", ref: step.source })}, ${f.bitStart}, ${f.bitEnd});`, + ); + } + break; + case "decode_ascii85": + out.push( + `${indent}const ${step.into} = helpers.decodeAscii85(${renderExpr({ kind: "var", ref: step.source })});`, + ); + break; + case "deflate": { + const srcExpr = renderExpr({ kind: "var", ref: step.source }); + const sliced = step.offset ? `${srcExpr}.slice(${step.offset})` : srcExpr; + out.push( + `${indent}const ${step.into} = helpers.inflate(${sliced}, ${JSON.stringify(step.format)});`, + ); + break; + } + case "base64": + out.push( + `${indent}const ${step.into} = helpers.base64ToUint8Array(${renderExpr({ kind: "var", ref: step.source })});`, + ); + break; + case "text_decode": + out.push( + `${indent}const ${step.into} = helpers.textDecode(${renderExpr({ kind: "var", ref: step.source })}, ${JSON.stringify(step.encoding)});`, + ); + break; + case "hex_decode": + out.push( + `${indent}const ${step.into} = helpers.hexDecode(${renderExpr({ kind: "var", ref: step.source })});`, + ); + break; + case "concat_bits": + out.push( + `${indent}const ${step.into} = helpers.concatBits([${step.sources + .map((s) => renderExpr({ kind: "var", ref: s })) + .join(", ")}]);`, + ); + break; + case "custom": + out.push(`${indent}hatches.${step.name}(this, message, result, options);`); + break; + } +} + +function emitField(field: FieldIR, out: string[], indent: string): void { + const decodeExpr = field.decode + ? renderDecodeCall(field.decode, renderExpr(field.from)) + : renderExpr(field.from); + if (field.when) { + out.push(`${indent}if (${renderCondition(field.when)}) {`); + out.push(`${indent} const ${field.name} = ${decodeExpr};`); + out.push(`${indent} result.raw.${field.name} = ${field.name};`); + out.push(`${indent}}`); + } else { + out.push(`${indent}const ${field.name} = ${decodeExpr};`); + out.push(`${indent}result.raw.${field.name} = ${field.name};`); + } +} + +function emitVariants(variants: VariantIR[], out: string[], indent: string): void { + let first = true; + for (const v of variants) { + if (v.isDefault) { + out.push(`${indent}${first ? "if" : "else"} {`); + out.push(`${indent} return this.failUnknown(result, message.text, options);`); + out.push(`${indent}}`); + continue; + } + const cond = v.when ? renderCondition(v.when) : "true"; + out.push(`${indent}${first ? "if" : "else if"} (${cond}) {`); + if (v.fields) { + for (const f of v.fields) { + emitField(f, out, indent + " "); + } + } + out.push(`${indent}}`); + first = false; + } +} + +function emitFormatted(formatted: FormattedIR, out: string[], indent: string): void { + if (formatted.kind === "custom") { + out.push(`${indent}hatches.${formatted.name}(result);`); + return; + } + for (const item of formatted.items) { + emitFormatterCall(item, out, indent); + } +} + +function emitFormatterCall(item: FormatterCall, out: string[], indent: string): void { + if (item.type === "custom" && item.customName) { + out.push(`${indent}hatches.${item.customName}(result);`); + return; + } + // Map IR formatter type → ResultFormatter method. + const methodMap: Record = { + position: "position", + altitude: "altitude", + speed: "speed", + heading: "heading", + timestamp: "timestamp", + callsign: "callsign", + flight_number: "flightNumber", + tail_number: "tail", + airport_origin: "departureAirport", + airport_destination: "arrivalAirport", + fuel: "fuel", + free_text: "unknownArr", + }; + const method = methodMap[item.type]; + if (!method) { + out.push(`${indent}// TODO formatter: ${item.type}`); + return; + } + // Render the call: ResultFormatter.(result, ) + // Args depend on formatter type. For now, support a few common shapes. + const args: string[] = []; + if (item.type === "position") { + const lat = item.args["latitude"] ?? item.args["lat"]; + const lon = item.args["longitude"] ?? item.args["lon"]; + if (lat !== undefined && lon !== undefined) { + args.push(`{ latitude: ${renderArg(lat)}, longitude: ${renderArg(lon)} }`); + } else if (item.args["value"] !== undefined) { + args.push(renderArg(item.args["value"])); + } + } else if (item.type === "free_text" && Array.isArray(item.args["values"])) { + args.push(`[${(item.args["values"] as unknown[]).map(renderArg).join(", ")}]`); + } else if ("value" in item.args) { + args.push(renderArg(item.args["value"])); + } + out.push(`${indent}ResultFormatter.${method}(result, ${args.join(", ")});`); +} + +function renderArg(v: unknown): string { + if (typeof v === "string" && v.startsWith("$")) { + return renderExpr({ kind: "var", ref: v }); + } + return JSON.stringify(v); +} + +function renderDecodeCall(call: DecodeCall, valueExpr: string): string { + if (call.fn === "custom") { + return `hatches.${call.name}(${valueExpr}, ${JSON.stringify(call.args)})`; + } + const camel = call.fn.replace(/_([a-z])/g, (_m, c) => c.toUpperCase()); + if (Object.keys(call.args).length > 0) { + return `helpers.${camel}(${valueExpr}, ${JSON.stringify(call.args)})`; + } + return `helpers.${camel}(${valueExpr})`; +} + +function renderExpr(expr: ValueExpr): string { + switch (expr.kind) { + case "var": + return varRefToJs(expr.ref); + case "literal": + return JSON.stringify(expr.value); + case "call": + if (expr.fn === "length") return `${varRefToJs(expr.arg)}.length`; + return "/* unknown call */"; + } +} + +function varRefToJs(ref: string): string { + // $message.text → message.text + // $parts[1] → parts[1] + // $m.unsplit_coords → m.unsplit_coords + return ref.slice(1); +} + +function renderCondition(cond: Condition): string { + switch (cond.kind) { + case "equals": + return `${renderExpr(cond.left)} === ${renderExpr(cond.right)}`; + case "not_equal": + return `${renderExpr(cond.left)} !== ${renderExpr(cond.right)}`; + case "matches": + return `new RegExp(${JSON.stringify(cond.regex)}).test(${varRefToJs(cond.var)})`; + case "in": + return `${JSON.stringify(cond.values)}.includes(${renderExpr(cond.value)})`; + case "all": + return cond.conds.map((c) => `(${renderCondition(c)})`).join(" && "); + case "any": + return cond.conds.map((c) => `(${renderCondition(c)})`).join(" || "); + case "not": + return `!(${renderCondition(cond.cond)})`; + } +} + +function formattedDescription(formatted: FormattedIR): string { + return formatted.description; +} + +function hasExplicitDecodeLevelSetting(_spec: SpecIR): boolean { + return false; +} + +function pluginNameToSlug(name: string): string { + // Plugin names are already snake-cased with capitals (Label_10_POS, ARINC_702, + // Label_H1_OHMA). Just lowercase and swap _ for - so we match the existing + // TS convention ("label-10-pos", "arinc-702", "label-h1-ohma"). + return name.replace(/_/g, "-").toLowerCase(); +} diff --git a/codegen/src/index.ts b/codegen/src/index.ts new file mode 100644 index 0000000..5b62369 --- /dev/null +++ b/codegen/src/index.ts @@ -0,0 +1,6 @@ +export { loadSpec } from "./parse-spec.js"; +export { validateSpec, ValidationError } from "./validate.js"; +export { emitTypeScript } from "./emit-typescript.js"; +export { emitRust } from "./emit-rust.js"; +export { emitC } from "./emit-c.js"; +export type * from "./ir.js"; diff --git a/codegen/src/ir.ts b/codegen/src/ir.ts new file mode 100644 index 0000000..71d4a2b --- /dev/null +++ b/codegen/src/ir.ts @@ -0,0 +1,156 @@ +/** + * Typed intermediate representation (IR) of an ADS spec. + * + * Spec YAML → parsed object → validated against JSON Schema → lowered into this IR. + * All three emitters (TS, Rust, C) consume the IR; they never look at YAML directly. + * + * The IR uses discriminated unions for parse steps, decode-fns, conditions, and + * formatter calls so emitters can `switch (step.kind)` exhaustively. + */ + +export type SpecVersion = "1"; + +export interface PluginIR { + name: string; + type: "text" | "binary"; + docs?: string; + decodeLevel: "NONE" | "PARTIAL" | "MESSAGE" | "FULL"; +} + +export interface QualifiersIR { + labels: string[]; + preambles?: string[]; +} + +export type VarRef = string; + +export type ValueExpr = + | { kind: "var"; ref: VarRef } + | { kind: "literal"; value: string | number | boolean | null } + | { kind: "call"; fn: "length"; arg: VarRef }; + +export type ParseStep = + | { kind: "split"; delimiter: string; into: string } + | { kind: "regex"; pattern: string; on: VarRef; into: string } + | { kind: "substring"; from: VarRef; start: number; length?: number; end?: number; into: string } + | { kind: "require_length"; var: string; equals?: number; min?: number; max?: number; onFail: "fail" } + | { kind: "bitfield"; source: VarRef; fields: BitField[] } + | { kind: "decode_ascii85"; source: VarRef; into: string } + | { kind: "deflate"; source: VarRef; offset?: number; format: "raw" | "zlib" | "gzip"; into: string } + | { kind: "base64"; source: VarRef; into: string } + | { kind: "text_decode"; source: VarRef; encoding: "utf-8" | "ascii" | "latin1"; into: string } + | { kind: "hex_decode"; source: VarRef; into: string } + | { kind: "concat_bits"; sources: VarRef[]; into: string } + | { kind: "custom"; name: string }; + +export interface BitField { + name: string; + bitStart: number; + bitEnd: number; +} + +export type ParseBlock = + | { kind: "steps"; steps: ParseStep[] } + | { kind: "custom"; name: string }; + +export type DecodeCall = + | { + fn: + | "coordinate" + | "coordinate_decimal_minutes" + | "integer" + | "float" + | "string" + | "trim" + | "uppercase" + | "lowercase" + | "timestamp_hhmmss" + | "timestamp_ddhhmm" + | "hex_to_bytes" + | "callsign" + | "airport" + | "tail_number" + | "flight_number" + | "altitude_feet" + | "speed_knots" + | "heading_degrees" + | "fuel_kg" + | "fuel_lb" + | "json_parse"; + args: Record; + } + | { fn: "custom"; name: string; args: Record }; + +export interface FieldIR { + name: string; + from: ValueExpr; + decode?: DecodeCall; + when?: Condition; + default?: ValueExpr; + description?: string; +} + +export type Condition = + | { kind: "equals"; left: ValueExpr; right: ValueExpr } + | { kind: "not_equal"; left: ValueExpr; right: ValueExpr } + | { kind: "matches"; var: VarRef; regex: string } + | { kind: "in"; value: ValueExpr; values: Array } + | { kind: "all"; conds: Condition[] } + | { kind: "any"; conds: Condition[] } + | { kind: "not"; cond: Condition }; + +export interface VariantIR { + name?: string; + when?: Condition; + isDefault: boolean; + defaultAction?: "fail"; + fields?: FieldIR[]; + checksum?: ChecksumAlgorithmIR[]; +} + +export interface ChecksumAlgorithmIR { + algorithm: + | "ARINC_665_CRC32" + | "ARINC_6_CRC16" + | "CRC16_IBM_SDLC_REV" + | "CRC16_GENIBUS"; + when?: Condition; + on: { startByte: number; endByte: number }; + expect: { kind: "tail" | "head"; n: number } | { kind: "range"; startByte: number; endByte: number }; +} + +export interface FormatterCall { + type: + | "position" + | "altitude" + | "speed" + | "heading" + | "timestamp" + | "callsign" + | "flight_number" + | "tail_number" + | "airport_origin" + | "airport_destination" + | "fuel" + | "free_text" + | "custom"; + customName?: string; + args: Record; +} + +export type FormattedIR = + | { kind: "structured"; description: string; items: FormatterCall[] } + | { kind: "custom"; name: string; description: string }; + +export interface SpecIR { + specVersion: SpecVersion; + plugin: PluginIR; + qualifiers: QualifiersIR; + parse: ParseBlock; + fields?: FieldIR[]; + variants?: VariantIR[]; + checksum?: ChecksumAlgorithmIR[]; + onChecksumFail?: { decoded: boolean }; + formatted: FormattedIR; + sourcePath: string; +} diff --git a/codegen/src/parse-spec.ts b/codegen/src/parse-spec.ts new file mode 100644 index 0000000..aa9c515 --- /dev/null +++ b/codegen/src/parse-spec.ts @@ -0,0 +1,275 @@ +import { readFileSync } from "node:fs"; +import { parse as parseYaml } from "yaml"; +import { validateSpec } from "./validate.js"; +import type { + BitField, + ChecksumAlgorithmIR, + Condition, + DecodeCall, + FieldIR, + FormattedIR, + FormatterCall, + ParseBlock, + ParseStep, + SpecIR, + ValueExpr, + VariantIR, +} from "./ir.js"; + +/** + * Loads a single spec YAML file, validates against schema, and lowers to IR. + */ +export function loadSpec(path: string): SpecIR { + const raw = readFileSync(path, "utf8"); + const parsed = parseYaml(raw); + validateSpec(parsed, path); + return lowerToIR(parsed, path); +} + +function lowerToIR(spec: any, sourcePath: string): SpecIR { + return { + specVersion: spec.spec_version, + plugin: { + name: spec.plugin.name, + type: spec.plugin.type, + docs: spec.plugin.docs, + decodeLevel: spec.plugin.decode_level ?? "MESSAGE", + }, + qualifiers: { + labels: spec.qualifiers.labels, + preambles: spec.qualifiers.preambles, + }, + parse: lowerParse(spec.parse), + fields: spec.fields?.map(lowerField), + variants: spec.variants?.map(lowerVariant), + checksum: spec.checksum?.map(lowerChecksum), + onChecksumFail: spec.on_checksum_fail, + formatted: lowerFormatted(spec.formatted), + sourcePath, + }; +} + +function lowerParse(parse: any): ParseBlock { + if (parse && typeof parse === "object" && "custom" in parse && !Array.isArray(parse)) { + return { kind: "custom", name: parse.custom }; + } + if (!Array.isArray(parse)) { + throw new Error(`Invalid parse block: expected array or {custom}, got ${typeof parse}`); + } + return { kind: "steps", steps: parse.map(lowerStep) }; +} + +function lowerStep(step: any): ParseStep { + const into: string | undefined = step.into; + if ("split" in step) { + if (!into) throw new Error("split step requires 'into'"); + return { kind: "split", delimiter: step.split, into }; + } + if ("regex" in step) { + if (!into) throw new Error("regex step requires 'into'"); + return { kind: "regex", pattern: step.regex, on: step.on, into }; + } + if ("substring" in step) { + if (!into) throw new Error("substring step requires 'into'"); + return { + kind: "substring", + from: step.substring.from, + start: step.substring.start, + length: step.substring.length, + end: step.substring.end, + into, + }; + } + if ("require_length" in step) { + if (step.else !== "fail") { + throw new Error("require_length currently requires 'else: fail'"); + } + return { + kind: "require_length", + var: step.require_length.var, + equals: step.require_length.equals, + min: step.require_length.min, + max: step.require_length.max, + onFail: "fail", + }; + } + if ("bitfield" in step) { + return { + kind: "bitfield", + source: step.bitfield.source, + fields: step.bitfield.fields.map(lowerBitField), + }; + } + if ("decode_ascii85" in step) { + if (!into) throw new Error("decode_ascii85 step requires 'into'"); + return { kind: "decode_ascii85", source: step.decode_ascii85, into }; + } + if ("deflate" in step) { + if (!into) throw new Error("deflate step requires 'into'"); + return { + kind: "deflate", + source: step.deflate.source, + offset: step.deflate.offset, + format: step.deflate.format ?? "raw", + into, + }; + } + if ("base64" in step) { + if (!into) throw new Error("base64 step requires 'into'"); + return { kind: "base64", source: step.base64, into }; + } + if ("text_decode" in step) { + if (!into) throw new Error("text_decode step requires 'into'"); + return { + kind: "text_decode", + source: step.text_decode.source, + encoding: step.text_decode.encoding ?? "utf-8", + into, + }; + } + if ("hex_decode" in step) { + if (!into) throw new Error("hex_decode step requires 'into'"); + return { kind: "hex_decode", source: step.hex_decode, into }; + } + if ("concat_bits" in step) { + if (!into) throw new Error("concat_bits step requires 'into'"); + return { kind: "concat_bits", sources: step.concat_bits, into }; + } + if ("custom" in step) { + return { kind: "custom", name: step.custom }; + } + throw new Error(`Unknown parse step: ${JSON.stringify(step)}`); +} + +function lowerBitField(bf: any): BitField { + const [start, end] = bf.bits.split(":").map((s: string) => Number(s)); + return { name: bf.name, bitStart: start, bitEnd: end }; +} + +function lowerField(field: any): FieldIR { + return { + name: field.name, + from: lowerExpr(field.from), + decode: field.decode ? lowerDecode(field.decode) : undefined, + when: field.when ? lowerCondition(field.when) : undefined, + default: field.default !== undefined ? lowerExpr(field.default) : undefined, + description: field.description, + }; +} + +function lowerExpr(expr: unknown): ValueExpr { + if (typeof expr === "string" && expr.startsWith("$")) { + return { kind: "var", ref: expr }; + } + if (typeof expr === "object" && expr !== null) { + if ("length" in expr && typeof (expr as any).length === "string") { + return { kind: "call", fn: "length", arg: (expr as any).length }; + } + } + if ( + typeof expr === "string" || + typeof expr === "number" || + typeof expr === "boolean" || + expr === null + ) { + return { kind: "literal", value: expr as string | number | boolean | null }; + } + throw new Error(`Unsupported value expression: ${JSON.stringify(expr)}`); +} + +function lowerDecode(decode: any): DecodeCall { + if (decode.fn === "custom") { + return { fn: "custom", name: decode.custom, args: decode.args ?? {} }; + } + return { fn: decode.fn, args: decode.args ?? {} }; +} + +function lowerCondition(cond: any): Condition { + const keys = Object.keys(cond); + if (keys.length !== 1) { + throw new Error(`Condition must have exactly one key, got: ${keys.join(", ")}`); + } + const key = keys[0]!; + switch (key) { + case "equals": + return { kind: "equals", left: lowerExpr(cond.equals[0]), right: lowerExpr(cond.equals[1]) }; + case "not_equal": + return { + kind: "not_equal", + left: lowerExpr(cond.not_equal[0]), + right: lowerExpr(cond.not_equal[1]), + }; + case "matches": + return { kind: "matches", var: cond.matches[0], regex: cond.matches[1] }; + case "in": + return { + kind: "in", + value: lowerExpr(cond.in[0]), + values: cond.in[1], + }; + case "all": + return { kind: "all", conds: cond.all.map(lowerCondition) }; + case "any": + return { kind: "any", conds: cond.any.map(lowerCondition) }; + case "not": + return { kind: "not", cond: lowerCondition(cond.not) }; + default: + throw new Error(`Unknown condition kind: ${key}`); + } +} + +function lowerVariant(variant: any): VariantIR { + if ("default" in variant) { + return { isDefault: true, defaultAction: variant.default }; + } + return { + isDefault: false, + name: variant.name, + when: lowerCondition(variant.when), + fields: variant.fields?.map(lowerField), + checksum: variant.checksum?.map(lowerChecksum), + }; +} + +function lowerChecksum(c: any): ChecksumAlgorithmIR { + const [s, e] = c.on.split("..").map((n: string) => Number(n)); + let expect: ChecksumAlgorithmIR["expect"]; + if (c.expect.startsWith("tail")) { + expect = { kind: "tail", n: Number(c.expect.slice(4)) }; + } else if (c.expect.startsWith("head")) { + expect = { kind: "head", n: Number(c.expect.slice(4)) }; + } else { + const [rs, re] = c.expect.split("..").map((n: string) => Number(n)); + expect = { kind: "range", startByte: rs, endByte: re }; + } + return { + algorithm: c.algorithm, + when: c.when ? lowerCondition(c.when) : undefined, + on: { startByte: s, endByte: e }, + expect, + }; +} + +function lowerFormatted(formatted: any): FormattedIR { + if ("custom" in formatted) { + return { + kind: "custom", + name: formatted.custom, + description: formatted.description ?? "Custom", + }; + } + return { + kind: "structured", + description: formatted.description, + items: (formatted.items ?? []).map(lowerFormatterCall), + }; +} + +function lowerFormatterCall(item: any): FormatterCall { + const { type, custom, ...rest } = item; + return { + type, + customName: custom, + args: rest, + }; +} diff --git a/codegen/src/validate.ts b/codegen/src/validate.ts new file mode 100644 index 0000000..9033cc0 --- /dev/null +++ b/codegen/src/validate.ts @@ -0,0 +1,57 @@ +import type { ErrorObject, ValidateFunction } from "ajv"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; +import { createRequire } from "node:module"; + +// ajv 8 / ajv-formats are CJS packages; use createRequire for clean interop +// rather than wrestling with default-pluck patterns under NodeNext. +const require = createRequire(import.meta.url); +const Ajv: new (opts?: object) => { compile: (s: unknown) => ValidateFunction } = + require("ajv/dist/2020"); +const addFormats: (ajv: object) => void = require("ajv-formats"); + +const here = dirname(fileURLToPath(import.meta.url)); + +let cachedValidator: ValidateFunction | undefined; + +function loadSchema(): ValidateFunction { + if (cachedValidator) return cachedValidator; + const schemaPath = resolve(here, "..", "..", "schema", "ads-v1.schema.json"); + const schema = JSON.parse(readFileSync(schemaPath, "utf8")); + const ajv = new Ajv({ allErrors: true, strict: false }); + addFormats(ajv); + const compiled = ajv.compile(schema); + cachedValidator = compiled; + return compiled; +} + +export interface ValidationFailure { + path: string; + errors: ErrorObject[]; +} + +export class ValidationError extends Error { + constructor(public readonly failure: ValidationFailure) { + super(formatErrors(failure)); + this.name = "ValidationError"; + } +} + +function formatErrors(failure: ValidationFailure): string { + const header = `Spec failed JSON Schema validation: ${failure.path}`; + const lines = failure.errors.map((e) => ` - ${e.instancePath || "/"} ${e.message ?? ""}`); + return [header, ...lines].join("\n"); +} + +/** + * Validates a parsed spec object against ads-v1.schema.json. Throws `ValidationError` + * on failure. Path is included in the error for human-friendly messages. + */ +export function validateSpec(spec: unknown, path: string): void { + const validator = loadSchema(); + const ok = validator(spec); + if (!ok) { + throw new ValidationError({ path, errors: validator.errors ?? [] }); + } +} diff --git a/codegen/tsconfig.json b/codegen/tsconfig.json new file mode 100644 index 0000000..5116176 --- /dev/null +++ b/codegen/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["dist", "node_modules", "tests"] +} diff --git a/codegen/vitest.config.ts b/codegen/vitest.config.ts new file mode 100644 index 0000000..7a16b27 --- /dev/null +++ b/codegen/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["tests/**/*.test.ts"], + coverage: { + provider: "v8", + reporter: ["text", "html"], + include: ["src/**/*.ts"], + }, + }, +}); diff --git a/corpus/.gitkeep b/corpus/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/corpus/README.md b/corpus/README.md new file mode 100644 index 0000000..37b4cf9 --- /dev/null +++ b/corpus/README.md @@ -0,0 +1,46 @@ +# Shared test corpus + +Golden input/expected pairs used by **every** language implementation. The corpus is the cross-language source of truth — if TS, Rust, and C disagree on a sample, the corpus says who is right. + +## Layout + +Mirrors `spec/`: + +``` +corpus/ +├── labels/10/POS/sample-001.json +├── labels/10/POS/sample-002-invalid.json +├── labels/44/POS/sample-001.json +├── wildcards/arinc_702/sample-001.json +└── ... +``` + +## Format + +```json +{ + "spec": "labels/10/POS", + "source": "acars-decoder-typescript/lib/plugins/Label_10_POS.test.ts", + "description": "human-readable test case description", + "input": { + "label": "10", + "text": "POS082150, N 3885,..." + }, + "expected": { + "decoded": true, + "decoder": { "name": "label-10-pos", "type": "pattern-match", "decodeLevel": "partial" }, + "formatted": { "description": "Position Report", "items": [ ... ] }, + "remaining": { "text": "..." } + } +} +``` + +## Extracting from TS tests + +`acars-decoder-typescript`'s `lib/plugins/*.test.ts` files are the source of truth. A small extractor script (planned, `codegen/scripts/extract-corpus.ts`) instruments the Jest run to dump `(input, decodeResult)` pairs to JSON. Manually-curated samples are also welcome and should set `source: "manual"`. + +## Validation + +Each language repo's CI loads every JSON file, runs the decoder against `input`, and deep-equals against `expected`. Divergence fails CI in that repo. + +A reference TypeScript interpreter (planned, `codegen/src/interpret.ts`) walks the spec IR directly and runs the corpus — this validates that specs are decodable before any language repo adopts them. diff --git a/corpus/labels/10/POS/sample-001.json b/corpus/labels/10/POS/sample-001.json new file mode 100644 index 0000000..c5108be --- /dev/null +++ b/corpus/labels/10/POS/sample-001.json @@ -0,0 +1,37 @@ +{ + "spec": "labels/10/POS", + "source": "acars-decoder-typescript/lib/plugins/Label_10_POS.test.ts", + "description": "Label 10 Preamble POS variant 1 — successful decode with partial level", + "input": { + "label": "10", + "text": "POS082150, N 3885,W 7841,---,308,26922, 51,22290, 529, 19,-225,6" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-10-pos", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "ARP", + "label": "Aircraft Position", + "value": "38.850 N, 78.410 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "22290 feet" + } + ] + }, + "remaining": { + "text": "POS082150,---,308,26922, 51, 529, 19,-225,6" + } + } +} diff --git a/corpus/labels/10/POS/sample-002-invalid.json b/corpus/labels/10/POS/sample-002-invalid.json new file mode 100644 index 0000000..a986602 --- /dev/null +++ b/corpus/labels/10/POS/sample-002-invalid.json @@ -0,0 +1,20 @@ +{ + "spec": "labels/10/POS", + "source": "acars-decoder-typescript/lib/plugins/Label_10_POS.test.ts", + "description": "Label 10 invalid input — fails decode with 'none' level", + "input": { + "label": "10", + "text": "POS Bogus Message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-10-pos", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Position Report" + } + } +} diff --git a/docs/ADOPTION.md b/docs/ADOPTION.md new file mode 100644 index 0000000..d7c6946 --- /dev/null +++ b/docs/ADOPTION.md @@ -0,0 +1,133 @@ +# ADS adoption guide + +How each language repo consumes the central `airframes-decoder` repo. + +## Common shape + +Every language repo: + +1. Adds `airframes-decoder` as a git submodule at `vendor/airframes-decoder/`. +2. Imports the language's runtime helpers from `vendor/airframes-decoder/runtimes//`. +3. Runs `ads-gen --target ` at build time to regenerate plugin source from spec YAML. +4. Commits generated files (CI verifies they are up to date). +5. Runs the shared corpus (`vendor/airframes-decoder/corpus/`) as part of its test suite. + +## `acars-decoder-typescript` + +```bash +git submodule add https://github.com/airframesio/airframes-decoder vendor/airframes-decoder +``` + +`package.json` — add to scripts: + +```json +{ + "scripts": { + "ads:generate": "node vendor/airframes-decoder/codegen/dist/cli.js generate --target ts --spec vendor/airframes-decoder/spec --out lib/plugins/generated", + "ads:check": "node vendor/airframes-decoder/codegen/dist/cli.js generate --target ts --spec vendor/airframes-decoder/spec --out lib/plugins/generated --check", + "test:corpus": "vitest run tests/corpus.test.ts" + } +} +``` + +`tsconfig.json` — add path aliases so generated code can `import` runtime helpers via short names: + +```json +{ + "compilerOptions": { + "paths": { + "@airframes/ads-runtime-ts": ["vendor/airframes-decoder/runtimes/typescript/index.ts"], + "@airframes/ads-runtime-ts/helpers": ["vendor/airframes-decoder/runtimes/typescript/helpers.ts"] + } + } +} +``` + +Replace the existing `lib/utils/*.ts` imports throughout `lib/plugins/` with imports from the runtime path. Stage 2 PR. + +## `acars-decoder-rust` + +```bash +git submodule add https://github.com/airframesio/airframes-decoder vendor/airframes-decoder +``` + +`Cargo.toml`: + +```toml +[dependencies] +ads-runtime = { path = "vendor/airframes-decoder/runtimes/rust" } +``` + +`build.rs`: + +```rust +use std::process::Command; +fn main() { + println!("cargo:rerun-if-changed=vendor/airframes-decoder/spec"); + let status = Command::new("node") + .args(["vendor/airframes-decoder/codegen/dist/cli.js", + "generate", "--target", "rust", + "--spec", "vendor/airframes-decoder/spec", + "--out", "src/plugins/generated"]) + .status() + .expect("ads-gen failed"); + assert!(status.success(), "ads-gen exited non-zero"); +} +``` + +Integration tests under `tests/corpus.rs` walk `vendor/airframes-decoder/corpus/` and assert deep-equal output. + +## `acars-decoder-c` + +CMake `CMakeLists.txt`: + +```cmake +add_subdirectory(vendor/airframes-decoder/runtimes/c) +target_link_libraries(acars_decoder PRIVATE ads_runtime_c cjson m) + +add_custom_command( + OUTPUT ${GENERATED_C_FILES} + COMMAND node ${CMAKE_SOURCE_DIR}/vendor/airframes-decoder/codegen/dist/cli.js + generate --target c + --spec ${CMAKE_SOURCE_DIR}/vendor/airframes-decoder/spec + --out ${CMAKE_SOURCE_DIR}/src/plugins/generated + DEPENDS ${SPEC_YAML_FILES} +) +``` + +Corpus test binary under `tests/corpus_test.c`. + +## Submodule contributor workflow + +```bash +git clone --recurse-submodules https://github.com/airframesio/ +# or, after an initial clone: +git submodule update --init --recursive +``` + +To bump the pinned ADS version: + +```bash +cd vendor/airframes-decoder && git fetch && git checkout && cd ../.. +git add vendor/airframes-decoder +git commit -m "ADS: bump to " +``` + +The next `ads-gen` run picks up any spec changes. + +## CI parity + +All three language repos call the same reusable workflows from `airframes-decoder/.github/workflows/`: + +```yaml +# .github/workflows/ci.yml +jobs: + codegen-up-to-date: + uses: airframesio/airframes-decoder/.github/workflows/codegen-check.yml@main + corpus: + uses: airframesio/airframes-decoder/.github/workflows/corpus-test.yml@main + with: + language: rust +``` + +See `airframes-decoder/.github/workflows/` for the workflow contracts. diff --git a/docs/DSL.md b/docs/DSL.md new file mode 100644 index 0000000..85281ee --- /dev/null +++ b/docs/DSL.md @@ -0,0 +1,245 @@ +# Airframes Decoder Spec (ADS) — DSL Reference + +ADS v1 is a YAML DSL for describing ACARS message decoders. One YAML file per plugin. Validated by `schema/ads-v1.schema.json`. Consumed by `@airframes/ads-codegen`, which emits idiomatic plugin source for TypeScript, Rust, and C (with WASM, Go, Python, etc. straightforward to add). + +**Authoritative reference behavior:** `acars-decoder-typescript` is in production and is the source of truth. Every spec must produce byte-for-byte identical output when re-emitted as TS and run against the same input. `acars-message-documentation` is a secondary reference for filling gaps where TS code is incomplete. + +## File layout + +``` +spec/ +├── labels/ # one directory or file per label +│ ├── 10/POS.yaml # label "10", preamble "POS" +│ ├── 44/POS.yaml +│ ├── H1/OHMA.yaml +│ └── 4A.yaml # single-file plugin (no preamble distinction) +├── wildcards/ # plugins where qualifier label is "*" +│ └── arinc_702.yaml +└── shared/ # spec data emitted into runtimes (CRC tables, etc.) + └── crc_tables.yaml +``` + +Filenames beginning with `_` are excluded from codegen (use for partials/includes). + +## Top-level schema + +Every spec file has six keys (one optional pair): + +```yaml +spec_version: "1" +plugin: { ... } # plugin metadata +qualifiers: { ... } # which messages this plugin matches +parse: [ ... ] # parse steps OR { custom: } +fields: [ ... ] # XOR with variants +variants: [ ... ] # XOR with fields +checksum: [ ... ] # optional +formatted: { ... } # output items +``` + +### `plugin` + +```yaml +plugin: + name: Label_10_POS # PascalCase. Used verbatim as TS class name. + type: text # text | binary + docs: https://... # optional URL to research docs + decode_level: PARTIAL # NONE | PARTIAL | MESSAGE | FULL (default MESSAGE) +``` + +### `qualifiers` + +```yaml +qualifiers: + labels: ["10"] # required; "*" is wildcard + preambles: ["POS"] # optional +``` + +## Variable references + +Strings beginning with `$` are variable references resolved at codegen time: + +| Reference | Meaning | +|--------------------------|------------------------------------------| +| `$message.text` | The raw message text | +| `$parts` | A variable defined by an earlier step | +| `$parts[1]` | Index into an array variable | +| `$m.unsplit_coords` | A named regex capture group | + +When a `$var[N]` appears inside a YAML flow sequence (`[...]`), quote it (`"$parts[1]"`) to prevent YAML from parsing the `[` as a nested sequence start. + +## `parse` block + +Either a list of steps OR a single escape hatch: + +```yaml +parse: + custom: arinc_702_dispatch # full delegate to per-language native function +``` + +Or: + +```yaml +parse: + - { split: ",", into: parts } + - { require_length: { var: parts, equals: 12 }, else: fail } + - regex: "^(?...),(?...)$" + on: $message.text + into: m +``` + +### Step kinds + +| Step | Purpose | +|-------------------|--------------------------------------------------| +| `split` | `String.split(delimiter)` → array | +| `regex` | Match with named capture groups → object | +| `substring` | Extract by start/length or start/end | +| `require_length` | Assert array length; on fail → `failUnknown` | +| `bitfield` | Extract bit ranges from a byte/byte-array source | +| `decode_ascii85` | ASCII85 → bytes | +| `deflate` | inflate (raw/zlib/gzip) bytes → bytes | +| `base64` | base64 → bytes | +| `text_decode` | bytes → string (utf-8 / ascii / latin1) | +| `hex_decode` | hex string → bytes | +| `concat_bits` | join multiple bit slices into a single value | +| `custom` | per-language native helper | + +Steps that produce a value require an `into: ` key. Variables become available to subsequent steps and to `fields`/`variants`/`formatted`. + +## `fields` and `variants` + +`fields` is a flat list; `variants` is a branched list — they are mutually exclusive at the top level. + +### Field + +```yaml +- name: latitude + from: $parts[1] # source expression + decode: + fn: coordinate # named decode helper (see below) + args: { ... } + when: { ... } # optional condition; field assigned only if true + default: ... # optional fallback value +``` + +### Variant + +```yaml +variants: + - name: format_11 + when: { equals: [{ length: $fields }, 11] } + fields: [ ... ] + - name: format_6 + when: + all: + - { equals: [{ length: $fields }, 6] } + - { matches: ["$fields[0]", "^[NS]"] } + fields: [ ... ] + - default: fail # fallthrough if no when matches +``` + +## Decode functions (`decode.fn`) + +Canonical set defined in `spec/shared/decode_fns.yaml` and implemented in `runtimes//`. Use `fn: custom, custom: ` for plugin-specific logic. + +| `fn` | Purpose | +|-------------------------------|--------------------------------------------------| +| `coordinate` | Single-axis or combined lat/lon | +| `coordinate_decimal_minutes` | DDMM.M format | +| `integer`, `float` | Numeric parsing (with optional substring args) | +| `string`, `trim`, `uppercase`, `lowercase` | String transforms | +| `timestamp_hhmmss` | HHMMSS → seconds since midnight | +| `timestamp_ddhhmm` | DDHHMM → epoch-style | +| `callsign`, `tail_number`, `flight_number`, `airport` | Identifier normalization | +| `altitude_feet`, `speed_knots`, `heading_degrees`, `fuel_kg`, `fuel_lb` | Typed numerics | +| `hex_to_bytes`, `json_parse` | Format conversions | +| `custom` | per-language native function (`custom: `) | + +## Conditions + +```yaml +{ equals: [a, b] } +{ not_equal: [a, b] } +{ matches: ["$var", "regex"] } # JS-flavor regex applied to the variable value +{ in: ["$var", [v1, v2, ...]] } +{ all: [cond1, cond2, ...] } +{ any: [cond1, cond2, ...] } +{ not: cond } +``` + +Computed sub-expression: `{ length: $var }` returns the length of an array or string variable. + +## `checksum` + +```yaml +checksum: + - algorithm: ARINC_665_CRC32 + when: { equals: [$version, 1] } + on: "0..-4" # byte range slice (inclusive start, exclusive end; negative from end) + expect: tail4 # last 4 bytes hold the expected value +on_checksum_fail: + decoded: false # what to do on mismatch +``` + +## `formatted` + +Either structured items or a per-language escape hatch: + +```yaml +formatted: + description: "Position Report" + items: + - { type: position, latitude: $latitude, longitude: $longitude } + - { type: altitude, value: $altitude } +``` + +Or: + +```yaml +formatted: + description: "Latest New Format" # human description still required + custom: label_4a_format # function in runtimes//escape_hatches/ +``` + +Available `type`s: `position`, `altitude`, `speed`, `heading`, `timestamp`, `callsign`, `flight_number`, `tail_number`, `airport_origin`, `airport_destination`, `fuel`, `free_text`, `custom`. + +## Escape hatches + +Reserved for the ~5% of plugins whose logic resists declarative spec (ARINC 702 heuristic offsets, CBand recursion, MIAM CRC version dispatch, complex variant slicing). + +```yaml +parse: + custom: arinc_702_dispatch # at the parse level +``` + +```yaml +- name: pos + from: $fields + decode: { fn: custom, custom: label_4a_variant_2_decode } # at the field level +``` + +```yaml +formatted: + description: "OHMA Message" + custom: ohma_message_format # at the formatter level +``` + +Each language ships a function by that exact name in `runtimes//escape_hatches/`. The function receives the plugin instance, message, result-so-far, and options. See [ESCAPE_HATCHES.md](ESCAPE_HATCHES.md) for the per-language conventions. + +## Validating a spec + +```bash +cd codegen +npm run build +node dist/cli.js validate --spec ../spec +``` + +## Generating code + +```bash +node dist/cli.js generate --target ts --spec ../spec --out ../runtimes/typescript/generated +node dist/cli.js generate --target rust --spec ../spec --out ../runtimes/rust/src/generated +node dist/cli.js generate --target c --spec ../spec --out ../runtimes/c/src/generated +``` + +`--check` validates and lists targets without writing files. diff --git a/docs/ESCAPE_HATCHES.md b/docs/ESCAPE_HATCHES.md new file mode 100644 index 0000000..520d2d4 --- /dev/null +++ b/docs/ESCAPE_HATCHES.md @@ -0,0 +1,118 @@ +# Escape hatches + +For the ~5% of ACARS plugins whose logic resists clean declarative spec — heuristic offset computation, recursive composition (CBand → MessageDecoder), version-dependent CRC dispatch, complex variant slicing — ADS provides escape hatches at three levels. + +## When to use one + +Use an escape hatch when adding new DSL primitives would be more complex than the logic itself. The threshold is roughly: + +- More than ~5 lines of branching logic that can't be expressed with `variants` + `when` +- Computed offsets that depend on parsed-so-far content (not just message structure) +- Recursive invocations of the message decoder +- Format-specific algorithms (CRC variants, base85 dialects) that warrant a single canonical implementation + +If you find yourself wanting more than one escape hatch per spec, consider whether the DSL needs another primitive. + +## Levels + +### Whole-plugin + +```yaml +parse: + custom: arinc_702_dispatch +formatted: + description: "ARINC 702 Message" + custom: arinc_702_format +``` + +The entire decode flow runs in a hand-written function. Generated code is a one-liner that delegates. + +### Field-level + +```yaml +- name: variant_result + from: $fields + decode: + fn: custom + custom: label_4a_variant_2_decode +``` + +The value of `from` plus the field's args is passed to the named function; its return becomes the field value. + +### Formatter-level + +```yaml +formatted: + description: "OHMA Message" + items: + - { type: position, ... } + - { type: custom, custom: ohma_payload_item } +``` + +Single formatter item dispatched to a hand-written function. + +## Per-language conventions + +Hand-written functions live in `runtimes//escape_hatches/`. They MUST exist with the exact name declared in the spec (codegen does not check this — language build does). + +### TypeScript + +```ts +// runtimes/typescript/escape_hatches/arinc_702.ts +import type { DecoderPlugin, DecodeResult, Message, Options } from "../index.js"; + +export function arinc_702_dispatch( + plugin: DecoderPlugin, + message: Message, + result: DecodeResult, + options: Options, +): DecodeResult { + // hand-written port of acars-decoder-typescript/lib/plugins/ARINC_702.ts +} +``` + +### Rust + +```rust +// runtimes/rust/src/escape_hatches/arinc_702.rs +use crate::{DecodeResult, Message, Options, Plugin}; + +pub fn arinc_702_dispatch( + plugin: &dyn Plugin, + message: &Message, + result: &mut DecodeResult, + options: &Options, +) -> DecodeResult { /* ... */ } +``` + +### C + +```c +/* runtimes/c/src/escape_hatches/arinc_702.c */ +#include "ads_runtime.h" + +DecodeResult *arinc_702_dispatch( + const Plugin *plugin, + const Message *message, + DecodeResult *result, + const Options *options +); +``` + +## Current escape hatch inventory + +These survive declarative description as of v1: + +| Hatch | Plugin | Why | +|----------------------------------------|------------------|------------------------------------------------| +| `arinc_702_dispatch` | ARINC_702 | Heuristic offset computation, fallback chain | +| `arinc_702_format` | ARINC_702 | Result merging from recursive helper | +| `label_4a_variant_2_decode` | Label_4A | Substring slicing of fields[0] and fields[1] | +| `label_4a_variant_3_position` | Label_4A | Concat + sanitize fields[4]+fields[5] | +| `label_4a_format` | Label_4A | Variant-specific item ordering | +| `ohma_unwrap_message` | Label_H1_OHMA | JSON-in-JSON unwrap (try/catch chains) | +| `ohma_message_item` | Label_H1_OHMA | Pretty-print + fallback | +| `parse_flight_level_or_ground` | Label_44_POS | "GRD"/"***" → 0 sentinel | +| `flight_level_to_altitude_feet` | Label_44_POS | flight_level * 100 | + +When porting a TS plugin reveals a needed hatch, file a follow-up to evaluate adding the equivalent DSL primitive in ADS v1.1. diff --git a/runtimes/c/.gitkeep b/runtimes/c/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/runtimes/rust/.gitkeep b/runtimes/rust/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/runtimes/typescript/.gitkeep b/runtimes/typescript/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/schema/ads-v1.schema.json b/schema/ads-v1.schema.json new file mode 100644 index 0000000..8e15031 --- /dev/null +++ b/schema/ads-v1.schema.json @@ -0,0 +1,478 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://airframes.io/spec/ads-v1.schema.json", + "title": "Airframes Decoder Spec (ADS) v1", + "description": "Declarative DSL for ACARS message decoders. Codegens to TypeScript, Rust, and C.", + "type": "object", + "required": ["spec_version", "plugin", "qualifiers", "parse", "formatted"], + "additionalProperties": false, + "properties": { + "spec_version": { + "const": "1", + "description": "ADS spec version. Must be \"1\"." + }, + "plugin": { "$ref": "#/$defs/plugin" }, + "qualifiers": { "$ref": "#/$defs/qualifiers" }, + "parse": { "$ref": "#/$defs/parse" }, + "fields": { + "type": "array", + "items": { "$ref": "#/$defs/field" }, + "description": "Field extractions. Mutually exclusive with top-level 'variants'." + }, + "variants": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/variant" }, + "description": "Variant dispatch. Each variant has a 'when' condition and its own field list." + }, + "checksum": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/checksum_algorithm" } + }, + "on_checksum_fail": { + "type": "object", + "additionalProperties": false, + "properties": { + "decoded": { "type": "boolean" } + } + }, + "formatted": { "$ref": "#/$defs/formatted" } + }, + "allOf": [ + { + "description": "Top-level fields and variants are mutually exclusive.", + "not": { "required": ["fields", "variants"] } + } + ], + + "$defs": { + "identifier": { + "type": "string", + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$", + "description": "Plugin or variable identifier (snake_case or PascalCase)." + }, + "var_ref": { + "type": "string", + "pattern": "^\\$[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*|\\[[0-9-]+(:[0-9-]+(\\.\\.[0-9-]+)?)?\\])*$", + "description": "A variable reference: $varname, $varname.field, $varname[N], $varname[N:M], $varname[N:M..K]. Bit-slice notation $byte[bitStart..bitEnd] permitted in bitfield contexts." + }, + "value_expr": { + "anyOf": [ + { "$ref": "#/$defs/var_ref" }, + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" }, + { "type": "null" } + ], + "description": "A literal value or a variable reference. Strings starting with $ are interpreted as variable refs at codegen time." + }, + + "plugin": { + "type": "object", + "required": ["name", "type"], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "pattern": "^[A-Z][A-Za-z0-9_]*$", + "description": "Plugin class name (PascalCase). Used verbatim in TS, snake_cased in Rust/C." + }, + "type": { + "enum": ["text", "binary"], + "description": "text = string-mode parsing (split/regex); binary = byte-mode parsing (bitfields, encodings)." + }, + "docs": { + "type": "string", + "format": "uri", + "description": "Optional URL to human-readable research docs (typically acars-message-documentation)." + }, + "decode_level": { + "enum": ["NONE", "PARTIAL", "MESSAGE", "FULL"], + "default": "MESSAGE", + "description": "Decode-level enum matching DecoderPluginInterface.ts." + } + } + }, + + "qualifiers": { + "type": "object", + "required": ["labels"], + "additionalProperties": false, + "properties": { + "labels": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "pattern": "^([A-Za-z0-9_]+|\\*)$" + }, + "description": "ACARS labels to match. '*' is wildcard." + }, + "preambles": { + "type": "array", + "items": { "type": "string" }, + "description": "Optional preamble strings; first match wins." + } + } + }, + + "parse": { + "oneOf": [ + { "$ref": "#/$defs/parse_custom" }, + { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/parse_step" } + } + ] + }, + + "parse_custom": { + "type": "object", + "required": ["custom"], + "additionalProperties": false, + "properties": { + "custom": { + "$ref": "#/$defs/identifier", + "description": "Name of a per-language native function that performs the entire parse. Escape hatch." + } + } + }, + + "parse_step": { + "type": "object", + "minProperties": 1, + "maxProperties": 4, + "oneOf": [ + { "required": ["split"] }, + { "required": ["regex"] }, + { "required": ["substring"] }, + { "required": ["require_length"] }, + { "required": ["bitfield"] }, + { "required": ["decode_ascii85"] }, + { "required": ["deflate"] }, + { "required": ["base64"] }, + { "required": ["text_decode"] }, + { "required": ["hex_decode"] }, + { "required": ["concat_bits"] }, + { "required": ["custom"] } + ], + "properties": { + "split": { "type": "string", "minLength": 1 }, + "regex": { "type": "string", "minLength": 1 }, + "substring": { + "type": "object", + "required": ["from", "start"], + "additionalProperties": false, + "properties": { + "from": { "$ref": "#/$defs/var_ref" }, + "start": { "type": "integer" }, + "length": { "type": "integer", "minimum": 0 }, + "end": { "type": "integer" } + } + }, + "require_length": { + "type": "object", + "required": ["var"], + "additionalProperties": false, + "properties": { + "var": { "$ref": "#/$defs/identifier" }, + "equals": { "type": "integer", "minimum": 0 }, + "min": { "type": "integer", "minimum": 0 }, + "max": { "type": "integer", "minimum": 0 } + } + }, + "bitfield": { + "type": "object", + "required": ["source", "fields"], + "additionalProperties": false, + "properties": { + "source": { "$ref": "#/$defs/var_ref" }, + "fields": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["name", "bits"], + "additionalProperties": false, + "properties": { + "name": { "$ref": "#/$defs/identifier" }, + "bits": { + "type": "string", + "pattern": "^[0-9]+:[0-9]+$", + "description": "Bit range, inclusive on both ends (e.g. \"0:3\" = bits 0..3 = 4 bits)." + } + } + } + } + } + }, + "decode_ascii85": { "$ref": "#/$defs/var_ref" }, + "deflate": { + "type": "object", + "required": ["source"], + "additionalProperties": false, + "properties": { + "source": { "$ref": "#/$defs/var_ref" }, + "offset": { "type": "integer", "minimum": 0 }, + "format": { "enum": ["raw", "zlib", "gzip"], "default": "raw" } + } + }, + "base64": { "$ref": "#/$defs/var_ref" }, + "text_decode": { + "type": "object", + "required": ["source"], + "additionalProperties": false, + "properties": { + "source": { "$ref": "#/$defs/var_ref" }, + "encoding": { "enum": ["utf-8", "ascii", "latin1"], "default": "utf-8" } + } + }, + "hex_decode": { "$ref": "#/$defs/var_ref" }, + "concat_bits": { + "type": "array", + "minItems": 2, + "items": { "$ref": "#/$defs/var_ref" } + }, + "custom": { "$ref": "#/$defs/identifier" }, + "into": { "$ref": "#/$defs/identifier" }, + "else": { "enum": ["fail"] } + } + }, + + "field": { + "type": "object", + "required": ["name", "from"], + "additionalProperties": false, + "properties": { + "name": { "$ref": "#/$defs/identifier" }, + "from": { "$ref": "#/$defs/value_expr" }, + "decode": { "$ref": "#/$defs/decode_call" }, + "when": { "$ref": "#/$defs/condition" }, + "default": { "$ref": "#/$defs/value_expr" }, + "description": { "type": "string" } + } + }, + + "decode_call": { + "type": "object", + "required": ["fn"], + "additionalProperties": false, + "properties": { + "fn": { + "enum": [ + "coordinate", + "coordinate_decimal_minutes", + "integer", + "float", + "string", + "trim", + "uppercase", + "lowercase", + "timestamp_hhmmss", + "timestamp_ddhhmm", + "hex_to_bytes", + "callsign", + "airport", + "tail_number", + "flight_number", + "altitude_feet", + "speed_knots", + "heading_degrees", + "fuel_kg", + "fuel_lb", + "json_parse", + "custom" + ] + }, + "args": { "type": "object", "additionalProperties": true }, + "custom": { + "$ref": "#/$defs/identifier", + "description": "When fn=custom: name of a per-language helper function." + } + } + }, + + "variant": { + "type": "object", + "oneOf": [ + { + "required": ["when", "fields"], + "additionalProperties": false, + "properties": { + "when": { "$ref": "#/$defs/condition" }, + "fields": { "type": "array", "items": { "$ref": "#/$defs/field" } }, + "name": { "type": "string" }, + "checksum": { + "type": "array", + "items": { "$ref": "#/$defs/checksum_algorithm" } + } + } + }, + { + "required": ["default"], + "additionalProperties": false, + "properties": { + "default": { "enum": ["fail"] } + } + } + ] + }, + + "condition": { + "type": "object", + "minProperties": 1, + "maxProperties": 1, + "properties": { + "equals": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { "$ref": "#/$defs/value_expr_or_call" } + }, + "not_equal": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { "$ref": "#/$defs/value_expr_or_call" } + }, + "matches": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "prefixItems": [ + { "$ref": "#/$defs/var_ref" }, + { "type": "string" } + ], + "items": false + }, + "in": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "prefixItems": [ + { "$ref": "#/$defs/value_expr" }, + { "type": "array" } + ], + "items": false + }, + "all": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/condition" } + }, + "any": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/condition" } + }, + "not": { "$ref": "#/$defs/condition" } + } + }, + + "value_expr_or_call": { + "anyOf": [ + { "$ref": "#/$defs/value_expr" }, + { + "type": "object", + "description": "Computed expression — e.g. {length: $parts}", + "minProperties": 1, + "maxProperties": 1, + "properties": { + "length": { "$ref": "#/$defs/var_ref" } + }, + "additionalProperties": false + } + ] + }, + + "checksum_algorithm": { + "type": "object", + "required": ["algorithm", "on", "expect"], + "additionalProperties": false, + "properties": { + "algorithm": { + "enum": [ + "ARINC_665_CRC32", + "ARINC_6_CRC16", + "CRC16_IBM_SDLC_REV", + "CRC16_GENIBUS" + ] + }, + "when": { "$ref": "#/$defs/condition" }, + "on": { + "type": "string", + "pattern": "^-?[0-9]+\\.\\.-?[0-9]+$", + "description": "Byte range slice notation, e.g. \"0..-4\" = all bytes except last 4." + }, + "expect": { + "type": "string", + "pattern": "^(tail[0-9]+|head[0-9]+|[0-9]+\\.\\.-?[0-9]+)$", + "description": "Where the expected checksum value is stored." + } + } + }, + + "formatted": { + "oneOf": [ + { + "type": "object", + "required": ["description"], + "additionalProperties": false, + "properties": { + "description": { + "type": "string", + "description": "Human description, may interpolate $varname." + }, + "items": { + "type": "array", + "items": { "$ref": "#/$defs/formatter_call" } + } + } + }, + { + "type": "object", + "required": ["custom"], + "additionalProperties": false, + "properties": { + "description": { + "type": "string", + "description": "Human-readable description used in initResult() even when items are custom." + }, + "custom": { + "$ref": "#/$defs/identifier", + "description": "Escape hatch: per-language native formatter function." + } + } + } + ] + }, + + "formatter_call": { + "type": "object", + "required": ["type"], + "additionalProperties": true, + "properties": { + "type": { + "enum": [ + "position", + "altitude", + "speed", + "heading", + "timestamp", + "callsign", + "flight_number", + "tail_number", + "airport_origin", + "airport_destination", + "fuel", + "free_text", + "custom" + ] + }, + "custom": { "$ref": "#/$defs/identifier" } + } + } + } +} diff --git a/spec/.gitkeep b/spec/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/spec/labels/10/POS.yaml b/spec/labels/10/POS.yaml new file mode 100644 index 0000000..a9111a5 --- /dev/null +++ b/spec/labels/10/POS.yaml @@ -0,0 +1,44 @@ +spec_version: "1" +plugin: + name: Label_10_POS + type: text + docs: https://github.com/airframesio/acars-message-documentation/blob/main/research/10/POS.md + decode_level: PARTIAL +qualifiers: + labels: ["10"] + preambles: ["POS"] +parse: + - { split: ",", into: parts } + - { require_length: { var: parts, equals: 12 }, else: fail } +fields: + - name: latitude + from: $parts[1] + decode: + fn: coordinate + args: + style: single_axis + axis: latitude + prefix_chars: [N, S] + digits: 5 + divisor: 100 + - name: longitude + from: $parts[2] + decode: + fn: coordinate + args: + style: single_axis + axis: longitude + prefix_chars: [E, W] + digits: 5 + divisor: 100 + - name: altitude + from: $parts[7] + decode: { fn: integer } +formatted: + description: "Position Report" + items: + - { type: position, latitude: $latitude, longitude: $longitude } + - { type: altitude, value: $altitude } + - type: free_text + label: unknown + values: ["$parts[0]", "$parts[3]", "$parts[4]", "$parts[5]", "$parts[6]", "$parts[8]", "$parts[9]", "$parts[10]", "$parts[11]"] diff --git a/spec/labels/44/POS.yaml b/spec/labels/44/POS.yaml new file mode 100644 index 0000000..4bd6104 --- /dev/null +++ b/spec/labels/44/POS.yaml @@ -0,0 +1,68 @@ +spec_version: "1" +plugin: + name: Label_44_POS + type: text + docs: https://github.com/airframesio/acars-message-documentation/blob/main/research/44/POS.md + decode_level: FULL +qualifiers: + labels: ["44"] + preambles: ["00POS01", "00POS02", "00POS03", "POS01", "POS02", "POS03"] +parse: + - regex: "^.*,(?.*),(?.*),(?.*),(?.*),(?.*),(?.*),(?.*),(?.*)$" + on: $message.text + into: m +fields: + - name: position + from: $m.unsplit_coords + decode: + fn: coordinate_decimal_minutes + args: { style: combined, format: "NSDDMM_M_EWDDMM_M" } + + - name: flight_level_raw + from: $m.flight_level_or_ground + decode: + fn: custom + custom: parse_flight_level_or_ground + - name: altitude + from: $flight_level_raw + decode: + fn: custom + custom: flight_level_to_altitude_feet + + - name: month + from: $m.current_date + decode: { fn: integer, args: { substring_start: 0, substring_length: 2 } } + - name: day + from: $m.current_date + decode: { fn: integer, args: { substring_start: 2, substring_length: 2 } } + + - name: timestamp + from: $m.current_time + decode: { fn: timestamp_hhmmss, args: { append: "00" } } + - name: eta + from: $m.eta_time + decode: { fn: timestamp_hhmmss, args: { append: "00" } } + + - name: fuel_in_tons + from: $m.fuel_in_tons + when: { not: { in: [$m.fuel_in_tons, ["***", "****"]] } } + decode: { fn: float } + + - name: departure_icao + from: $m.departure_icao + decode: { fn: airport } + - name: arrival_icao + from: $m.arrival_icao + decode: { fn: airport } +formatted: + description: "Position Report" + items: + - { type: position, value: $position } + - { type: timestamp, kind: month, value: $month } + - { type: timestamp, kind: day, value: $day } + - { type: timestamp, kind: current, value: $timestamp } + - { type: timestamp, kind: eta, value: $eta } + - { type: fuel, units: tons, value: $fuel_in_tons, when_present: true } + - { type: airport_origin, value: $departure_icao } + - { type: airport_destination, value: $arrival_icao } + - { type: altitude, value: $altitude } diff --git a/spec/labels/4A.yaml b/spec/labels/4A.yaml new file mode 100644 index 0000000..98ffb35 --- /dev/null +++ b/spec/labels/4A.yaml @@ -0,0 +1,68 @@ +spec_version: "1" +plugin: + name: Label_4A + type: text + docs: https://github.com/airframesio/acars-message-documentation/blob/main/research/4A/README.md + decode_level: MESSAGE +qualifiers: + labels: ["4A"] +parse: + - { split: ",", into: fields } +variants: + # Variant 1: 11 fields → timestamp + tail + callsign + airports + - name: latest_new_format_11 + when: { equals: [{ length: $fields }, 11] } + fields: + - name: timestamp + from: $fields[0] + decode: { fn: timestamp_hhmmss } + - name: tail + from: $fields[2] + decode: { fn: tail_number, args: { strip_chars: "." } } + - name: callsign + from: $fields[3] + when: { not_equal: ["$fields[3]", ""] } + decode: { fn: callsign } + - name: departure_icao + from: $fields[4] + decode: { fn: airport } + - name: arrival_icao + from: $fields[5] + decode: { fn: airport } + + # Variant 2: 6 fields, first one starts with N or S — coords + waypoints. + # Complex enough that it gets an escape hatch. + - name: latest_new_format_6_position + when: + all: + - { equals: [{ length: $fields }, 6] } + - { matches: ["$fields[0]", "^[NS]"] } + fields: + - name: variant_2_result + from: $fields + decode: + fn: custom + custom: label_4a_variant_2_decode + + # Variant 3: 6 fields, not starting with N or S — timestamp + ETA + position + - name: latest_new_format_6_status + when: { equals: [{ length: $fields }, 6] } + fields: + - name: timestamp + from: $fields[0] + decode: { fn: timestamp_hhmmss } + - name: eta + from: $fields[1] + decode: { fn: timestamp_hhmmss } + - name: altitude + from: $fields[3] + decode: { fn: integer } + - name: position + from: $fields + decode: + fn: custom + custom: label_4a_variant_3_position + - default: fail +formatted: + description: "Latest New Format" + custom: label_4a_format diff --git a/spec/labels/H1/OHMA.yaml b/spec/labels/H1/OHMA.yaml new file mode 100644 index 0000000..073f7be --- /dev/null +++ b/spec/labels/H1/OHMA.yaml @@ -0,0 +1,28 @@ +spec_version: "1" +plugin: + name: Label_H1_OHMA + type: binary + docs: https://github.com/airframesio/acars-message-documentation/blob/main/research/H1/OHMA.md + decode_level: FULL +qualifiers: + labels: ["H1"] + preambles: ["OHMA", "/RTNBOCR.OHMA", "#T1B/RTNBOCR.OHMA"] +parse: + # message.text contains "...OHMA". Strip prefix, base64-decode, + # deflate (raw), UTF-8 decode → JSON text. + - { split: "OHMA", into: ohma_parts } + - { require_length: { var: ohma_parts, min: 2 }, else: fail } + - { base64: "$ohma_parts[1]", into: deflated_bytes } + - { deflate: { source: $deflated_bytes, format: raw }, into: json_bytes } + - { text_decode: { source: $json_bytes, encoding: utf-8 }, into: json_text } +fields: + - name: ohma + from: $json_text + decode: + fn: custom + custom: ohma_unwrap_message +formatted: + description: "OHMA Message" + items: + - type: custom + custom: ohma_message_item diff --git a/spec/shared/README.md b/spec/shared/README.md new file mode 100644 index 0000000..fa65b79 --- /dev/null +++ b/spec/shared/README.md @@ -0,0 +1,21 @@ +# Shared spec data + +Cross-cutting data referenced by plugin specs and emitted into per-language runtimes by codegen. Avoids duplicating canonical constants (CRC tables, decoder semantics, coordinate format definitions) across three language implementations. + +| File | Purpose | +|-----------------------|-----------------------------------------------------------------------------------| +| `crc_tables.yaml` | CRC polynomial lookup tables. Codegen emits identical tables into each runtime. | +| `decode_fns.yaml` | Canonical semantics of named decode functions (`coordinate`, `timestamp_hhmmss`, etc.). Per-language runtimes implement these. | +| `coord_formats.yaml` | Coordinate format definitions (single-axis vs combined, prefix chars, divisor) | + +Files in `spec/shared/` are **excluded** from plugin codegen — they are data, not plugins. The codegen tool reads them when emitting runtime constants tables. + +Plugin spec files MAY reference shared data: + +```yaml +decode: + fn: coordinate + args: { format_ref: shared/coord_formats.yaml#NS_DDDDD_DIV100 } +``` + +(This `format_ref` indirection is planned for ADS v1.1; v1 uses inline args.) diff --git a/spec/shared/crc_tables.yaml b/spec/shared/crc_tables.yaml new file mode 100644 index 0000000..5e4a9dd --- /dev/null +++ b/spec/shared/crc_tables.yaml @@ -0,0 +1,50 @@ +# CRC polynomial lookup tables for ACARS checksum validation. +# Codegen emits these as immutable arrays into each language runtime: +# runtimes/typescript/crc_tables.ts +# runtimes/rust/src/crc_tables.rs +# runtimes/c/src/crc_tables.c + crc_tables.h +# +# Source values are extracted from acars-decoder-typescript/lib/utils/arinc_702_helper.ts. + +version: 1 + +# CRC-16 (IBM SDLC, reversed). Used by ARINC 702 H1 message checksums. +crc16_ibm_sdlc_rev: + polynomial: 0x8408 + init: 0xFFFF + refin: true + refout: true + xorout: 0xFFFF + width: 16 + # Table values populated during runtime helper migration (task #19). + table: [] + +# CRC-16 (GENIBUS). Used as fallback for ARINC 702 H1 checksum mismatch. +crc16_genibus: + polynomial: 0x1021 + init: 0xFFFF + refin: false + refout: false + xorout: 0xFFFF + width: 16 + table: [] + +# CRC-32 used by MIAM PDU v1 (ARINC 665). +arinc_665_crc32: + polynomial: 0x04C11DB7 + init: 0xFFFFFFFF + refin: false + refout: false + xorout: 0xFFFFFFFF + width: 32 + table: [] + +# CRC-16 used by MIAM PDU v2 (ARINC 6). +arinc_6_crc16: + polynomial: 0x8005 + init: 0x0000 + refin: false + refout: false + xorout: 0x0000 + width: 16 + table: [] diff --git a/spec/shared/decode_fns.yaml b/spec/shared/decode_fns.yaml new file mode 100644 index 0000000..a301bbb --- /dev/null +++ b/spec/shared/decode_fns.yaml @@ -0,0 +1,94 @@ +# Canonical semantics of named decode functions referenced from plugin specs. +# Implementations live in runtimes//. This file documents the contract. +# +# Source-of-truth behavior matches acars-decoder-typescript/lib/utils/. + +version: 1 + +coordinate: + description: "Single-axis or combined latitude/longitude parser." + args: + style: { type: enum, values: [single_axis, combined] } + axis: { type: enum, values: [latitude, longitude], when: { style: single_axis } } + prefix_chars: { type: array, items: string } + digits: { type: integer, min: 1 } + divisor: { type: number, min: 1 } + format: { type: string, when: { style: combined } } + reference_ts: "acars-decoder-typescript/lib/utils/coordinate_utils.ts" + reference_ts_inline_examples: + - "Label_10_POS.ts uses inline ((sign) * Number(rest)) / 100" + +coordinate_decimal_minutes: + description: "DDMM.M decimal-minute coordinate parser." + args: + style: { type: enum, values: [combined] } + format: { type: string } + reference_ts: "acars-decoder-typescript/lib/utils/coordinate_utils.ts#decodeStringCoordinatesDecimalMinutes" + +integer: + description: "Parse a substring as integer, optional multiplier." + args: + substring_start: { type: integer, default: 0 } + substring_length: { type: integer, optional: true } + multiplier: { type: number, default: 1 } + reference_ts: "Number(s)" + +float: + description: "Parse string as float." + args: + multiplier: { type: number, default: 1 } + reference_ts: "Number(s)" + +string: + description: "Pass through as string (with optional trim/case)." + args: + trim: { type: boolean, default: false } + +trim: + description: "String.trim()" + +uppercase: { description: "String.toUpperCase()" } +lowercase: { description: "String.toLowerCase()" } + +timestamp_hhmmss: + description: "HHMMSS → seconds since midnight. Optional 'append' for shorter inputs." + args: + append: { type: string, optional: true, description: "appended to input before parsing (e.g. '00' to convert HHMM to HHMM00)" } + reference_ts: "acars-decoder-typescript/lib/DateTimeUtils.ts#convertHHMMSSToTod" + +timestamp_ddhhmm: + description: "DDHHMM → epoch-ish." + reference_ts: "acars-decoder-typescript/lib/DateTimeUtils.ts#convertDateTimeToEpoch" + +callsign: + description: "Normalize callsign string." + +tail_number: + description: "Normalize tail number; optional character stripping." + args: + strip_chars: { type: string, optional: true, description: "characters to remove from the result" } + +flight_number: + description: "Normalize flight number string." + +airport: + description: "ICAO airport code (4 letters typically)." + +altitude_feet: + description: "Number with feet semantics." + +speed_knots: { description: "Number with knots semantics." } +heading_degrees: { description: "Number with degrees semantics." } +fuel_kg: { description: "Number with kg semantics." } +fuel_lb: { description: "Number with lb semantics." } + +hex_to_bytes: + description: "Hex string → byte array." + +json_parse: + description: "JSON.parse()." + +custom: + description: "Per-language native function. See ESCAPE_HATCHES.md." + args: + custom: { type: identifier, required: true } diff --git a/spec/wildcards/arinc_702.yaml b/spec/wildcards/arinc_702.yaml new file mode 100644 index 0000000..645d4c9 --- /dev/null +++ b/spec/wildcards/arinc_702.yaml @@ -0,0 +1,15 @@ +spec_version: "1" +plugin: + name: ARINC_702 + type: text + docs: https://github.com/airframesio/acars-message-documentation/blob/main/research/ARINC_702.md + decode_level: MESSAGE +qualifiers: + labels: ["*"] +parse: + # Pure escape hatch: heuristic offset computation, fallback chains, and + # recursive H1 message decoding all live in hand-written per-language code. + custom: arinc_702_dispatch +formatted: + description: "ARINC 702 Message" + custom: arinc_702_format From 3b8d975895fb7d2182254ccc9c8d6c7fc65c1bd9 Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Wed, 27 May 2026 21:28:11 -0700 Subject: [PATCH 02/23] Implement Rust + C emitters; fix snake-case filename for digit-letter labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rust emitter targets an `ads_runtime` crate API parallel to the TS shape: use ads_runtime::{Plugin, Message, Options, DecodeResult, Qualifiers, ResultFormatter, helpers}; pub struct LabelXyz; impl Plugin for LabelXyz { fn qualifiers(...) ... fn decode(...) ... } C emitter generates per-plugin .c + .h pairs targeting an `ads_runtime.h` API: ads_decode_result_t *_decode(const ads_message_t *msg, const ads_options_t *opts); ads_qualifiers_t _qualifiers(void); extern const ads_plugin_descriptor_t _descriptor; Both emitters cover all parse steps (split, regex, substring, require_length, bitfield, ascii85, deflate, base64, text_decode, hex_decode, concat_bits, custom), fields with optional `when` gating, variant dispatch with conditional matching, formatter calls (typed + custom), and escape-hatch delegation at parse/field/formatter levels. Stage 2 fills in the actual runtime libraries under runtimes/{rust,c}/ matching these emitted call sites. Filename fix in cli.ts: the previous camelCase→snake_case regex inserted an unwanted underscore between digit and capital (Label_4A → label_4_a). Plugin names are already snake-style with caps so a plain lowercase is correct (Label_4A → label_4a, Label_10_POS → label_10_pos). Verified end-to-end: ads-gen generate --target {ts,rust,c} produces clean, readable output for all 5 reference specs. Co-Authored-By: Claude Opus 4.7 (1M context) --- codegen/src/cli.ts | 11 +- codegen/src/emit-c.ts | 410 ++++++++++++++++++++++++++++++++++++++- codegen/src/emit-rust.ts | 359 +++++++++++++++++++++++++++++++++- 3 files changed, 764 insertions(+), 16 deletions(-) diff --git a/codegen/src/cli.ts b/codegen/src/cli.ts index 97abf96..7ba2a97 100644 --- a/codegen/src/cli.ts +++ b/codegen/src/cli.ts @@ -64,13 +64,12 @@ function runGenerate(opts: { target: string; spec: string; out: string; check?: } function pluginToFileBase(spec: SpecIR, target: "ts" | "rust" | "c"): string { - // TS keeps PascalCase to match existing convention (Label_10_POS.ts). - // Rust + C use snake_case. + // TS keeps PascalCase (Label_10_POS.ts). Rust + C use snake_case. + // Plugin names are already snake-style with capitals — just lowercase. + // (The old camelCase-to-snake regex inserted an unwanted underscore when a + // digit was followed by a capital, e.g. Label_4A → label_4_a.) if (target === "ts") return spec.plugin.name; - return spec.plugin.name - .replace(/([a-z0-9])([A-Z])/g, "$1_$2") - .replace(/__+/g, "_") - .toLowerCase(); + return spec.plugin.name.toLowerCase(); } function loadAllSpecs(specRoot: string): SpecIR[] { diff --git a/codegen/src/emit-c.ts b/codegen/src/emit-c.ts index 28fc8fd..32cb73a 100644 --- a/codegen/src/emit-c.ts +++ b/codegen/src/emit-c.ts @@ -1,13 +1,411 @@ -import type { SpecIR } from "./ir.js"; +import type { + Condition, + DecodeCall, + FieldIR, + FormattedIR, + FormatterCall, + ParseStep, + SpecIR, + ValueExpr, + VariantIR, +} from "./ir.js"; /** - * Emit C99 source + header from a SpecIR. - * Filled in by task #7. + * Emit C99 plugin source from a SpecIR. + * + * Target runtime API (from runtimes/c/include/ads_runtime.h, written in Stage 2): + * + * #include "ads_runtime.h" + * ads_decode_result_t *_decode(const ads_message_t *msg, const ads_options_t *opts); + * ads_qualifiers_t _qualifiers(void); + * + * Plus a registry entry for the dispatcher to find this plugin. + * + * Generated files: + * .c — implementation + * .h — function prototypes + plugin descriptor */ export function emitC(spec: SpecIR): { source: string; header: string } { - const banner = `/* AUTO-GENERATED from ${spec.sourcePath}. Do not edit. */\n/* Plugin: ${spec.plugin.name} */\n`; + const cls = spec.plugin.name; + const snake = pluginNameToSnake(cls); + const slug = pluginNameToSlug(cls); + return { - source: banner + "/* TODO: emit body (task #7) */\n", - header: banner + "/* TODO: emit header (task #7) */\n", + source: emitSource(spec, snake, slug), + header: emitHeader(spec, snake), }; } + +function emitHeader(spec: SpecIR, snake: string): string { + const guard = `ADS_PLUGIN_${snake.toUpperCase()}_H`; + const out: string[] = []; + out.push(`/* AUTO-GENERATED from ${spec.sourcePath}. Do not edit. */`); + out.push(`/* Plugin: ${spec.plugin.name} */`); + if (spec.plugin.docs) out.push(`/* Docs: ${spec.plugin.docs} */`); + out.push(""); + out.push(`#ifndef ${guard}`); + out.push(`#define ${guard}`); + out.push(""); + out.push(`#include "ads_runtime.h"`); + out.push(""); + out.push(`ads_decode_result_t *${snake}_decode(const ads_message_t *msg, const ads_options_t *opts);`); + out.push(`ads_qualifiers_t ${snake}_qualifiers(void);`); + out.push(""); + out.push(`extern const ads_plugin_descriptor_t ${snake}_descriptor;`); + out.push(""); + out.push(`#endif /* ${guard} */`); + return out.join("\n") + "\n"; +} + +function emitSource(spec: SpecIR, snake: string, slug: string): string { + const out: string[] = []; + out.push(`/* AUTO-GENERATED from ${spec.sourcePath}. Do not edit. */`); + out.push(`/* Plugin: ${spec.plugin.name} */`); + out.push(""); + out.push(`#include "${snake}.h"`); + out.push(`#include "ads_helpers.h"`); + out.push(`#include "ads_escape_hatches.h"`); + out.push(""); + + // qualifiers + emitQualifiers(spec, snake, out); + + // decode + emitDecode(spec, snake, slug, out); + + // descriptor + out.push(`const ads_plugin_descriptor_t ${snake}_descriptor = {`); + out.push(` .name = ${cString(slug)},`); + out.push(` .qualifiers = ${snake}_qualifiers,`); + out.push(` .decode = ${snake}_decode,`); + out.push(`};`); + return out.join("\n") + "\n"; +} + +function emitQualifiers(spec: SpecIR, snake: string, out: string[]): void { + out.push(`static const char *const ${snake}_labels[] = {`); + for (const l of spec.qualifiers.labels) out.push(` ${cString(l)},`); + out.push(` NULL,`); + out.push(`};`); + if (spec.qualifiers.preambles && spec.qualifiers.preambles.length > 0) { + out.push(`static const char *const ${snake}_preambles[] = {`); + for (const p of spec.qualifiers.preambles) out.push(` ${cString(p)},`); + out.push(` NULL,`); + out.push(`};`); + } + out.push(""); + out.push(`ads_qualifiers_t ${snake}_qualifiers(void) {`); + out.push(` ads_qualifiers_t q = {0};`); + out.push(` q.labels = ${snake}_labels;`); + if (spec.qualifiers.preambles && spec.qualifiers.preambles.length > 0) { + out.push(` q.preambles = ${snake}_preambles;`); + } + out.push(` return q;`); + out.push(`}`); + out.push(""); +} + +function emitDecode(spec: SpecIR, snake: string, _slug: string, out: string[]): void { + out.push( + `ads_decode_result_t *${snake}_decode(const ads_message_t *msg, const ads_options_t *opts) {`, + ); + out.push(` (void)opts;`); + out.push( + ` ads_decode_result_t *result = ads_result_new(${cString(pluginNameToSlug(spec.plugin.name))}, ${cString(formattedDescription(spec.formatted))}, msg);`, + ); + out.push(` if (!result) return NULL;`); + out.push(""); + + if (spec.parse.kind === "custom") { + out.push(` return ads_hatch_${spec.parse.name}(msg, result, opts);`); + out.push(`}`); + return; + } + + for (const step of spec.parse.steps) { + emitParseStep(step, out, " "); + } + + if (spec.variants) { + emitVariants(spec.variants, out, " "); + } else if (spec.fields) { + for (const field of spec.fields) { + emitField(field, out, " "); + } + } + + emitFormatted(spec.formatted, out, " "); + out.push(` ads_result_set_decoded(result, true);`); + out.push(` return result;`); + out.push(`}`); +} + +function emitParseStep(step: ParseStep, out: string[], indent: string): void { + switch (step.kind) { + case "split": + out.push( + `${indent}ads_str_list_t ${step.into} = ads_split(msg->text, ${cString(step.delimiter)});`, + ); + break; + case "regex": + out.push( + `${indent}ads_regex_match_t ${step.into} = ads_regex_match(${cString(step.pattern)}, ${renderExpr({ kind: "var", ref: step.on })});`, + ); + out.push(`${indent}if (!ads_regex_match_ok(&${step.into})) {`); + out.push(`${indent} return ads_result_fail_unknown(result, msg->text);`); + out.push(`${indent}}`); + break; + case "substring": + out.push( + `${indent}const char *${step.into} = ads_substring(${renderExpr({ kind: "var", ref: step.from })}, ${step.start}, ${step.length ?? step.end ?? -1});`, + ); + break; + case "require_length": { + const checks: string[] = []; + if (step.equals !== undefined) checks.push(`${step.var}.count != ${step.equals}`); + if (step.min !== undefined) checks.push(`${step.var}.count < ${step.min}`); + if (step.max !== undefined) checks.push(`${step.var}.count > ${step.max}`); + out.push(`${indent}if (${checks.join(" || ")}) {`); + out.push(`${indent} return ads_result_fail_unknown(result, msg->text);`); + out.push(`${indent}}`); + break; + } + case "bitfield": + for (const f of step.fields) { + out.push( + `${indent}uint32_t ${f.name} = ads_bitslice(${renderExpr({ kind: "var", ref: step.source })}, ${f.bitStart}, ${f.bitEnd});`, + ); + } + break; + case "decode_ascii85": + out.push( + `${indent}ads_bytes_t ${step.into} = ads_decode_ascii85(${renderExpr({ kind: "var", ref: step.source })});`, + ); + break; + case "deflate": + out.push( + `${indent}ads_bytes_t ${step.into} = ads_inflate(${renderExpr({ kind: "var", ref: step.source })}, ${step.offset ?? 0}, ${cString(step.format)});`, + ); + break; + case "base64": + out.push( + `${indent}ads_bytes_t ${step.into} = ads_base64_decode(${renderExpr({ kind: "var", ref: step.source })});`, + ); + break; + case "text_decode": + out.push( + `${indent}char *${step.into} = ads_text_decode(${renderExpr({ kind: "var", ref: step.source })}, ${cString(step.encoding)});`, + ); + break; + case "hex_decode": + out.push( + `${indent}ads_bytes_t ${step.into} = ads_hex_decode(${renderExpr({ kind: "var", ref: step.source })});`, + ); + break; + case "concat_bits": + out.push( + `${indent}uint32_t ${step.into} = ads_concat_bits((const uint32_t[]){${step.sources + .map((s) => renderExpr({ kind: "var", ref: s })) + .join(", ")}}, ${step.sources.length});`, + ); + break; + case "custom": + out.push(`${indent}ads_hatch_${step.name}(msg, result, opts);`); + break; + } +} + +function emitField(field: FieldIR, out: string[], indent: string): void { + const valueExpr = renderExpr(field.from); + const decodeExpr = field.decode ? renderDecodeCall(field.decode, valueExpr) : valueExpr; + if (field.when) { + out.push(`${indent}if (${renderCondition(field.when)}) {`); + out.push(`${indent} ads_value_t ${field.name} = ${decodeExpr};`); + out.push( + `${indent} ads_result_raw_set(result, ${cString(field.name)}, ${field.name});`, + ); + out.push(`${indent}}`); + } else { + out.push(`${indent}ads_value_t ${field.name} = ${decodeExpr};`); + out.push(`${indent}ads_result_raw_set(result, ${cString(field.name)}, ${field.name});`); + } +} + +function emitVariants(variants: VariantIR[], out: string[], indent: string): void { + let first = true; + for (const v of variants) { + if (v.isDefault) { + out.push(`${indent}${first ? "{" : "else {"}`); + out.push(`${indent} return ads_result_fail_unknown(result, msg->text);`); + out.push(`${indent}}`); + continue; + } + const cond = v.when ? renderCondition(v.when) : "true"; + out.push(`${indent}${first ? "if" : "else if"} (${cond}) {`); + if (v.fields) { + for (const f of v.fields) { + emitField(f, out, indent + " "); + } + } + out.push(`${indent}}`); + first = false; + } +} + +function emitFormatted(formatted: FormattedIR, out: string[], indent: string): void { + if (formatted.kind === "custom") { + out.push(`${indent}ads_hatch_${formatted.name}(result);`); + return; + } + for (const item of formatted.items) { + emitFormatterCall(item, out, indent); + } +} + +function emitFormatterCall(item: FormatterCall, out: string[], indent: string): void { + if (item.type === "custom" && item.customName) { + out.push(`${indent}ads_hatch_${item.customName}(result);`); + return; + } + const methodMap: Record = { + position: "position", + altitude: "altitude", + speed: "speed", + heading: "heading", + timestamp: "timestamp", + callsign: "callsign", + flight_number: "flight_number", + tail_number: "tail", + airport_origin: "departure_airport", + airport_destination: "arrival_airport", + fuel: "fuel", + free_text: "unknown_arr", + }; + const method = methodMap[item.type]; + if (!method) { + out.push(`${indent}/* TODO formatter: ${item.type} */`); + return; + } + if (item.type === "position") { + const lat = item.args["latitude"] ?? item.args["lat"]; + const lon = item.args["longitude"] ?? item.args["lon"]; + if (lat !== undefined && lon !== undefined) { + out.push( + `${indent}ads_fmt_position(result, ${renderCArg(lat)}, ${renderCArg(lon)});`, + ); + return; + } + if (item.args["value"] !== undefined) { + out.push(`${indent}ads_fmt_position_value(result, ${renderCArg(item.args["value"])});`); + return; + } + } + if (item.type === "free_text" && Array.isArray(item.args["values"])) { + const values = item.args["values"] as unknown[]; + out.push( + `${indent}ads_fmt_unknown_arr(result, (const char *const[]){${values + .map(renderCArg) + .join(", ")}}, ${values.length});`, + ); + return; + } + if ("value" in item.args) { + out.push(`${indent}ads_fmt_${method}(result, ${renderCArg(item.args["value"])});`); + } else { + out.push(`${indent}ads_fmt_${method}(result);`); + } +} + +function renderCArg(v: unknown): string { + if (typeof v === "string" && v.startsWith("$")) return varRefToC(v.slice(1)); + if (typeof v === "string") return cString(v); + if (typeof v === "number") return String(v); + if (typeof v === "boolean") return v ? "true" : "false"; + return cString(JSON.stringify(v)); +} + +function renderDecodeCall(call: DecodeCall, valueExpr: string): string { + if (call.fn === "custom") { + return `ads_hatch_${call.name}(${valueExpr}, ${cString(JSON.stringify(call.args))})`; + } + if (Object.keys(call.args).length > 0) { + return `ads_decode_${call.fn}(${valueExpr}, ${cString(JSON.stringify(call.args))})`; + } + return `ads_decode_${call.fn}(${valueExpr})`; +} + +function renderExpr(expr: ValueExpr): string { + switch (expr.kind) { + case "var": + return varRefToC(expr.ref.slice(1)); + case "literal": + if (typeof expr.value === "string") return cString(expr.value); + if (expr.value === null) return "NULL"; + return String(expr.value); + case "call": + if (expr.fn === "length") return `${varRefToC(expr.arg.slice(1))}.count`; + return "/* unknown call */"; + } +} + +function varRefToC(body: string): string { + // message.text → msg->text + // parts[1] → parts.items[1] + // m.unsplit_coords → ads_regex_group(&m, "unsplit_coords") + if (body.startsWith("message.")) { + return body.replace(/^message\./, "msg->"); + } + const m = body.match(/^([a-z_][a-z0-9_]*)\.([a-z_][a-z0-9_]*)$/i); + if (m) { + return `ads_regex_group(&${m[1]}, ${cString(m[2]!)})`; + } + // parts[N] → parts.items[N] + return body.replace(/^([a-z_][a-z0-9_]*)\[(\d+)\]$/i, "$1.items[$2]"); +} + +function renderCondition(cond: Condition): string { + switch (cond.kind) { + case "equals": + return cEquality(renderExpr(cond.left), renderExpr(cond.right), true); + case "not_equal": + return cEquality(renderExpr(cond.left), renderExpr(cond.right), false); + case "matches": + return `ads_regex_test(${cString(cond.regex)}, ${varRefToC(cond.var.slice(1))})`; + case "in": { + const list = (cond.values as unknown[]).map(renderCArg).join(", "); + return `ads_str_in((const char *const[]){${list}}, ${(cond.values as unknown[]).length}, ${renderExpr(cond.value)})`; + } + case "all": + return cond.conds.map((c) => `(${renderCondition(c)})`).join(" && "); + case "any": + return cond.conds.map((c) => `(${renderCondition(c)})`).join(" || "); + case "not": + return `!(${renderCondition(cond.cond)})`; + } +} + +function cEquality(left: string, right: string, equal: boolean): string { + const op = equal ? "==" : "!="; + // If either side is a quoted string literal, use strcmp. + const isLit = (s: string) => s.startsWith('"'); + if (isLit(left) || isLit(right)) { + return `(strcmp(${left}, ${right}) ${op} 0)`; + } + return `${left} ${op} ${right}`; +} + +function cString(s: string): string { + return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`; +} + +function formattedDescription(formatted: FormattedIR): string { + return formatted.description; +} + +function pluginNameToSnake(name: string): string { + return name.replace(/_/g, "_").toLowerCase(); +} + +function pluginNameToSlug(name: string): string { + return name.replace(/_/g, "-").toLowerCase(); +} diff --git a/codegen/src/emit-rust.ts b/codegen/src/emit-rust.ts index 33f74a9..3c1541a 100644 --- a/codegen/src/emit-rust.ts +++ b/codegen/src/emit-rust.ts @@ -1,9 +1,360 @@ -import type { SpecIR } from "./ir.js"; +import type { + Condition, + DecodeCall, + FieldIR, + FormattedIR, + FormatterCall, + ParseStep, + SpecIR, + ValueExpr, + VariantIR, +} from "./ir.js"; /** - * Emit idiomatic Rust trait impl from a SpecIR. - * Filled in by task #6. + * Emit idiomatic Rust source from a SpecIR. + * + * Targets a runtime API parallel to the TS shape: + * use ads_runtime::{Plugin, Message, Options, DecodeResult, Qualifiers, ResultFormatter, helpers}; + * use crate::escape_hatches; + * pub struct LabelXyz; + * impl Plugin for LabelXyz { fn qualifiers(&self) -> Qualifiers { ... } fn decode(...) -> DecodeResult { ... } } + * + * The Stage 2 Rust impl (`acars-decoder-rust`) provides the `ads_runtime` + * crate (from `runtimes/rust/`) with these exact types. */ export function emitRust(spec: SpecIR): string { - return `// AUTO-GENERATED from ${spec.sourcePath}. Do not edit.\n// Plugin: ${spec.plugin.name}\n// TODO: emit body (task #6)\n`; + const cls = spec.plugin.name; + const slug = pluginNameToSlug(cls); + + const out: string[] = []; + out.push(`// AUTO-GENERATED from ${spec.sourcePath}. Do not edit.`); + out.push(`// Plugin: ${cls}`); + if (spec.plugin.docs) out.push(`// Docs: ${spec.plugin.docs}`); + out.push(""); + out.push( + `use ads_runtime::{Plugin, Message, Options, DecodeResult, Qualifiers, ResultFormatter, helpers};`, + ); + out.push(`use crate::escape_hatches;`); + out.push(""); + out.push(`pub struct ${cls};`); + out.push(""); + out.push(`impl Plugin for ${cls} {`); + out.push(` fn name(&self) -> &'static str { ${rustString(slug)} }`); + out.push(""); + + // qualifiers() + out.push(` fn qualifiers(&self) -> Qualifiers {`); + out.push(` Qualifiers {`); + out.push(` labels: vec![${spec.qualifiers.labels.map(rustString).join(", ")}],`); + if (spec.qualifiers.preambles && spec.qualifiers.preambles.length > 0) { + out.push(` preambles: vec![${spec.qualifiers.preambles.map(rustString).join(", ")}],`); + } else { + out.push(` preambles: vec![],`); + } + out.push(` }`); + out.push(` }`); + out.push(""); + + // decode() + out.push(` fn decode(&self, message: &Message, options: &Options) -> DecodeResult {`); + out.push( + ` let mut result = DecodeResult::new(self.name(), ${rustString(formattedDescription(spec.formatted))});`, + ); + out.push(` result.set_message(message);`); + out.push(""); + + if (spec.parse.kind === "custom") { + out.push( + ` return escape_hatches::${spec.parse.name}(self, message, &mut result, options);`, + ); + out.push(` }`); + out.push(`}`); + return out.join("\n") + "\n"; + } + + for (const step of spec.parse.steps) { + emitParseStep(step, out, " "); + } + + if (spec.variants) { + emitVariants(spec.variants, out, " "); + } else if (spec.fields) { + for (const field of spec.fields) { + emitField(field, out, " "); + } + } + + emitFormatted(spec.formatted, out, " "); + + out.push(` result.set_decoded(true);`); + out.push(` result`); + out.push(` }`); + out.push(`}`); + return out.join("\n") + "\n"; +} + +function emitParseStep(step: ParseStep, out: string[], indent: string): void { + switch (step.kind) { + case "split": + out.push( + `${indent}let ${step.into}: Vec<&str> = message.text.split(${rustString(step.delimiter)}).collect();`, + ); + break; + case "regex": { + const onExpr = renderExpr({ kind: "var", ref: step.on }); + out.push(`${indent}let ${step.into}_re = helpers::regex(${rustString(step.pattern)});`); + out.push(`${indent}let ${step.into} = match ${step.into}_re.captures(${onExpr}) {`); + out.push(`${indent} Some(c) => c,`); + out.push(`${indent} None => return result.fail_unknown(&message.text),`); + out.push(`${indent}};`); + break; + } + case "substring": { + const fromExpr = renderExpr({ kind: "var", ref: step.from }); + if (step.length !== undefined) { + out.push( + `${indent}let ${step.into} = helpers::substring(${fromExpr}, ${step.start}, Some(${step.start + step.length}));`, + ); + } else if (step.end !== undefined) { + out.push( + `${indent}let ${step.into} = helpers::substring(${fromExpr}, ${step.start}, Some(${step.end}));`, + ); + } else { + out.push(`${indent}let ${step.into} = helpers::substring(${fromExpr}, ${step.start}, None);`); + } + break; + } + case "require_length": { + const checks: string[] = []; + if (step.equals !== undefined) checks.push(`${step.var}.len() != ${step.equals}`); + if (step.min !== undefined) checks.push(`${step.var}.len() < ${step.min}`); + if (step.max !== undefined) checks.push(`${step.var}.len() > ${step.max}`); + out.push(`${indent}if ${checks.join(" || ")} {`); + out.push(`${indent} return result.fail_unknown(&message.text);`); + out.push(`${indent}}`); + break; + } + case "bitfield": + for (const f of step.fields) { + out.push( + `${indent}let ${f.name} = helpers::bitslice(${renderExpr({ kind: "var", ref: step.source })}, ${f.bitStart}, ${f.bitEnd});`, + ); + } + break; + case "decode_ascii85": + out.push( + `${indent}let ${step.into} = helpers::decode_ascii85(${renderExpr({ kind: "var", ref: step.source })});`, + ); + break; + case "deflate": { + const srcExpr = renderExpr({ kind: "var", ref: step.source }); + const sliced = step.offset ? `&${srcExpr}[${step.offset}..]` : srcExpr; + out.push( + `${indent}let ${step.into} = helpers::inflate(${sliced}, ${rustString(step.format)});`, + ); + break; + } + case "base64": + out.push( + `${indent}let ${step.into} = helpers::base64_decode(${renderExpr({ kind: "var", ref: step.source })});`, + ); + break; + case "text_decode": + out.push( + `${indent}let ${step.into} = helpers::text_decode(${renderExpr({ kind: "var", ref: step.source })}, ${rustString(step.encoding)});`, + ); + break; + case "hex_decode": + out.push( + `${indent}let ${step.into} = helpers::hex_decode(${renderExpr({ kind: "var", ref: step.source })});`, + ); + break; + case "concat_bits": + out.push( + `${indent}let ${step.into} = helpers::concat_bits(&[${step.sources + .map((s) => renderExpr({ kind: "var", ref: s })) + .join(", ")}]);`, + ); + break; + case "custom": + out.push(`${indent}escape_hatches::${step.name}(self, message, &mut result, options);`); + break; + } +} + +function emitField(field: FieldIR, out: string[], indent: string): void { + const valueExpr = renderExpr(field.from); + const decodeExpr = field.decode ? renderDecodeCall(field.decode, valueExpr) : valueExpr; + if (field.when) { + out.push(`${indent}if ${renderCondition(field.when)} {`); + out.push(`${indent} let ${field.name} = ${decodeExpr};`); + out.push(`${indent} result.raw.insert(${rustString(field.name)}, ${field.name}.into());`); + out.push(`${indent}}`); + } else { + out.push(`${indent}let ${field.name} = ${decodeExpr};`); + out.push(`${indent}result.raw.insert(${rustString(field.name)}, ${field.name}.clone().into());`); + } +} + +function emitVariants(variants: VariantIR[], out: string[], indent: string): void { + let first = true; + for (const v of variants) { + if (v.isDefault) { + out.push(`${indent}${first ? "{" : "else {"}`); + out.push(`${indent} return result.fail_unknown(&message.text);`); + out.push(`${indent}}`); + continue; + } + const cond = v.when ? renderCondition(v.when) : "true"; + out.push(`${indent}${first ? "if" : "else if"} ${cond} {`); + if (v.fields) { + for (const f of v.fields) { + emitField(f, out, indent + " "); + } + } + out.push(`${indent}}`); + first = false; + } +} + +function emitFormatted(formatted: FormattedIR, out: string[], indent: string): void { + if (formatted.kind === "custom") { + out.push(`${indent}escape_hatches::${formatted.name}(&mut result);`); + return; + } + for (const item of formatted.items) { + emitFormatterCall(item, out, indent); + } +} + +function emitFormatterCall(item: FormatterCall, out: string[], indent: string): void { + if (item.type === "custom" && item.customName) { + out.push(`${indent}escape_hatches::${item.customName}(&mut result);`); + return; + } + const methodMap: Record = { + position: "position", + altitude: "altitude", + speed: "speed", + heading: "heading", + timestamp: "timestamp", + callsign: "callsign", + flight_number: "flight_number", + tail_number: "tail", + airport_origin: "departure_airport", + airport_destination: "arrival_airport", + fuel: "fuel", + free_text: "unknown_arr", + }; + const method = methodMap[item.type]; + if (!method) { + out.push(`${indent}// TODO formatter: ${item.type}`); + return; + } + if (item.type === "position") { + const lat = item.args["latitude"] ?? item.args["lat"]; + const lon = item.args["longitude"] ?? item.args["lon"]; + if (lat !== undefined && lon !== undefined) { + out.push( + `${indent}ResultFormatter::position(&mut result, ${renderRustArg(lat)}, ${renderRustArg(lon)});`, + ); + return; + } + if (item.args["value"] !== undefined) { + out.push( + `${indent}ResultFormatter::position_value(&mut result, ${renderRustArg(item.args["value"])});`, + ); + return; + } + } + if (item.type === "free_text" && Array.isArray(item.args["values"])) { + const vals = (item.args["values"] as unknown[]).map(renderRustArg).join(", "); + out.push(`${indent}ResultFormatter::unknown_arr(&mut result, vec![${vals}]);`); + return; + } + if ("value" in item.args) { + out.push( + `${indent}ResultFormatter::${method}(&mut result, ${renderRustArg(item.args["value"])});`, + ); + } else { + out.push(`${indent}ResultFormatter::${method}(&mut result);`); + } +} + +function renderRustArg(v: unknown): string { + if (typeof v === "string" && v.startsWith("$")) { + return `${renderExpr({ kind: "var", ref: v })}.clone()`; + } + if (typeof v === "string") return rustString(v); + if (typeof v === "number") return String(v); + if (typeof v === "boolean") return String(v); + return rustString(JSON.stringify(v)); +} + +function renderDecodeCall(call: DecodeCall, valueExpr: string): string { + if (call.fn === "custom") { + return `escape_hatches::${call.name}(${valueExpr}, ${rustString(JSON.stringify(call.args))})`; + } + const fn = call.fn; + if (Object.keys(call.args).length > 0) { + return `helpers::${fn}(${valueExpr}, ${rustString(JSON.stringify(call.args))})`; + } + return `helpers::${fn}(${valueExpr})`; +} + +function renderExpr(expr: ValueExpr): string { + switch (expr.kind) { + case "var": + return varRefToRust(expr.ref); + case "literal": + if (typeof expr.value === "string") return rustString(expr.value); + return String(expr.value); + case "call": + if (expr.fn === "length") return `${varRefToRust(expr.arg)}.len()`; + return "/* unknown call */"; + } +} + +function varRefToRust(ref: string): string { + // $message.text → message.text (struct field) + // $parts[1] → parts[1] + // $m.unsplit_coords → &m["unsplit_coords"] (regex captures use named-group access) + let body = ref.slice(1); + // For named regex group access ($m.foo), rewrite as &m["foo"].as_str() + body = body.replace(/^([a-z_][a-z0-9_]*)\.([a-z_][a-z0-9_]*)$/i, (_m, head, group) => { + if (head === "message") return `${head}.${group}`; + return `&${head}[${rustString(group)}].as_str()`; + }); + return body; +} + +function renderCondition(cond: Condition): string { + switch (cond.kind) { + case "equals": + return `${renderExpr(cond.left)} == ${renderExpr(cond.right)}`; + case "not_equal": + return `${renderExpr(cond.left)} != ${renderExpr(cond.right)}`; + case "matches": + return `helpers::regex(${rustString(cond.regex)}).is_match(${varRefToRust(cond.var)})`; + case "in": + return `[${(cond.values as unknown[]).map(renderRustArg).join(", ")}].contains(&${renderExpr(cond.value)})`; + case "all": + return cond.conds.map((c) => `(${renderCondition(c)})`).join(" && "); + case "any": + return cond.conds.map((c) => `(${renderCondition(c)})`).join(" || "); + case "not": + return `!(${renderCondition(cond.cond)})`; + } +} + +function rustString(s: string): string { + return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; +} + +function formattedDescription(formatted: FormattedIR): string { + return formatted.description; +} + +function pluginNameToSlug(name: string): string { + return name.replace(/_/g, "-").toLowerCase(); } From 04f536a6912268426226da009b5d190b7353aeb5 Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Wed, 27 May 2026 21:59:28 -0700 Subject: [PATCH 03/23] Populate runtimes/typescript/ with centralized TS helper library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copies the canonical TS source for: DecoderPlugin, DecoderPluginInterface, DateTimeUtils, ResultFormatter, CoordinateUtils, ASCII85 decoder, compression (pako wrapper + base64), MIAM PDU parser, ARINC 702 helper with CRC tables, ICAO FPL parser, flight plan / route utils, and the Route/Waypoint/Wind type modules. Adds: - index.ts — public exports: DecoderPlugin, types, ResultFormatter, utils - helpers.ts — codegen-call adapters (coordinate, integer, float, string, timestampHhmmss, callsign, tailNumber, airport, base64ToUint8Array, inflate, textDecode, decodeAscii85, hexDecode, bitslice, concatBits). Behavior matches the inline plugin logic in acars-decoder-typescript byte-for-byte; the shared corpus enforces this. - MessageDecoder.ts — interface for the dispatcher (decouples DecoderPlugin from the concrete dispatcher, which stays in the language repo as the public entry point). - escape_hatches/ — placeholder index for per-plugin custom functions. - package.json, tsconfig.json, README.md. Latent bug fixes uncovered during type-checking under the runtime's stricter tsconfig: - arinc_702_helper.ts: two surviving `Buffer.from(data, 'ascii')` calls the Web APIs migration missed. Replaced with a hand-rolled asciiStringToBytes() so CRC helpers run in browser / Deno / Bun. - miam.ts: strict-null narrowing failed after `body = decoded` reassignment; switched to a single-expression assignment that preserves narrowing. - result_formatter.ts: sequence-number / sequence-response items pushed numeric values into a string-typed `value` field. Wrapped in String(...) to match the interface. - flight_plan_utils.ts: two unknown-narrowing casts for plugin-specific raw fields (procedures, company_route). These changes preserve runtime behavior exactly; Stage 2 will adopt this runtime in acars-decoder-typescript via tsconfig path alias and remove the duplicated lib/utils/ files from that repo. Co-Authored-By: Claude Opus 4.7 (1M context) --- runtimes/typescript/.gitkeep | 0 runtimes/typescript/DateTimeUtils.ts | 98 +++ runtimes/typescript/DecoderPlugin.ts | 124 +++ runtimes/typescript/DecoderPluginInterface.ts | 155 ++++ runtimes/typescript/MessageDecoder.ts | 12 + runtimes/typescript/README.md | 56 ++ runtimes/typescript/escape_hatches/index.ts | 25 + runtimes/typescript/helpers.ts | 182 +++++ runtimes/typescript/index.ts | 29 + runtimes/typescript/package-lock.json | 50 ++ runtimes/typescript/package.json | 34 + runtimes/typescript/tsconfig.json | 17 + runtimes/typescript/types/route.ts | 17 + runtimes/typescript/types/waypoint.ts | 36 + runtimes/typescript/types/wind.ts | 12 + runtimes/typescript/utils/arinc_702_helper.ts | 726 ++++++++++++++++++ runtimes/typescript/utils/ascii85.ts | 51 ++ runtimes/typescript/utils/compression.ts | 56 ++ runtimes/typescript/utils/coordinate_utils.ts | 96 +++ .../typescript/utils/flight_plan_utils.ts | 197 +++++ runtimes/typescript/utils/icao_fpl_utils.ts | 195 +++++ runtimes/typescript/utils/miam.ts | 608 +++++++++++++++ runtimes/typescript/utils/result_formatter.ts | 694 +++++++++++++++++ runtimes/typescript/utils/route_utils.ts | 112 +++ 24 files changed, 3582 insertions(+) delete mode 100644 runtimes/typescript/.gitkeep create mode 100644 runtimes/typescript/DateTimeUtils.ts create mode 100644 runtimes/typescript/DecoderPlugin.ts create mode 100644 runtimes/typescript/DecoderPluginInterface.ts create mode 100644 runtimes/typescript/MessageDecoder.ts create mode 100644 runtimes/typescript/README.md create mode 100644 runtimes/typescript/escape_hatches/index.ts create mode 100644 runtimes/typescript/helpers.ts create mode 100644 runtimes/typescript/index.ts create mode 100644 runtimes/typescript/package-lock.json create mode 100644 runtimes/typescript/package.json create mode 100644 runtimes/typescript/tsconfig.json create mode 100644 runtimes/typescript/types/route.ts create mode 100644 runtimes/typescript/types/waypoint.ts create mode 100644 runtimes/typescript/types/wind.ts create mode 100644 runtimes/typescript/utils/arinc_702_helper.ts create mode 100644 runtimes/typescript/utils/ascii85.ts create mode 100644 runtimes/typescript/utils/compression.ts create mode 100644 runtimes/typescript/utils/coordinate_utils.ts create mode 100644 runtimes/typescript/utils/flight_plan_utils.ts create mode 100644 runtimes/typescript/utils/icao_fpl_utils.ts create mode 100644 runtimes/typescript/utils/miam.ts create mode 100644 runtimes/typescript/utils/result_formatter.ts create mode 100644 runtimes/typescript/utils/route_utils.ts diff --git a/runtimes/typescript/.gitkeep b/runtimes/typescript/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/runtimes/typescript/DateTimeUtils.ts b/runtimes/typescript/DateTimeUtils.ts new file mode 100644 index 0000000..f436148 --- /dev/null +++ b/runtimes/typescript/DateTimeUtils.ts @@ -0,0 +1,98 @@ +export class DateTimeUtils { + // Expects a four digit UTC time string (HHMM) + public static UTCToString(UTCString: string) { + let utcDate = new Date(); + utcDate.setUTCHours(+UTCString.substr(0, 2), +UTCString.substr(2, 2), 0); + return utcDate.toTimeString(); + } + + // Expects a six digit date string and a four digit UTC time string + // (DDMMYY) (HHMM) + public static UTCDateTimeToString(dateString: string, timeString: string) { + let utcDate = new Date(); + utcDate.setUTCDate(+dateString.substr(0, 2)); + utcDate.setUTCMonth(+dateString.substr(2, 2)); + if (dateString.length === 6) { + utcDate.setUTCFullYear(2000 + +dateString.substr(4, 2)); + } + if (timeString.length === 6) { + utcDate.setUTCHours( + +timeString.substr(0, 2), + +timeString.substr(2, 2), + +timeString.substr(4, 2), + ); + } else { + utcDate.setUTCHours( + +timeString.substr(0, 2), + +timeString.substr(2, 2), + 0, + ); + } + return utcDate.toUTCString(); + } + + /** + * + * @param time HHMMSS or HHMM + * @returns seconds since midnight + */ + public static convertHHMMSSToTod(time: string): number { + if (time.length === 4) { + time += '00'; + } + const h = Number(time.substring(0, 2)); + const m = Number(time.substring(2, 4)); + const s = Number(time.substring(4, 6)); + const tod = h * 3600 + m * 60 + s; + return tod; + } + + public static convertDayTimeToTod(time: string): number { + const d = Number(time.substring(0, 2)); + const h = Number(time.substring(2, 4)); + const m = Number(time.substring(4, 6)); + const s = Number(time.substring(6, 8)); + const tod = d * 86400 + h * 3600 + m * 60 + s; + return tod; + } + + /** + * + * @param time HHMMSS + * @param date DDMMYY or DDMMYYYY + * @returns seconds since epoch + */ + public static convertDateTimeToEpoch(time: string, date: string): number { + //YYYY-MM-DDTHH:mm:ss.sssZ + if (date.length === 6) { + date = date.substring(0, 4) + `20${date.substring(4, 6)}`; + } + const timestamp = `${date.substring(4, 8)}-${date.substring(2, 4)}-${date.substring(0, 2)}T${time.substring(0, 2)}:${time.substring(2, 4)}:${time.substring(4, 6)}.000Z`; + const millis = Date.parse(timestamp); + return millis / 1000; + } + + /** + * Converts a timestamp to a string + * + * ISO-8601 format for 'epoch' + * HH:MM:SS for 'tod' + * @param time + * @param format + * @returns + */ + public static timestampToString(time: number): string { + const date = new Date(time * 1000); + + if (time < 86400) { + // only time, no date + return date.toISOString().slice(11, 19); + } + if (time < 2678400) { + // unknown month and date + return `YYYY-MM-${date.toISOString().slice(8, 19)}`; + } + //strip off millis + return date.toISOString().slice(0, -5) + 'Z'; + } +} diff --git a/runtimes/typescript/DecoderPlugin.ts b/runtimes/typescript/DecoderPlugin.ts new file mode 100644 index 0000000..34f3bff --- /dev/null +++ b/runtimes/typescript/DecoderPlugin.ts @@ -0,0 +1,124 @@ +import { + DecodeResult, + DecoderPluginInterface, + Message, + Options, + Qualifiers, +} from './DecoderPluginInterface'; +import { ResultFormatter } from './utils/result_formatter'; +import type { MessageDecoder } from './MessageDecoder'; + +export abstract class DecoderPlugin implements DecoderPluginInterface { + decoder!: MessageDecoder; + + name: string = 'unknown'; + + defaultResult(): DecodeResult { + return { + decoded: false, + decoder: { + name: 'unknown', + type: 'pattern-match', + decodeLevel: 'none', + }, + formatted: { + description: 'Unknown', + items: [], + }, + raw: {}, + remaining: {}, + }; + } + + /** + * Creates a DecodeResult pre-populated with the plugin name, description, and message. + * Replaces the common boilerplate at the start of every decode() method. + */ + protected initResult(message: Message, description: string): DecodeResult { + const result = this.defaultResult(); + result.decoder.name = this.name; + result.formatted.description = description; + result.message = message; + return result; + } + + /** + * Sets the decoded flag and decodeLevel on a result. + * If decoded is true and no explicit level is given, infers 'full' or 'partial' + * based on whether remaining.text is set. + */ + protected setDecodeLevel( + result: DecodeResult, + decoded: boolean, + level?: 'full' | 'partial', + ): void { + result.decoded = decoded; + if (decoded) { + result.decoder.decodeLevel = + level ?? (result.remaining.text ? 'partial' : 'full'); + } else { + result.decoder.decodeLevel = 'none'; + } + } + + /** + * Logs a debug message prefixed with the plugin name, only if options.debug is true. + */ + protected debug(options: Options, ...args: unknown[]): void { + if (options.debug) { + console.log(`[${this.name}]`, ...args); + } + } + + /** + * Marks a result as a failed decode with 'none' level, sets the remaining text + * as unknown, and logs a debug message. Returns the result for convenient early return. + */ + protected failUnknown( + result: DecodeResult, + text: string, + options: Options = {}, + ): DecodeResult { + this.debug(options, `Unknown message: ${text}`); + ResultFormatter.unknown(result, text); + result.decoded = false; + result.decoder.decodeLevel = 'none'; + return result; + } + + options: object; + + constructor(decoder: MessageDecoder, options: Options = {}) { + this.decoder = decoder; + this.options = options; + } + + id(): string { + console.log( + 'DecoderPlugin subclass has not overriden id() to provide a unique ID for this plugin!', + ); + return 'abstract_decoder_plugin'; + } + + meetsStateRequirements(): boolean { + return true; + } + + // onRegister(store: Store) { + // this.store = store; + // } + + qualifiers(): Qualifiers { + const labels: Array = []; + + return { + labels, + }; + } + + decode(message: Message, options: Options = {}): DecodeResult { + const decodeResult = this.defaultResult(); + decodeResult.remaining.text = message.text; + return decodeResult; + } +} diff --git a/runtimes/typescript/DecoderPluginInterface.ts b/runtimes/typescript/DecoderPluginInterface.ts new file mode 100644 index 0000000..100873d --- /dev/null +++ b/runtimes/typescript/DecoderPluginInterface.ts @@ -0,0 +1,155 @@ +import { Route } from './types/route'; +import { Wind } from './types/wind'; + +/** + * Representation of a Message + */ +export interface Message { + label: string; + sublabel?: string; + text: string; +} + +/** + * Decoder Options + */ +export interface Options { + debug?: boolean; +} + +/** + * Known fields that can appear on DecodeResult.raw. + * The index signature allows plugin-specific fields without losing type safety + * on the well-known ones. + */ +export interface RawFields { + // Position & navigation + position?: { latitude: number; longitude: number }; + altitude?: number; + heading?: number; + groundspeed?: number; + airspeed?: number; + mach?: number; + + // Flight identification + flight_number?: string; + callsign?: string; + tail?: string; + + // Airports + departure_icao?: string; + departure_iata?: string; + arrival_icao?: string; + arrival_iata?: string; + alternate_icao?: string; + + // Runways + departure_runway?: string; + arrival_runway?: string; + alternate_runway?: string; + + // Times (seconds since midnight or epoch) + message_timestamp?: number; + eta_time?: number; + out_time?: number; + off_time?: number; + on_time?: number; + in_time?: number; + engine_start_time?: number; + engine_stop_time?: number; + + // Date + day?: number | string; + month?: number; + departure_day?: number; + arrival_day?: number; + + // Fuel + fuel_on_board?: number; + fuel_burned?: number; + fuel_remaining?: number; + fuel_in_tons?: number; + out_fuel?: number; + off_fuel?: number; + on_fuel?: number; + in_fuel?: number; + start_fuel?: number; + + // Temperature + outside_air_temperature?: number; + total_air_temperature?: number; + + // Weight & balance + center_of_gravity?: number; + cg_lower_limit?: number; + cg_upper_limit?: number; + mac?: number; + trim?: number; + + // Route & flight plan + route?: Route; + wind_data?: Wind[]; + requested_alts?: number[]; + desired_alt?: number; + start_point?: string; + route_number?: string; + flight_plan?: string; + + // Message metadata + label?: string; + sublabel?: string; + preamble?: string; + version?: number; + checksum_algorithm?: string; + checksum?: number; + sequence_number?: number; + sequence_response?: number; + ground_address?: string; + text?: string; + + // State & events + state_change?: { from: string; to: string }; + door_event?: { door: string; state: string }; + + // Allow plugin-specific fields + [key: string]: unknown; +} + +/** + * Results from decoding a message + */ +export interface DecodeResult { + decoded: boolean; + decoder: { + name: string; + type: 'pattern-match' | 'none'; + decodeLevel: 'none' | 'partial' | 'full'; + }; + error?: string; + formatted: { + description: string; + items: { + type: string; + code: string; + label: string; + value: string; + }[]; + }; + message?: Message; + raw: RawFields; + remaining: { + text?: string; + }; +} + +export interface Qualifiers { + labels: string[]; + preambles?: string[]; +} + +export interface DecoderPluginInterface { + decode(message: Message, options?: Options): DecodeResult; + meetsStateRequirements(): boolean; + // onRegister(store: Store) : void; + qualifiers(): Qualifiers; +} diff --git a/runtimes/typescript/MessageDecoder.ts b/runtimes/typescript/MessageDecoder.ts new file mode 100644 index 0000000..bb157af --- /dev/null +++ b/runtimes/typescript/MessageDecoder.ts @@ -0,0 +1,12 @@ +import type { DecodeResult, Message, Options } from "./DecoderPluginInterface"; + +/** + * Minimal contract for a MessageDecoder — the dispatcher that runs registered + * plugins against a message. The concrete dispatcher lives in the consuming + * language repo (acars-decoder-typescript/lib/MessageDecoder.ts) and implements + * this interface; the runtime package only needs the contract so DecoderPlugin + * can reference it (used by wrapper plugins like CBand that recurse). + */ +export interface MessageDecoder { + decode(message: Message, options?: Options): DecodeResult; +} diff --git a/runtimes/typescript/README.md b/runtimes/typescript/README.md new file mode 100644 index 0000000..764d22f --- /dev/null +++ b/runtimes/typescript/README.md @@ -0,0 +1,56 @@ +# @airframes/ads-runtime-ts + +TypeScript runtime library for ADS-generated ACARS decoder plugins. + +## Layout + +``` +runtimes/typescript/ +├── index.ts # Public exports: DecoderPlugin, types, ResultFormatter, utils +├── helpers.ts # Decode-fn helpers the codegen emits calls into +├── DecoderPlugin.ts # Base class generated plugins extend +├── DecoderPluginInterface.ts # Message, DecodeResult, Qualifiers, RawFields, … +├── MessageDecoder.ts # Interface the dispatcher implements (concrete impl lives in language repo) +├── DateTimeUtils.ts # Date/time helpers (HHMMSS → tod, etc.) +├── utils/ +│ ├── result_formatter.ts # ResultFormatter with 30+ methods +│ ├── coordinate_utils.ts # decodeStringCoordinates, decimal-minutes +│ ├── ascii85.ts # Pure-JS ASCII85 decoder +│ ├── compression.ts # pako wrapper + base64 +│ ├── miam.ts # MIAM PDU parsing +│ ├── arinc_702_helper.ts # H1 field dispatch + CRC validation +│ ├── icao_fpl_utils.ts # ICAO flight plan parsing +│ ├── flight_plan_utils.ts +│ └── route_utils.ts +├── types/ +│ ├── route.ts +│ ├── waypoint.ts +│ └── wind.ts +└── escape_hatches/ # Per-plugin custom-decoder/formatter hatches + └── index.ts # Re-exports each hatch module (added as needed) +``` + +## Source of truth + +Behavior comes from **`acars-decoder-typescript`** (in production). These files are the canonical copies; Stage 2 of the unification removes the duplicates from `acars-decoder-typescript/lib/` and points the language repo at this submodule path via a `tsconfig.json` path alias. + +## Used by + +Generated plugin source (output of `ads-gen --target ts`): + +```ts +import { DecoderPlugin } from "@airframes/ads-runtime-ts"; +import type { DecodeResult, Message, Options } from "@airframes/ads-runtime-ts"; +import { ResultFormatter } from "@airframes/ads-runtime-ts"; +import * as helpers from "@airframes/ads-runtime-ts/helpers"; +import * as hatches from "../escape_hatches"; + +export class Label_10_POS extends DecoderPlugin { /* ... */ } +``` + +## Type-checking + +```bash +npm install +npm run lint +``` diff --git a/runtimes/typescript/escape_hatches/index.ts b/runtimes/typescript/escape_hatches/index.ts new file mode 100644 index 0000000..e94238d --- /dev/null +++ b/runtimes/typescript/escape_hatches/index.ts @@ -0,0 +1,25 @@ +/** + * Per-plugin escape-hatch functions referenced from spec YAML via `custom: `. + * + * One file per plugin that needs hatches; each exports the named functions. + * Generated plugin code does: + * import * as hatches from "../escape_hatches"; + * hatches.(...) + * + * As specs are ported and reveal needed hatches, add the corresponding files + * here and re-export from this index. See docs/ESCAPE_HATCHES.md for the + * naming and signature contract. + * + * Currently expected (from reference specs): + * - arinc_702_dispatch (parse-level whole-plugin hatch) + * - arinc_702_format (formatter-level) + * - label_4a_variant_2_decode (field-level) + * - label_4a_variant_3_position (field-level) + * - label_4a_format (formatter-level) + * - ohma_unwrap_message (field-level) + * - ohma_message_item (formatter-level) + * - parse_flight_level_or_ground (decode-fn level) + * - flight_level_to_altitude_feet (decode-fn level) + */ + +export {}; diff --git a/runtimes/typescript/helpers.ts b/runtimes/typescript/helpers.ts new file mode 100644 index 0000000..dbdcc10 --- /dev/null +++ b/runtimes/typescript/helpers.ts @@ -0,0 +1,182 @@ +/** + * Helper functions the codegen emits calls into. + * + * Generated plugin code looks like: + * import * as helpers from "@airframes/ads-runtime-ts/helpers"; + * const latitude = helpers.coordinate(parts[1], { ... }); + * + * These functions wrap the underlying util modules (CoordinateUtils, + * DateTimeUtils, ascii85, compression) in a uniform shape matching the + * decode-fn semantics defined in spec/shared/decode_fns.yaml. + * + * Source of truth for behavior: acars-decoder-typescript. Behavior here MUST + * match the inline logic of the original plugin source byte-for-byte; the + * shared corpus enforces this. + */ + +import { CoordinateUtils } from "./utils/coordinate_utils"; +import { DateTimeUtils } from "./DateTimeUtils"; +import { base64ToUint8Array, inflateData } from "./utils/compression"; +import { ascii85Decode as ascii85Impl } from "./utils/ascii85"; + +// ─── coordinates ───────────────────────────────────────────────────────────── + +export interface CoordinateArgs { + style?: "single_axis" | "combined"; + axis?: "latitude" | "longitude"; + prefix_chars?: string[]; + digits?: number; + divisor?: number; + format?: string; +} + +/** + * Coordinate decoder. Two styles: + * - single_axis: input like "N12345" → ((sign) * Number(rest)) / divisor + * - combined: input like "N12345W123456" → {latitude, longitude} via CoordinateUtils + */ +export function coordinate(value: string, args: CoordinateArgs = {}): number | { latitude: number; longitude: number } | undefined { + const trimmed = value.trim(); + if (args.style === "combined") { + return CoordinateUtils.decodeStringCoordinates(trimmed); + } + // Default: single-axis prefixed + const prefixChars = args.prefix_chars ?? ["N", "S", "E", "W"]; + const divisor = args.divisor ?? 1000; + const positive = ["N", "E"]; + const prefix = trimmed[0]; + if (prefix === undefined || !prefixChars.includes(prefix)) return undefined; + const sign = positive.includes(prefix) ? 1 : -1; + const digits = trimmed.substring(1).trim(); + return (sign * Number(digits)) / divisor; +} + +export function coordinateDecimalMinutes(value: string, _args: CoordinateArgs = {}): { latitude: number; longitude: number } | undefined { + return CoordinateUtils.decodeStringCoordinatesDecimalMinutes(value); +} + +// ─── numerics ──────────────────────────────────────────────────────────────── + +export interface IntegerArgs { + substring_start?: number; + substring_length?: number; + multiplier?: number; +} + +export function integer(value: string, args: IntegerArgs = {}): number { + let s = value; + if (args.substring_start !== undefined || args.substring_length !== undefined) { + const start = args.substring_start ?? 0; + const end = args.substring_length !== undefined ? start + args.substring_length : undefined; + s = end !== undefined ? s.substring(start, end) : s.substring(start); + } + const n = Number(s); + return args.multiplier ? n * args.multiplier : n; +} + +export function float(value: string, args: { multiplier?: number } = {}): number { + const n = Number(value); + return args.multiplier ? n * args.multiplier : n; +} + +// ─── strings ───────────────────────────────────────────────────────────────── + +export function string(value: unknown): string { + return String(value); +} +export function trim(value: string): string { + return value.trim(); +} +export function uppercase(value: string): string { + return value.toUpperCase(); +} +export function lowercase(value: string): string { + return value.toLowerCase(); +} + +// ─── identifiers ───────────────────────────────────────────────────────────── + +export function callsign(value: string): string { + return value.trim(); +} + +export function tailNumber(value: string, args: { strip_chars?: string } = {}): string { + let s = value.trim(); + if (args.strip_chars) { + for (const c of args.strip_chars) s = s.split(c).join(""); + } + return s; +} + +export function flightNumber(value: string): string { + return value.trim(); +} + +export function airport(value: string): string { + return value.trim().toUpperCase(); +} + +// ─── timestamps ────────────────────────────────────────────────────────────── + +export function timestampHhmmss(value: string, args: { append?: string } = {}): number { + const s = args.append ? value + args.append : value; + return DateTimeUtils.convertHHMMSSToTod(s); +} + +// ─── binary / encoding ─────────────────────────────────────────────────────── + +export function base64ToUint8ArrayHelper(value: string): Uint8Array { + return base64ToUint8Array(value); +} +export { base64ToUint8ArrayHelper as base64ToUint8Array }; + +export function inflate(bytes: Uint8Array, _format: "raw" | "zlib" | "gzip" = "raw"): Uint8Array { + // inflateData(bytes, isZlib) — false → raw + const out = inflateData(bytes, false); + if (!out) throw new Error("inflate failed"); + return out; +} + +const _textDecoderCache = new Map(); +export function textDecode(bytes: Uint8Array, encoding: "utf-8" | "ascii" | "latin1" = "utf-8"): string { + let dec = _textDecoderCache.get(encoding); + if (!dec) { + dec = new TextDecoder(encoding); + _textDecoderCache.set(encoding, dec); + } + return dec.decode(bytes); +} + +export function decodeAscii85(value: string): Uint8Array { + const out = ascii85Impl(value); + if (out === null) throw new Error("ASCII85 decode failed"); + return out; +} + +export function hexDecode(value: string): Uint8Array { + const clean = value.replace(/[^0-9a-fA-F]/g, ""); + const out = new Uint8Array(clean.length / 2); + for (let i = 0; i < out.length; i++) { + out[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16); + } + return out; +} + +// ─── bitfield ──────────────────────────────────────────────────────────────── + +/** + * Extract bits [start..end] (inclusive on both ends) from a byte. + * Bit 0 = MSB by convention here (matches the spec's "0:3" = top 4 bits). + */ +export function bitslice(byte: number, start: number, end: number): number { + const width = end - start + 1; + const shift = 8 - end - 1; + const mask = (1 << width) - 1; + return (byte >> shift) & mask; +} + +export function concatBits(values: number[]): number { + // Placeholder — each spec defines its own width contract. Real impl will + // accept (value, width) pairs in v1.1. + return values.reduce((acc, v) => (acc << 8) | v, 0); +} diff --git a/runtimes/typescript/index.ts b/runtimes/typescript/index.ts new file mode 100644 index 0000000..c35c8e9 --- /dev/null +++ b/runtimes/typescript/index.ts @@ -0,0 +1,29 @@ +/** + * @airframes/ads-runtime-ts + * + * The TypeScript runtime library for ADS-generated plugins. Provides the + * DecoderPlugin base class, message/result types, the ResultFormatter, and + * the shared utility modules previously hosted in acars-decoder-typescript/lib/. + * + * Generated plugins (output of `ads-gen --target ts`) import from this package + * via the @airframes/ads-runtime-ts path alias configured in the consuming + * repo's tsconfig. + */ + +export { DecoderPlugin } from "./DecoderPlugin"; +export type { MessageDecoder } from "./MessageDecoder"; +export type { + DecodeResult, + DecoderPluginInterface, + Message, + Options, + Qualifiers, + RawFields, +} from "./DecoderPluginInterface"; +export { ResultFormatter } from "./utils/result_formatter"; +export { CoordinateUtils } from "./utils/coordinate_utils"; +export { DateTimeUtils } from "./DateTimeUtils"; + +export type { Route } from "./types/route"; +export type { Waypoint } from "./types/waypoint"; +export type { Wind } from "./types/wind"; diff --git a/runtimes/typescript/package-lock.json b/runtimes/typescript/package-lock.json new file mode 100644 index 0000000..5e02911 --- /dev/null +++ b/runtimes/typescript/package-lock.json @@ -0,0 +1,50 @@ +{ + "name": "@airframes/ads-runtime-ts", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@airframes/ads-runtime-ts", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "pako": "^2.1.0" + }, + "devDependencies": { + "@types/pako": "^2.0.4", + "typescript": "^5.7.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/runtimes/typescript/package.json b/runtimes/typescript/package.json new file mode 100644 index 0000000..64f5947 --- /dev/null +++ b/runtimes/typescript/package.json @@ -0,0 +1,34 @@ +{ + "name": "@airframes/ads-runtime-ts", + "version": "0.1.0", + "description": "TypeScript runtime library for ADS-generated ACARS decoder plugins. Consumed by acars-decoder-typescript via path alias or path dependency from the airframes-decoder submodule.", + "type": "module", + "main": "./index.ts", + "exports": { + ".": "./index.ts", + "./helpers": "./helpers.ts", + "./escape_hatches": "./escape_hatches/index.ts" + }, + "files": [ + "*.ts", + "utils/", + "types/", + "escape_hatches/", + "README.md" + ], + "scripts": { + "lint": "tsc --noEmit -p tsconfig.json" + }, + "dependencies": { + "pako": "^2.1.0" + }, + "devDependencies": { + "@types/pako": "^2.0.4", + "typescript": "^5.7.2" + }, + "engines": { + "node": ">=18" + }, + "license": "MIT", + "keywords": ["acars", "decoder", "runtime"] +} diff --git a/runtimes/typescript/tsconfig.json b/runtimes/typescript/tsconfig.json new file mode 100644 index 0000000..b0cacbf --- /dev/null +++ b/runtimes/typescript/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM"], + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/runtimes/typescript/types/route.ts b/runtimes/typescript/types/route.ts new file mode 100644 index 0000000..5922fea --- /dev/null +++ b/runtimes/typescript/types/route.ts @@ -0,0 +1,17 @@ +import { Waypoint } from './waypoint'; + +/** + * Representation of a route + * + * Typically a list of waypoints, this can also be a named company route. + */ +export interface Route { + /** optional name. If not set, `waypoints` is required */ + name?: string; + + /** optional runway */ + runway?: string; + + /** optional list of waypoints. If not set, `name` is required */ + waypoints?: Waypoint[]; +} diff --git a/runtimes/typescript/types/waypoint.ts b/runtimes/typescript/types/waypoint.ts new file mode 100644 index 0000000..f4eae3a --- /dev/null +++ b/runtimes/typescript/types/waypoint.ts @@ -0,0 +1,36 @@ +/** + * Represenation of a waypoint. + * + * Usually used in Routes which is an array of waypoints. + * Airways/Jetways can also be represented as a waypoint, by just the name. + * There is no distinction between the two currently because there is no difference from the current messages + * Distinction must be determined from tne name. + * In the event that a waypoint is a GPS position, the name can be the raw position string (N12345W012345) + */ +export interface Waypoint { + /** unique identifier of the waypoint*/ + name: string; + /** + * latitude in decimal degrees + * + * if set, longitude must be provided + */ + latitude?: number; + /** longitude in decimal degrees + * + * if set, latitude must be provided + */ + longitude?: number; + /** + * time of arrival. If in future, it is an ETA. + */ + time?: number; + + /** + * offset from the actual waypoint + * + * bearing: degrees from the waypoint + * distance: distance in nautical miles + */ + offset?: { bearing: number; distance: number }; +} diff --git a/runtimes/typescript/types/wind.ts b/runtimes/typescript/types/wind.ts new file mode 100644 index 0000000..1205872 --- /dev/null +++ b/runtimes/typescript/types/wind.ts @@ -0,0 +1,12 @@ +import { Waypoint } from './waypoint'; + +export interface Wind { + waypoint: Waypoint; + flightLevel: number; + windDirection: number; + windSpeed: number; + temperature?: { + flightLevel: number; + degreesC: number; + }; +} diff --git a/runtimes/typescript/utils/arinc_702_helper.ts b/runtimes/typescript/utils/arinc_702_helper.ts new file mode 100644 index 0000000..250d652 --- /dev/null +++ b/runtimes/typescript/utils/arinc_702_helper.ts @@ -0,0 +1,726 @@ +import { DateTimeUtils } from '../DateTimeUtils'; +import { DecodeResult } from '../DecoderPluginInterface'; +import { Waypoint } from '../types/waypoint'; +import { Wind } from '../types/wind'; +import { CoordinateUtils } from './coordinate_utils'; +import { FlightPlanUtils } from './flight_plan_utils'; +import { ResultFormatter } from './result_formatter'; +import { RouteUtils } from './route_utils'; + +// One byte per ASCII char — replaces the Buffer.from(data, 'ascii') idiom so +// the helper runs in browser / Deno / Bun without Node polyfills. +function asciiStringToBytes(data: string): Uint8Array { + const bytes = new Uint8Array(data.length); + for (let i = 0; i < data.length; i++) bytes[i] = data.charCodeAt(i) & 0xff; + return bytes; +} + +/** + * Helper class for decoding ARINC 702 messages + * contains core logic for decoding different types of ARINC 702 messages, + * as well as utility functions for processing common fields + * + * Core format: + * IMI/IEIdata/IEIdata/...checksum + */ +export class Arinc702Helper { + /** + * Decodes an ARINC 702 message + * + * Steps: + * 1. Check against known checksum algorithms to determine if message is valid + * 2. Parse the Imbedded Message Identifier (IMI) to determine the type of message + * 3. Iterate through the Information Elements (IEI) and decode known fields, while storing unknown fields for further analysis + * + * @param decodeResult - object to store decoded results in + * @param message - message to decode + * @returns boolean indicating if the message was successfully decoded + */ + public static decodeH1Message(decodeResult: DecodeResult, message: string) { + decodeResult.remaining.text = ''; + const checksum = parseInt(message.slice(-4), 16); + const data = message.slice(0, message.length - 4); + let checksumAlgorithmUsed = ''; + if (crc16IbmSdlcRev(data) === checksum) { + // ACARS/VDL2/HFDL/Iridium + checksumAlgorithmUsed = 'IBM-SDLC reversed'; + } else if (crc16Genibus(data) === checksum) { + // inmarsat + checksumAlgorithmUsed = 'GENIBUS'; + } else { + decodeResult.decoded = false; + decodeResult.decoder.decodeLevel = 'none'; + return false; + } + const fields = data.split('/'); + const canDecode = parseMessageType(decodeResult, fields[0]); + if (!canDecode) { + decodeResult.decoded = false; + decodeResult.decoder.decodeLevel = 'none'; + return false; + } + for (let i = 1; i < fields.length; ++i) { + const iei = fields[i].substring(0, 2); + const data = fields[i].substring(2); + switch (iei) { + case 'AF': + processAirField(decodeResult, data.split(',')); + break; + case 'AK': + ResultFormatter.unknown(decodeResult, fields[i], '/'); + // processAK(decodeResult, data.split(',')); + break; + case 'CG': + processCenterOfGravity(decodeResult, data.split(',')); + break; + case 'DC': + processDateCode(decodeResult, data.split(',')); + break; + case 'DI': + ResultFormatter.unknown(decodeResult, fields[i], '/'); + // processDI(decodeResult, data.split(',')); + break; + case 'DT': //processDestination? + processDT(decodeResult, data.split(',')); + break; + case 'DQ': + ResultFormatter.desiredAltitude( + decodeResult, + parseInt(data, 10) * 100, + ); + // processDQ(decodeResult, data.split(',')); + break; + case 'ET': + processETA(data, decodeResult, fields, i); + break; + case 'FB': + ResultFormatter.currentFuel(decodeResult, parseInt(data, 10)); + break; + case 'FN': + decodeResult.raw.flight_number = data; + break; + case 'FX': + ResultFormatter.text(decodeResult, data); + break; + case 'GA': + ResultFormatter.groundAddress(decodeResult, data); + break; + case 'ID': + processIdentification(decodeResult, data.split(',')); + break; + case 'LR': + processLandingReport(decodeResult, data.split(',')); + break; + case 'MR': + processMessageReference(decodeResult, data.split(',')); + break; + case 'PR': + processPerformanceData(decodeResult, data.split(',')); + break; + //case 'PR': + // TODO: decode /PR fields + // ResultFormatter.unknown(decodeResult, fields[i], '/'); + // break; + case 'PS': // Position + Arinc702Helper.processPS(decodeResult, data.split(',')); + break; + case 'RF': + case 'RI': + case 'RM': + case 'RP': + case 'RS': + // TODO - use key/data instead of whole message + FlightPlanUtils.processFlightPlan(decodeResult, fields[i].split(':')); + break; + case 'RN': + ResultFormatter.routeNumber(decodeResult, data); + break; + case 'RW': + processRunway(decodeResult, data.split(':')); + break; + case 'SM': + processSummary(decodeResult, data.split(',')); + break; + case 'SN': + decodeResult.raw.serial_number = data; + break; + case 'SP': + ResultFormatter.startPoint(decodeResult, data); + break; + case 'TD': + processTimeOfDeparture(decodeResult, data.split(',')); + break; + case 'TS': + Arinc702Helper.processTimeStamp(decodeResult, data.split(',')); + break; + case 'VR': + ResultFormatter.version(decodeResult, parseInt(data, 10) / 10); + break; + case 'WD': + processWindData(decodeResult, data); + break; + case 'WQ': + processWeatherQuery(decodeResult, data.split(':')); + break; + default: + ResultFormatter.unknown(decodeResult, fields[i], '/'); + } + } + + ResultFormatter.checksumAlgorithm(decodeResult, checksumAlgorithmUsed); + ResultFormatter.checksum(decodeResult, checksum); + + return true; + } + + public static processPS(decodeResult: DecodeResult, data: string[]) { + const position = CoordinateUtils.decodeStringCoordinatesDecimalMinutes( + data[0], + ); + if (position) { + ResultFormatter.position(decodeResult, position); + } + if (data.length === 9) { + // variant 7 + processRoute(decodeResult, data[3], data[1], data[5], data[4], undefined); + ResultFormatter.altitude(decodeResult, Number(data[2]) * 100); + ResultFormatter.temperature(decodeResult, data[6]); + ResultFormatter.unknown(decodeResult, data[7]); + ResultFormatter.unknown(decodeResult, data[8]); + } + if (data.length === 14) { + // variant 2 + ResultFormatter.altitude(decodeResult, Number(data[3]) * 100); + processRoute(decodeResult, data[1], data[2], data[4], data[5], data[6]); + ResultFormatter.temperature(decodeResult, data[7]); + ResultFormatter.unknownArr(decodeResult, data.slice(8)); + } + } + + public static processPosition(decodeResult: DecodeResult, data: string[]) { + const position = CoordinateUtils.decodeStringCoordinatesDecimalMinutes( + data[0], + ); + if (position) { + ResultFormatter.position(decodeResult, position); + } + if (data.length >= 10) { + // variant 1, short + ResultFormatter.altitude(decodeResult, Number(data[3]) * 100); + processRoute(decodeResult, data[1], data[2], data[4], data[5], data[6]); + ResultFormatter.temperature(decodeResult, data[7]); + ResultFormatter.unknown(decodeResult, data[8]); + ResultFormatter.unknown(decodeResult, data[9]); + } + if (data.length >= 14) { + // variant 2,long + ResultFormatter.unknownArr(decodeResult, data.slice(10)); + } + } + + public static processTimeStamp(decodeResult: DecodeResult, data: string[]) { + if (data.length > 2) { + const positionData = data.slice(1); + positionData[0] = positionData[0].substring(6); // strip time from position field + this.processPosition(decodeResult, positionData); + } + let time = DateTimeUtils.convertDateTimeToEpoch( + data[0], + data[1].substring(0, 6), + ); + + if (Number.isNaN(time)) { + // convert DDMMYY to MMDDYY - TODO figure out a better way to determine + const date = + data[1].substring(2, 4) + + data[1].substring(0, 2) + + data[1].substring(4, 6); + time = DateTimeUtils.convertDateTimeToEpoch(data[0], date); + } + decodeResult.raw.message_timestamp = time; + } +} + +function processETA( + data: string, + decodeResult: DecodeResult, + fields: string[], + i: number, +) { + if (data.length === 5) { + // 1 digit day + ResultFormatter.day(decodeResult, Number(data.substring(0, 1))); + ResultFormatter.eta( + decodeResult, + DateTimeUtils.convertHHMMSSToTod(data.substring(1)), + ); + } else if (data.length === 6) { + // 2 digit day + ResultFormatter.day(decodeResult, Number(data.substring(0, 2))); + ResultFormatter.eta( + decodeResult, + DateTimeUtils.convertHHMMSSToTod(data.substring(2)), + ); + } else { + ResultFormatter.unknown(decodeResult, fields[i], '/'); + } +} + +function processAirField(decodeResult: DecodeResult, data: string[]) { + if (data.length === 2) { + ResultFormatter.departureAirport(decodeResult, data[0]); + ResultFormatter.arrivalAirport(decodeResult, data[1]); + } else { + ResultFormatter.unknown(decodeResult, data.join(','), 'AF/'); + } +} +function processTimeOfDeparture(decodeResult: DecodeResult, data: string[]) { + if (data.length === 2) { + decodeResult.raw.planned_departure_time = data[0]; //DDHHMM - TODO: make int + decodeResult.formatted.items.push({ + type: 'ptd', + code: 'ptd', + label: 'Planned Departure Time', + value: `YYYY-MM-${data[0].substring(0, 2)}T${data[0].substring(2, 4)}:${data[0].substring(4)}:00Z`, + }); + + decodeResult.raw.estimated_departure_time = data[1]; //HHMM - TODO: make int + decodeResult.formatted.items.push({ + type: 'etd', + code: 'etd', + label: 'Estimated Departure Time', + value: `${data[1].substring(0, 2)}:${data[1].substring(2)}`, + }); + } else { + ResultFormatter.unknown(decodeResult, data.join(','), '/TD'); + } +} + +function processIdentification(decodeResult: DecodeResult, data: string[]) { + ResultFormatter.tail(decodeResult, data[0]); + if (data.length > 1) { + ResultFormatter.flightNumber(decodeResult, data[1]); + } + if (data.length > 2) { + //TODO: figure out what this is + decodeResult.raw.mission_number = data[2]; + } +} + +function processDT(decodeResult: DecodeResult, data: string[]) { + if (data.length === 9) { + ResultFormatter.unknown(decodeResult, data[0]); + if (!decodeResult.raw.arrival_icao) { + ResultFormatter.arrivalAirport(decodeResult, data[1]); + } + ResultFormatter.arrivalRunway(decodeResult, data[2]); + ResultFormatter.currentFuel(decodeResult, parseInt(data[3], 10)); + ResultFormatter.eta( + decodeResult, + DateTimeUtils.convertHHMMSSToTod(data[4]), + ); + ResultFormatter.unknown(decodeResult, data[5]); + ResultFormatter.position( + decodeResult, + CoordinateUtils.decodeStringCoordinates(data[6]), + ); + ResultFormatter.unknown(decodeResult, data[7]); + ResultFormatter.unknown(decodeResult, data[8]); + return; + } + if (data.length === 4 || data.length === 5) { + if (!decodeResult.raw.arrival_icao) { + ResultFormatter.arrivalAirport(decodeResult, data[0]); + } + ResultFormatter.arrivalRunway(decodeResult, data[1]); + ResultFormatter.currentFuel(decodeResult, parseInt(data[2], 10)); + ResultFormatter.eta( + decodeResult, + DateTimeUtils.convertHHMMSSToTod(data[3]), + ); + if (data.length > 4) { + ResultFormatter.remainingFuel(decodeResult, Number(data[4])); + } + return; + } + ResultFormatter.unknownArr(decodeResult, data, ','); +} + +function processLandingReport(decodeResult: DecodeResult, data: string[]) { + if (data.length === 19) { + ResultFormatter.unknown(decodeResult, data[1]); + ResultFormatter.flightNumber(decodeResult, data[2]); + ResultFormatter.departureAirport(decodeResult, data[3]); + ResultFormatter.arrivalAirport(decodeResult, data[4]); + ResultFormatter.arrivalRunway(decodeResult, data[5]); + ResultFormatter.unknown(decodeResult, data.slice(6, 19).join(',')); + } else { + ResultFormatter.unknown(decodeResult, data.join(',')); + } +} + +function processCenterOfGravity(decodeResult: DecodeResult, data: string[]) { + if (data.length === 1) { + if (data[0] !== undefined && data[0] !== '') { + ResultFormatter.cg(decodeResult, parseInt(data[0], 10) / 10); + } + } else if (data.length === 3) { + if (data[0] !== undefined && data[0] !== '') { + ResultFormatter.cg(decodeResult, parseInt(data[0], 10) / 10, 'center'); + } + if (data[1] !== undefined && data[1] !== '') { + ResultFormatter.cg(decodeResult, parseInt(data[1], 10) / 10, 'lower'); + } + if (data[2] !== undefined && data[2] !== '') { + ResultFormatter.cg(decodeResult, parseInt(data[2], 10) / 10, 'upper'); + } + } else { + ResultFormatter.unknown(decodeResult, data.join(',')); + } +} + +function parseMessageType( + decodeResult: DecodeResult, + messagePart: string, +): boolean { + const messageParts = messagePart.split(','); + const messageType = messageParts[0]; + if (messageType.startsWith('POS')) { + Arinc702Helper.processPosition( + decodeResult, + messagePart.substring(3).split(','), + ); + return processIMI(decodeResult, 'POS'); + } else if (messageType.length === 6) { + const part1 = processIMI(decodeResult, messageType.substring(0, 3)); + const description = decodeResult.formatted.description; + const part2 = processIMI(decodeResult, messageType.substring(3, 6)); + decodeResult.formatted.description = + description + ' for ' + decodeResult.formatted.description; + if (messageParts.length > 1) { + // TODO handle REJPWI/REJPOS + ResultFormatter.unknown( + decodeResult, + messageParts.slice(1).join(','), + '/', + ); + } + return part1 && part2; + } + + if (messageParts.length > 1) { + // TODO handle + ResultFormatter.unknown(decodeResult, messageParts.slice(1).join(','), '/'); + } + + return processIMI(decodeResult, messageType); +} + +/** + * Processes the Imbedded Message Identifier (IMI) + * which indicates the type of message and is used to determine what the message type is + * @param decodeResult + * @param type + * @returns + */ +function processIMI(decodeResult: DecodeResult, type: string): boolean { + if (type === 'FPN') { + decodeResult.formatted.description = 'Flight Plan'; + } else if (type === 'FTX') { + decodeResult.formatted.description = 'Free Text'; + } else if (type === 'INI') { + decodeResult.formatted.description = 'Initial Report'; + } else if (type === 'INR') { + decodeResult.formatted.description = 'In-Range Report'; + } else if (type === 'LDI') { + decodeResult.formatted.description = 'Load Distribution Information'; + } else if (type === 'PER') { + decodeResult.formatted.description = 'Performance Report'; + } else if (type === 'POS') { + decodeResult.formatted.description = 'Position Report'; + } else if (type === 'PRG') { + decodeResult.formatted.description = 'Progress Report'; + } else if (type === 'PWI') { + decodeResult.formatted.description = 'Pilot Weather Information'; + } else if (type === 'WXR') { + decodeResult.formatted.description = 'Weather Report'; + } else if (type === 'REJ') { + decodeResult.formatted.description = 'Reject'; + } else if (type === 'REQ') { + decodeResult.formatted.description = 'Request'; + } else if (type === 'RES') { + decodeResult.formatted.description = 'Response'; + } else if (type === 'SUM') { + decodeResult.formatted.description = 'Summary Report'; + } else { + decodeResult.formatted.description = 'Unknown H1 Message'; + return false; + } + return true; +} + +function processPerformanceData(decodeResult: DecodeResult, data: string[]) { + // /PR fields contain performance data + // Known field positions (12-field short variant and 18-field long variant): + // [0]: ground speed (knots * 10 or similar) + // [1]: indicated airspeed or mach-related + // [2]: altitude in hundreds of feet + // [3]: fuel-related value + // [4]: unknown (often empty) + // [5]: fuel flow or consumption + // [6]: fuel-related + // [7]: wind data (DDDSSSS format) or empty + // [8]: temperature (M=minus, P=plus) + // [9]: heading or unknown + + if (data.length < 3) { + ResultFormatter.unknownArr(decodeResult, data, ','); + return; + } + + // Field 2: altitude in hundreds + if (data[2] !== undefined && data[2] !== '') { + const alt = Number(data[2]) * 100; + if (!isNaN(alt) && alt > 0) { + ResultFormatter.altitude(decodeResult, alt); + } + } + + // Field 8: temperature (if present) + if (data.length > 8 && data[8] !== undefined && data[8] !== '') { + ResultFormatter.temperature(decodeResult, data[8]); + } + + // Collect remaining undecoded fields + const remaining: string[] = []; + for (let i = 0; i < data.length; i++) { + if ( + i === 2 && + data[i] !== '' && + !isNaN(Number(data[i])) && + Number(data[i]) * 100 > 0 + ) { + continue; // altitude - already decoded + } + if ( + i === 8 && + data[i] !== '' && + (data[i].startsWith('M') || + data[i].startsWith('P') || + !isNaN(Number(data[i]))) + ) { + continue; // temperature - already decoded + } + remaining.push(data[i]); + } + if (remaining.length > 0 && remaining.some((r) => r !== '')) { + ResultFormatter.unknown(decodeResult, remaining.join(','), '/PR'); + } +} + +function processDateCode(decodeResult: DecodeResult, data: string[]) { + if (data.length === 1) { + // noop? + } else if (data.length === 2) { + // convert DDMMYY to MMDDYY - TODO figure out a better way to determine + const date = + data[0].substring(2, 4) + + data[0].substring(0, 2) + + data[0].substring(4, 6); + const time = DateTimeUtils.convertDateTimeToEpoch(data[1], data[0]); // HHMMSS + + decodeResult.raw.message_timestamp = time; + } +} + +function processWeatherQuery(decodeResult: DecodeResult, data: string[]) { + if (data.length !== 2) { + ResultFormatter.unknown(decodeResult, data.join(':'), 'WQ/'); + return; + } + + const alts = data[0].split('.'); + const route = data[1].split('.'); + + ResultFormatter.requestedAltitudes( + decodeResult, + alts.map((a) => parseInt(a, 10) * 100), + ); + + const waypoints = route.map((wp) => { + return { name: wp }; + }); + ResultFormatter.route(decodeResult, { waypoints: waypoints }); +} + +function processRoute( + decodeResult: DecodeResult, + last: string, + time: string, + next: string, + eta: string, + then?: string, + date?: string, +) { + const lastTime = date + ? DateTimeUtils.convertDateTimeToEpoch(time, date) + : DateTimeUtils.convertHHMMSSToTod(time); + const nextTime = date + ? DateTimeUtils.convertDateTimeToEpoch(eta, date) + : DateTimeUtils.convertHHMMSSToTod(eta); + + const lastWaypoint = RouteUtils.getWaypoint(last); + lastWaypoint.time = lastTime; + + const nextWaypoint = RouteUtils.getWaypoint(next); + nextWaypoint.time = nextTime; + + const thenWaypoint = RouteUtils.getWaypoint(then || '?'); + + const waypoints: Waypoint[] = [lastWaypoint, nextWaypoint, thenWaypoint]; + ResultFormatter.route(decodeResult, { waypoints: waypoints }); +} + +function processWindData(decodeResult: DecodeResult, message: string) { + const wind = [] as Wind[]; + + const flightLevel = Number(message.slice(0, 3)); + const fields = message.slice(4).split('.'); // strip off altitude and comma + fields.forEach((field) => { + if (field.length < 4) { + // probably need a more robust check to determine to skip + return; + } + const data = field.split(','); + const waypoint = { name: data[0] }; + const windData = data[1]; + const windDirection = parseInt(windData.slice(0, 3), 10); + const windSpeed = parseInt(windData.slice(3), 10); + + if (data.length === 3) { + const tempData = data[2]; + const tempFlightLevel = Number(tempData.slice(0, 3)); + const tempString = tempData.slice(3); + const tempDegrees = + Number(tempString.substring(1)) * + (tempString.charAt(0) === 'M' ? -1 : 1); + wind.push({ + waypoint: waypoint, + flightLevel: flightLevel, + windDirection: windDirection, + windSpeed: windSpeed, + temperature: { + flightLevel: tempFlightLevel, + degreesC: tempDegrees, + }, + }); + } else { + wind.push({ + waypoint: waypoint, + flightLevel: flightLevel, + windDirection: windDirection, + windSpeed: windSpeed, + }); + } + }); + + ResultFormatter.windData(decodeResult, wind); +} + +function processRunway(decodeResult: DecodeResult, data: string[]) { + //FIXME - figure out names + const parts = data[0].split('.'); + const more = parts[0].split(','); + + ResultFormatter.departureRunway(decodeResult, more[0]); + ResultFormatter.unknownArr(decodeResult, more.slice(1), ','); + ResultFormatter.unknownArr(decodeResult, parts.slice(1), '.'); + ResultFormatter.departureAirport(decodeResult, data[1].split(',')[0]); + ResultFormatter.unknownArr(decodeResult, data[1].split(',').slice(1), ','); + //ResultFormatter.unknownArr(decodeResult, data.slice(2), ':'); +} +function processSummary(decodeResult: DecodeResult, data: string[]) { + if (data.length !== 11) { + return ResultFormatter.unknown(decodeResult, data.join(','), 'SM/'); + } + + ResultFormatter.engineStart( + decodeResult, + DateTimeUtils.convertDayTimeToTod(data[0]), + ); + ResultFormatter.startFuel(decodeResult, 100 * parseInt(data[1], 10)); + ResultFormatter.engineStop( + decodeResult, + DateTimeUtils.convertDayTimeToTod(data[2]), + ); + ResultFormatter.out(decodeResult, DateTimeUtils.convertDayTimeToTod(data[3])); + ResultFormatter.outFuel(decodeResult, 100 * parseInt(data[4], 10)); + ResultFormatter.off(decodeResult, DateTimeUtils.convertDayTimeToTod(data[5])); + ResultFormatter.offFuel(decodeResult, 100 * parseInt(data[6], 10)); + ResultFormatter.on(decodeResult, DateTimeUtils.convertDayTimeToTod(data[7])); + ResultFormatter.onFuel(decodeResult, 100 * parseInt(data[8], 10)); + ResultFormatter.in(decodeResult, DateTimeUtils.convertDayTimeToTod(data[9])); + ResultFormatter.inFuel(decodeResult, 100 * parseInt(data[10], 10)); +} + +// CRC-16/IBM-SDLC but nibbles are reversed +function crc16IbmSdlcRev(data: string): number { + let crc = 0xffff; + const bytes = asciiStringToBytes(data); + + for (const byte of bytes) { + crc ^= byte; + for (let i = 0; i < 8; i++) { + if ((crc & 0x0001) !== 0) { + crc = (crc >>> 1) ^ 0x8408; + } else { + crc = crc >>> 1; + } + } + } + crc = (crc ^ 0xffff) & 0xffff; + + const nibble1 = (crc >> 12) & 0xf; + const nibble2 = (crc >> 8) & 0xf; + const nibble3 = (crc >> 4) & 0xf; + const nibble4 = crc & 0xf; + + return (nibble4 << 12) | (nibble3 << 8) | (nibble2 << 4) | nibble1; +} + +/** + * Calculates the CRC-16/GENIBUS checksum for a given Uint8Array. + * @param data The input data as a byte array. + * @returns The 16-bit checksum as a number. + */ +function crc16Genibus(data: string): number { + let crc = 0xffff; + const polynomial = 0x1021; + + const bytes = asciiStringToBytes(data); + + for (const byte of bytes) { + crc ^= byte << 8; + + for (let i = 0; i < 8; i++) { + if ((crc & 0x8000) !== 0) { + crc = ((crc << 1) ^ polynomial) & 0xffff; + } else { + crc = (crc << 1) & 0xffff; + } + } + } + + return (crc ^ 0xffff) & 0xffff; +} +function processMessageReference(decodeResult: DecodeResult, data: string[]) { + if (data.length !== 2) { + ResultFormatter.unknown(decodeResult, data.join(','), '/MR'); + return; + } + + ResultFormatter.sequenceNumber(decodeResult, parseInt(data[0], 10)); + if (data[1] !== '') { + ResultFormatter.sequenceResponse(decodeResult, parseInt(data[1], 10)); + } +} diff --git a/runtimes/typescript/utils/ascii85.ts b/runtimes/typescript/utils/ascii85.ts new file mode 100644 index 0000000..5a3a7fe --- /dev/null +++ b/runtimes/typescript/utils/ascii85.ts @@ -0,0 +1,51 @@ +/** + * Pure JavaScript ASCII85 (Adobe variant) decoder. + * Replaces the `base85` npm package to eliminate Node.js Buffer dependency. + * Works in browsers, Node.js 18+, Deno, Bun, and edge runtimes. + */ +export function ascii85Decode(input: string): Uint8Array | null { + let str = input; + if (str.startsWith('<~')) str = str.slice(2); + if (str.endsWith('~>')) str = str.slice(0, -2); + str = str.replace(/\s/g, ''); + if (str.length === 0) return new Uint8Array(0); + + const output: number[] = []; + let i = 0; + + while (i < str.length) { + if (str[i] === 'z') { + output.push(0, 0, 0, 0); + i++; + continue; + } + + const remaining = str.length - i; + const chunkLen = Math.min(5, remaining); + if (chunkLen === 1) break; // trailing single char from truncated input; ignore it + + let padded = str.slice(i, i + chunkLen); + while (padded.length < 5) padded += 'u'; + + let value = 0; + for (let j = 0; j < 5; j++) { + const digit = padded.charCodeAt(j) - 33; + if (digit < 0 || digit > 84) return null; + value = value * 85 + digit; + } + + if (chunkLen === 5 && value > 0xffffffff) return null; + + // Use >>> 0 to simulate uint32 overflow for padded groups + const v = value >>> 0; + const numBytes = chunkLen === 5 ? 4 : chunkLen - 1; + if (numBytes >= 1) output.push((v >>> 24) & 0xff); + if (numBytes >= 2) output.push((v >>> 16) & 0xff); + if (numBytes >= 3) output.push((v >>> 8) & 0xff); + if (numBytes >= 4) output.push(v & 0xff); + + i += chunkLen; + } + + return new Uint8Array(output); +} diff --git a/runtimes/typescript/utils/compression.ts b/runtimes/typescript/utils/compression.ts new file mode 100644 index 0000000..ddd25d9 --- /dev/null +++ b/runtimes/typescript/utils/compression.ts @@ -0,0 +1,56 @@ +import * as pako from 'pako'; + +/** + * Inflate compressed data with support for partial/truncated streams. + * Uses Z_SYNC_FLUSH to extract as much data as possible, even from + * incomplete deflate streams. Captures output via onData callback. + * + * @param data - The compressed input bytes + * @param raw - If true, use raw deflate (no zlib header); if false, expect zlib header + * @returns The decompressed bytes, or undefined if no output was produced + */ +export function inflateData( + data: Uint8Array, + raw: boolean, +): Uint8Array | undefined { + const chunks: Uint8Array[] = []; + const inflator = new pako.Inflate({ windowBits: raw ? -15 : 15 }); + inflator.onData = (chunk: Uint8Array) => { + chunks.push(chunk); + }; + inflator.push(data, 2); // Z_SYNC_FLUSH + + if (chunks.length === 0) return undefined; + if (chunks.length === 1) return chunks[0]; + + const totalLen = chunks.reduce((sum, c) => sum + c.length, 0); + const result = new Uint8Array(totalLen); + let offset = 0; + for (const chunk of chunks) { + result.set(chunk, offset); + offset += chunk.length; + } + return result; +} + +/** + * Decode a base64 or base64url string to Uint8Array. + * Handles missing padding and converts base64url chars (- and _) to + * standard base64 chars (+ and /), matching Buffer.from(str, 'base64') behavior. + * + * @param base64 - The base64 or base64url encoded string + * @returns The decoded bytes + */ +export function base64ToUint8Array(base64: string): Uint8Array { + const cleaned = base64 + .replace(/-/g, '+') + .replace(/_/g, '/') + .replace(/[^A-Za-z0-9+/]/g, ''); + const padded = cleaned + '='.repeat((4 - (cleaned.length % 4)) % 4); + const binary = atob(padded); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; +} diff --git a/runtimes/typescript/utils/coordinate_utils.ts b/runtimes/typescript/utils/coordinate_utils.ts new file mode 100644 index 0000000..66e66f0 --- /dev/null +++ b/runtimes/typescript/utils/coordinate_utils.ts @@ -0,0 +1,96 @@ +export class CoordinateUtils { + /** + * Decode a string of coordinates into an object with latitude and longitude in millidegrees + * @param stringCoords - The string of coordinates to decode + * + * @returns An object with latitude and longitude properties + */ + public static decodeStringCoordinates( + stringCoords: string, + ): { latitude: number; longitude: number } | undefined { + // format: N12345W123456 or N12345 W123456 + const firstChar = stringCoords.substring(0, 1); + let middleChar = stringCoords.substring(6, 7); + let longitudeChars = stringCoords.substring(7, 13); + if (middleChar == ' ') { + middleChar = stringCoords.substring(7, 8); + longitudeChars = stringCoords.substring(8, 14); + } + if ( + (firstChar === 'N' || firstChar === 'S') && + (middleChar === 'W' || middleChar === 'E') + ) { + return { + latitude: + (Number(stringCoords.substring(1, 6)) / 1000) * + CoordinateUtils.getDirection(firstChar), + longitude: + (Number(longitudeChars) / 1000) * + CoordinateUtils.getDirection(middleChar), + }; + } + + return undefined; + } + + /** + * Decode a string of coordinates into an object with latitude and longitude in degrees and decimal minutes + * @param stringCoords - The string of coordinates to decode + * + * @returns An object with latitude and longitude properties + */ + public static decodeStringCoordinatesDecimalMinutes( + stringCoords: string, + ): { latitude: number; longitude: number } | undefined { + // format: N12345W123456 or N12345 W123456 + const firstChar = stringCoords.substring(0, 1); + let middleChar = stringCoords.substring(6, 7); + let longitudeChars = stringCoords.substring(7, 13); + if (middleChar == ' ') { + middleChar = stringCoords.substring(7, 8); + longitudeChars = stringCoords.substring(8, 14); + } + const latDeg = Math.trunc(Number(stringCoords.substring(1, 6)) / 1000); + const latMin = (Number(stringCoords.substring(1, 6)) % 1000) / 10; + const lonDeg = Math.trunc(Number(longitudeChars) / 1000); + const lonMin = (Number(longitudeChars) % 1000) / 10; + + if ( + (firstChar === 'N' || firstChar === 'S') && + (middleChar === 'W' || middleChar === 'E') + ) { + return { + latitude: + (latDeg + latMin / 60) * CoordinateUtils.getDirection(firstChar), + longitude: + (lonDeg + lonMin / 60) * CoordinateUtils.getDirection(middleChar), + }; + } + return undefined; + } + public static coordinateString(coords: { + latitude: number; + longitude: number; + }): string { + const latDir = coords.latitude > 0 ? 'N' : 'S'; + const lonDir = coords.longitude > 0 ? 'E' : 'W'; + return `${Math.abs(coords.latitude).toFixed(3)} ${latDir}, ${Math.abs(coords.longitude).toFixed(3)} ${lonDir}`; + } + + public static getDirection(coord: string): number { + if (coord.startsWith('N') || coord.startsWith('E')) { + return 1; + } else if (coord.startsWith('S') || coord.startsWith('W')) { + return -1; + } + return NaN; + } + + public static dmsToDecimalDegrees( + degrees: number, + minutes: number, + seconds: number, + ): number { + return degrees + minutes / 60 + seconds / 3600; + } +} diff --git a/runtimes/typescript/utils/flight_plan_utils.ts b/runtimes/typescript/utils/flight_plan_utils.ts new file mode 100644 index 0000000..cd87c4b --- /dev/null +++ b/runtimes/typescript/utils/flight_plan_utils.ts @@ -0,0 +1,197 @@ +import { DecodeResult } from '../DecoderPluginInterface'; +import { ResultFormatter } from './result_formatter'; +import { RouteUtils } from './route_utils'; + +export class FlightPlanUtils { + /** + * Processes flight plan data + * + * Expected format is [header, fpei, val1, ... fpeiN, valN] + * + * @see https://atlaske-content.garmin.com/filestorage//email/outbound/attachments/GTN_Flight_Plan_and_User_Waypoint_transfer_Time1712844670119.pdf + * @param decodeResult - results + * @param data - original message split by ':' + * @returns whether all fields were processed or not + */ + public static processFlightPlan( + decodeResult: DecodeResult, + data: string[], + ): boolean { + let allKnownFields = FlightPlanUtils.parseHeader(decodeResult, data[0]); + for (let i = 1; i < data.length; i += 2) { + const fpei = data[i]; + const value = data[i + 1]; + // TODO: discuss how store commented out bits as both raw and formatted + switch (fpei) { + case 'A': // Arrival Procedure (?) + FlightPlanUtils.addProcedure(decodeResult, value, 'arrival'); + break; + case 'AA': + addArrivalAirport(decodeResult, value); + break; + case 'AP': + FlightPlanUtils.addProcedure(decodeResult, value, 'approach'); + break; + case 'CR': + addCompanyRoute(decodeResult, value); + break; + case 'D': // Departure Procedure + FlightPlanUtils.addProcedure(decodeResult, value, 'departure'); + break; + case 'DA': + addDepartureAirport(decodeResult, value); + break; + case 'F': // First Waypoint + addRoute(decodeResult, value); + break; + case 'FP': + ResultFormatter.flightPlan(decodeResult, value.trim()); + break; + case 'R': + addRunway(decodeResult, value); + break; + // case 'WS': // something about routes, has altitude, so current parsing won't work + // break; + default: + if (allKnownFields) { + decodeResult.remaining.text = ''; + allKnownFields = false; + } + decodeResult.remaining.text += `:${fpei}:${value}`; + decodeResult.decoder.decodeLevel = 'partial'; + } + } + return allKnownFields; + } + public static parseHeader( + decodeResult: DecodeResult, + header: string, + ): boolean { + let allKnownFields = true; + if (header.startsWith('RF')) { + decodeResult.formatted.items.push({ + type: 'status', + code: 'ROUTE_STATUS', + label: 'Route Status', + value: 'Route Filed', + }); + decodeResult.raw.route_status = 'RF'; + if (header.length > 2) { + addRoute(decodeResult, header.substring(2)); + } + } else if (header.startsWith('RP')) { + decodeResult.raw.route_status = 'RP'; + decodeResult.formatted.items.push({ + type: 'status', + code: 'ROUTE_STATUS', + label: 'Route Status', + value: 'Route Planned', + }); + decodeResult.raw.route_status = header; + } else if (header.startsWith('RI')) { + decodeResult.raw.route_status = 'RI'; + decodeResult.formatted.items.push({ + type: 'status', + code: 'ROUTE_STATUS', + label: 'Route Status', + value: 'Route Inactive', + }); + } else if (header.startsWith('RM')) { + decodeResult.raw.route_status = 'RM'; + decodeResult.formatted.items.push({ + type: 'status', + code: 'ROUTE_STATUS', + label: 'Route Status', + value: 'Route Mapped', + }); + } else if (header.startsWith('RS')) { + decodeResult.raw.route_status = 'RS'; + decodeResult.formatted.items.push({ + type: 'status', + code: 'ROUTE_STATUS', + label: 'Route Status', + value: 'Route Saved', + }); + } else { + decodeResult.remaining.text += header; + allKnownFields = false; + } + return allKnownFields; + } + + public static addProcedure( + decodeResult: DecodeResult, + value: string, + type: string, + ) { + if (decodeResult.raw.procedures === undefined) { + decodeResult.raw.procedures = []; + } + const data = value.split('.'); + let waypoints; + if (data.length > 1) { + waypoints = data.slice(1).map((leg) => RouteUtils.getWaypoint(leg)); + } + const route = { name: data[0], waypoints: waypoints }; + (decodeResult.raw.procedures as Array<{ type: string; route: object }>).push({ type: type, route: route }); + const procedureName = type.substring(0, 1).toUpperCase() + type.slice(1); + decodeResult.formatted.items.push({ + type: 'procedure', + code: 'proc', + label: `${procedureName} Procedure`, + value: RouteUtils.routeToString(route), + }); + } +} + +function addArrivalAirport(decodeResult: DecodeResult, value: string) { + ResultFormatter.arrivalAirport(decodeResult, value); +} + +function addDepartureAirport(decodeResult: DecodeResult, value: string) { + ResultFormatter.departureAirport(decodeResult, value); +} + +function addRunway(decodeResult: DecodeResult, value: string) { + // xxx(yyy) where xxx is the departure runway and yyy is the arrival runway + if (value.length === 8) { + ResultFormatter.arrivalRunway(decodeResult, value.substring(4, 7)); + } + + ResultFormatter.departureRunway(decodeResult, value.substring(0, 3)); +} + +function addRoute(decodeResult: DecodeResult, value: string) { + const route = value.split('.'); + ResultFormatter.route(decodeResult, { + waypoints: route.map((leg) => RouteUtils.getWaypoint(leg)), + }); +} + +function addCompanyRoute(decodeResult: DecodeResult, value: string) { + const segments = value.split('.'); + const parens_idx = segments[0].indexOf('('); + let name; + let runway; + if (parens_idx === -1) { + name = segments[0]; + } else { + name = segments[0].slice(0, parens_idx); + runway = segments[0].slice(parens_idx + 1, segments[0].indexOf(')')); + } + let waypoints; + if (segments.length > 1) { + waypoints = segments.slice(1).map((leg) => RouteUtils.getWaypoint(leg)); + } + decodeResult.raw.company_route = { + name: name, + runway: runway, + waypoints: waypoints, + }; + decodeResult.formatted.items.push({ + type: 'company_route', + code: 'CR', + label: 'Company Route', + value: RouteUtils.routeToString(decodeResult.raw.company_route as import("../types/route").Route), + }); +} diff --git a/runtimes/typescript/utils/icao_fpl_utils.ts b/runtimes/typescript/utils/icao_fpl_utils.ts new file mode 100644 index 0000000..41cc67a --- /dev/null +++ b/runtimes/typescript/utils/icao_fpl_utils.ts @@ -0,0 +1,195 @@ +/** + * ICAO Doc 4444 Flight Plan (FPL) parser utility. + * + * Parses the standard ICAO FPL format: + * (FPL-callsign-flightRulesType + * -aircraftType/wake-equipment/surveillance + * -departureHHMM + * -speedLevel route + * -destinationHHMM alternate1 alternate2 + * -otherInfo) + */ + +import { Route } from '../types/route'; + +export interface IcaoFlightPlan { + callsign: string; + flightRules: string; + flightType: string; + aircraftType: string; + wakeTurbulence: string; + equipment: string; + surveillance: string; + departure: string; + departureTime: string; + cruiseSpeed: string; + cruiseLevel: string; + route: Route; + destination: string; + eet: string; + alternates: string[]; + otherInfo: Record; +} + +export function parseIcaoFpl(text: string): IcaoFlightPlan | null { + // Find the (FPL-...) block + const fplStart = text.indexOf('(FPL-'); + if (fplStart === -1) { + return null; + } + + // Find matching closing paren + const fplEnd = text.indexOf(')', fplStart); + if (fplEnd === -1) { + return null; + } + + // Extract inner content after "(FPL-" + const inner = text.substring(fplStart + 5, fplEnd); + + // Normalize: collapse all whitespace (newlines, multiple spaces) to single space + const normalized = inner.replace(/\s+/g, ' ').trim(); + + // Split on " -" or leading "-" to get the field sections. + // The format is: callsign-rulesType-acft/wake-equip/surv-deptHHMM-speedLevel route-destHHMM alts-other + // After removing "(FPL-", the first field is callsign-rulesType. + // Subsequent fields are separated by "-" that appears after a space or at start of a section line. + // However, the "-" delimiter is tricky: it separates major sections but also appears + // within equipment strings (e.g. SDE3FGHIJ1...) — those are NOT delimiters. + // + // The ICAO FPL has exactly 6 major sections after the callsign section, delimited by "-". + // We need to split carefully. The format after "(FPL-" is: + // section1-section2-section3-section4-section5-section6 + // where section1 = "callsign-rulesType" (contains one internal "-") + // + // Strategy: split on "-" and reconstruct based on known patterns. + + const parts = normalized.split('-'); + + // We need at least 7 parts (callsign, rulesType, acftType/wake, equip/surv, dept, speed+route, dest+alts, other...) + // But the equipment section may contain hyphens too, so we parse positionally. + + if (parts.length < 7) { + return null; + } + + // Section 1: callsign + const callsign = parts[0].trim(); + if (!callsign) { + return null; + } + + // Section 2: flight rules + flight type (e.g., "IS") + const rulesType = parts[1].trim(); + if (rulesType.length < 2) { + return null; + } + const flightRules = rulesType[0]; + const flightType = rulesType[1]; + + // Now we need to find the remaining sections. The challenge is that equipment strings + // can be long but don't contain "-". The aircraft/wake section is "TYPE/WAKE". + // The equipment/surveillance section is "EQUIP/SURV". + // These are separated by "-". + + // Section 3: aircraft type / wake turbulence category (e.g., "B77W/H") + const acftWake = parts[2].trim(); + const acftSlash = acftWake.indexOf('/'); + if (acftSlash === -1) { + return null; + } + const aircraftType = acftWake.substring(0, acftSlash); + const wakeTurbulence = acftWake.substring(acftSlash + 1); + + // Section 4: equipment / surveillance (e.g., "SDE3FGHIJ1J2J3J4J5M1P2RWXYZ/LB1D1") + const equipSurv = parts[3].trim(); + const equipSlash = equipSurv.indexOf('/'); + if (equipSlash === -1) { + return null; + } + const equipment = equipSurv.substring(0, equipSlash); + const surveillance = equipSurv.substring(equipSlash + 1); + + // Section 5: departure aerodrome + time (e.g., "VESPA2354" or "KSFO0100") + const deptField = parts[4].trim(); + // Time is always last 4 digits + const departureTime = deptField.substring(deptField.length - 4); + const departure = deptField.substring(0, deptField.length - 4); + + // Section 6: cruise speed/level + route (e.g., "N0482F350 DCT ENI DCT OAK DCT BURGL IRNMN2") + const speedRouteField = parts[5].trim(); + // Speed is first token like N0482 or M084, level follows like F350 or S1190 + const speedMatch = speedRouteField.match( + /^([NKM]\d{3,4})([FAVMS]\d{3,4})\s*(.*)/, + ); + if (!speedMatch) { + return null; + } + const cruiseSpeed = speedMatch[1]; + const cruiseLevel = speedMatch[2]; + const route = { + waypoints: speedMatch[3].split(' ').map((s) => ({ + name: s, + })), + }; + + // Section 7: destination + EET + alternates (e.g., "KLAX0117 KSFO") + const destField = parts[6].trim(); + const destParts = destField.split(/\s+/); + const destTime = destParts[0]; + // Destination ICAO is first 4 chars, EET is last 4 digits + const destination = destTime.substring(0, destTime.length - 4); + const eet = destTime.substring(destTime.length - 4); + const alternates = destParts.slice(1).filter((s) => s.length > 0); + + // Section 8+: other information (remaining parts joined, then parsed as key/value) + const otherRaw = parts.slice(7).join('-').trim(); + const otherInfo: Record = {}; + + if (otherRaw.length > 0) { + // Other info is a series of KEY/VALUE pairs separated by spaces + // e.g., "PBN/A1B1C1D1L1O2S2T1 NAV/ABAS RNP2 DAT/1FANSE SUR/RSP180 DOF/260310 REG/B2036" + // Parse by finding KEY/ patterns + const otherTokens = otherRaw.split(/\s+/); + let currentKey = ''; + let currentValue = ''; + + for (const token of otherTokens) { + const kvMatch = token.match(/^([A-Z]{2,})\/(.*)/); + if (kvMatch) { + // Save previous key/value if exists + if (currentKey) { + otherInfo[currentKey] = currentValue.trim(); + } + currentKey = kvMatch[1]; + currentValue = kvMatch[2]; + } else if (currentKey) { + // Continuation of previous value + currentValue += ' ' + token; + } + } + // Save last key/value + if (currentKey) { + otherInfo[currentKey] = currentValue.trim(); + } + } + + return { + callsign, + flightRules, + flightType, + aircraftType, + wakeTurbulence, + equipment, + surveillance, + departure, + departureTime, + cruiseSpeed, + cruiseLevel, + route, + destination, + eet, + alternates, + otherInfo, + }; +} diff --git a/runtimes/typescript/utils/miam.ts b/runtimes/typescript/utils/miam.ts new file mode 100644 index 0000000..e7c4264 --- /dev/null +++ b/runtimes/typescript/utils/miam.ts @@ -0,0 +1,608 @@ +import { ascii85Decode } from './ascii85'; +import { inflateData } from './compression'; + +const textDecoder = new TextDecoder(); +const textEncoder = new TextEncoder(); + +enum MIAMVersion { + V1 = 1, + V2 = 2, +} + +enum MIAMFid { + SingleTransfer = 'T', + FileTransferRequest = 'F', + FileTransferAccept = 'K', + FileSegment = 'S', + FileTransferAbort = 'A', + XOFFIndication = 'Y', + XONIndication = 'X', +} + +enum MIAMCorePdu { + Data = 0, + Ack = 1, + Aloha = 2, + AlohaReply = 3, +} + +enum MIAMCoreApp { + ACARS2Char = 0x0, + ACARS4Char = 0x1, + ACARS6Char = 0x2, + NonACARS6Char = 0x3, +} + +export enum MIAMCoreV1Compression { + None = 0x0, + Deflate = 0x1, +} + +export enum MIAMCoreV2Compression { + None = 0x0, + Deflate = 0x1, +} + +const MIAMCoreV1CRCLength = 4; +const MIAMCoreV2CRCLength = 2; + +interface PduACARSData { + tail?: string; + label: string; + sublabel?: string; + mfi?: string; + text?: string; +} + +interface PduNonACARSData { + appId?: string; + text?: string; +} + +interface Pdu { + version: MIAMVersion; + crc: number; + crcOk: boolean; + complete: boolean; + compression: number; + encoding: number; + msgNum: number; + ackOptions: number; + acars?: PduACARSData; + non_acars?: PduNonACARSData; +} +const isMIAMVersion = (x: number): x is MIAMVersion => + Object.values(MIAMVersion).includes(x as MIAMVersion); +const isMIAMFid = (x: string): x is MIAMFid => + Object.values(MIAMFid).includes(x as MIAMFid); +const isMIAMCoreApp = (x: number): x is MIAMCoreApp => + Object.values(MIAMCoreApp).includes(x as MIAMCoreApp); +const isMIAMCorePdu = (x: number): x is MIAMCorePdu => + Object.values(MIAMCorePdu).includes(x as MIAMCorePdu); + +interface PduDecodingSuccess { + decoded: true; + message: { + data: Pdu; + }; +} + +interface PduDecodingFailure { + decoded: false; + error: string; +} + +type PduDecodingResult = PduDecodingSuccess | PduDecodingFailure; + +export class MIAMCoreUtils { + static AppTypeToAppIdLenTable: Record< + MIAMVersion, + Record + > = { + [MIAMVersion.V1]: { + [MIAMCoreApp.ACARS2Char]: 2, + [MIAMCoreApp.ACARS4Char]: 4, + [MIAMCoreApp.ACARS6Char]: 6, + [MIAMCoreApp.NonACARS6Char]: 6, + }, + [MIAMVersion.V2]: { + [MIAMCoreApp.ACARS2Char]: 2, + [MIAMCoreApp.ACARS4Char]: 4, + [MIAMCoreApp.ACARS6Char]: 6, + [MIAMCoreApp.NonACARS6Char]: 6, + }, + }; + + static FidHandlerTable: Record PduDecodingResult> = + { + [MIAMFid.SingleTransfer]: (txt: string) => { + if (txt.length < 3) { + return { + decoded: false, + error: 'Raw MIAM message too short (' + txt.length + ' < 3) ', + }; + } + + let bpad = txt[0]; + + if ('0123-.'.indexOf(bpad) === -1) { + return { + decoded: false, + error: "Invalid body padding value: '" + bpad + "'", + }; + } + + if ('0123'.indexOf(txt[1]) === -1) { + return { + decoded: false, + error: "Invalid header padding value: '" + txt[1] + "'", + }; + } + + const hpad = parseInt(txt[1]); + + const delimIdx = txt.indexOf('|'); + if (delimIdx === -1) { + return { + decoded: false, + error: 'Raw MIAM message missing header-body delimiter', + }; + } + + const rawHdr = txt.substring(2, delimIdx); + if (rawHdr.length === 0) { + return { + decoded: false, + error: 'Empty MIAM message header', + }; + } + + let hdr = ascii85Decode('<~' + rawHdr + '~>'); + if (!hdr || hdr.length < hpad) { + return { + decoded: false, + error: 'Ascii85 decode failed for MIAM message header', + }; + } + + let body: Uint8Array | undefined = undefined; + + const rawBody = txt.substring(delimIdx + 1); + if (rawBody.length > 0) { + if ('0123'.indexOf(bpad) >= 0) { + const bpadValue = parseInt(bpad); + + const decoded = ascii85Decode('<~' + rawBody + '~>'); + if (decoded === null) { + return { + decoded: false, + error: 'Ascii85 decode failed for MIAM message body', + }; + } + body = + decoded.length >= bpadValue + ? decoded.subarray(0, decoded.length - bpadValue) + : decoded; + } else if (bpad === '-') { + body = textEncoder.encode(rawBody); + } + } + + hdr = hdr.subarray(0, hdr.length - hpad); + + const version = hdr[0] & 0xf; + const pduType = (hdr[0] >> 4) & 0xf; + + if (isMIAMVersion(version) && isMIAMCorePdu(pduType)) { + const versionPduHandler = + this.VersionPduHandlerTable[version][pduType]; + return versionPduHandler(hdr, body); + } else { + return { + decoded: false, + error: + 'Invalid version and PDU type combination: v=' + + version + + ', pdu=' + + pduType, + }; + } + }, + [MIAMFid.FileTransferRequest]: () => { + return { + decoded: false, + error: 'File Transfer Request not implemented', + }; + }, + [MIAMFid.FileTransferAccept]: () => { + return { + decoded: false, + error: 'File Transfer Accept not implemented', + }; + }, + [MIAMFid.FileSegment]: () => { + return { decoded: false, error: 'File Segment not implemented' }; + }, + [MIAMFid.FileTransferAbort]: () => { + return { decoded: false, error: 'File Transfer Abort not implemented' }; + }, + [MIAMFid.XOFFIndication]: () => { + return { decoded: false, error: 'XOFF Indication not implemented' }; + }, + [MIAMFid.XONIndication]: () => { + return { decoded: false, error: 'XON Indication not implemented' }; + }, + }; + + private static arincCrc16(buf: Uint8Array, seed?: number) { + const crctable = [ + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, + 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210, + 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x9339, 0x8318, 0xb37b, + 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401, + 0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, + 0xf5cf, 0xc5ac, 0xd58d, 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, + 0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, + 0xc7bc, 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0x5af5, + 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc, + 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 0x6ca6, 0x7c87, 0x4ce4, + 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd, + 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, + 0x2e32, 0x1e51, 0x0e70, 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, + 0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, + 0xe16f, 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 0x02b1, + 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb, + 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 0x34e2, 0x24c3, 0x14a0, + 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8, + 0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, + 0x7676, 0x4615, 0x5634, 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, + 0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, + 0x28a3, 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, + 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 0xfd2e, + 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07, + 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 0xef1f, 0xff3e, 0xcf5d, + 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74, + 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, + ]; + + let crc = (seed || 0) & 0xffff; + + for (let i = 0; i < buf.length; i++) { + crc = + (((crc << 8) >>> 0) ^ + crctable[(((crc >>> 8) ^ buf[i]) >>> 0) & 0xff]) >>> + 0; + } + + return crc & 0xffff; + } + + private static arinc665Crc32(buf: Uint8Array, seed?: number) { + const crctable = [ + 0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, 0x130476dc, 0x17c56b6b, + 0x1a864db2, 0x1e475005, 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61, + 0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd, 0x4c11db70, 0x48d0c6c7, + 0x4593e01e, 0x4152fda9, 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75, + 0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, 0x791d4014, 0x7ddc5da3, + 0x709f7b7a, 0x745e66cd, 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039, + 0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, 0xbe2b5b58, 0xbaea46ef, + 0xb7a96036, 0xb3687d81, 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d, + 0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, 0xc7361b4c, 0xc3f706fb, + 0xceb42022, 0xca753d95, 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1, + 0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, 0x34867077, 0x30476dc0, + 0x3d044b19, 0x39c556ae, 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072, + 0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, 0x018aeb13, 0x054bf6a4, + 0x0808d07d, 0x0cc9cdca, 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde, + 0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02, 0x5e9f46bf, 0x5a5e5b08, + 0x571d7dd1, 0x53dc6066, 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba, + 0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, 0xbfa1b04b, 0xbb60adfc, + 0xb6238b25, 0xb2e29692, 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6, + 0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a, 0xe0b41de7, 0xe4750050, + 0xe9362689, 0xedf73b3e, 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2, + 0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, 0xd5b88683, 0xd1799b34, + 0xdc3abded, 0xd8fba05a, 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637, + 0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb, 0x4f040d56, 0x4bc510e1, + 0x46863638, 0x42472b8f, 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53, + 0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, 0x36194d42, 0x32d850f5, + 0x3f9b762c, 0x3b5a6b9b, 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff, + 0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623, 0xf12f560e, 0xf5ee4bb9, + 0xf8ad6d60, 0xfc6c70d7, 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b, + 0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, 0xc423cd6a, 0xc0e2d0dd, + 0xcda1f604, 0xc960ebb3, 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7, + 0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b, 0x9b3660c6, 0x9ff77d71, + 0x92b45ba8, 0x9675461f, 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3, + 0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, 0x4e8ee645, 0x4a4ffbf2, + 0x470cdd2b, 0x43cdc09c, 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8, + 0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24, 0x119b4be9, 0x155a565e, + 0x18197087, 0x1cd86d30, 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec, + 0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, 0x2497d08d, 0x2056cd3a, + 0x2d15ebe3, 0x29d4f654, 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0, + 0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c, 0xe3a1cbc1, 0xe760d676, + 0xea23f0af, 0xeee2ed18, 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4, + 0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, 0x9abc8bd5, 0x9e7d9662, + 0x933eb0bb, 0x97ffad0c, 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668, + 0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4, + ]; + + let crc = seed || 0; + + for (let i = 0; i < buf.length; i++) { + crc = + (((crc << 8) >>> 0) ^ crctable[((crc >>> 24) ^ buf[i]) >>> 0]) >>> 0; + } + + return crc; + } + + public static parse(txt: string): PduDecodingResult { + const fidType = txt[0]; + + if (isMIAMFid(fidType)) { + const handler = this.FidHandlerTable[fidType]; + return handler(txt.substring(1)); + } else { + return { + decoded: false, + error: 'Unsupported FID type: ' + fidType, + }; + } + } + + private static corePduDataHandler( + version: MIAMVersion, + minHdrSize: number, + crcLen: number, + hdr: Uint8Array, + body?: Uint8Array, + ): PduDecodingResult { + if (hdr.length < minHdrSize) { + return { + decoded: false, + error: + 'v' + + version + + ' header size too short; expected >= ' + + minHdrSize + + ', got ' + + hdr.length, + }; + } + + let pduSize: number | undefined = undefined; + let pduCompression: number = 0; + let pduEncoding: number = 0; + let pduAppType: number = 0; + let pduAppId: string = ''; + let pduCrc: number = 0; + let pduData: Uint8Array | null = null; + let pduCrcIsOk: boolean = false; + let pduIsComplete: boolean = true; + + let pduErrors: string[] = []; + + let tail: string | undefined = undefined; + let msgNum: number = 0; + let ackOptions: number = 0; + + if (version === MIAMVersion.V1) { + pduSize = (hdr[1] << 16) | (hdr[2] << 8) | hdr[3]; + + const msgSize = hdr.length + (body === undefined ? 0 : body.length); + if (pduSize > msgSize) { + pduIsComplete = false; + pduErrors.push( + 'v1 PDU truncated: expecting ' + pduSize + ', got ' + msgSize, + ); + } + hdr = hdr.subarray(4); + + tail = textDecoder.decode(hdr.subarray(0, 7)); + hdr = hdr.subarray(7); + } else if (version === MIAMVersion.V2) { + hdr = hdr.subarray(1); + } + + msgNum = (hdr[0] >> 1) & 0x7f; + ackOptions = hdr[0] & 1; + hdr = hdr.subarray(1); + + pduCompression = ((hdr[0] << 2) | ((hdr[1] >> 6) & 0x3)) & 0x7; + pduEncoding = (hdr[1] >> 4) & 0x3; + pduAppType = hdr[1] & 0xf; + hdr = hdr.subarray(2); + + let appIdLen; + if (isMIAMCoreApp(pduAppType)) { + appIdLen = this.AppTypeToAppIdLenTable[version][pduAppType]; + } else { + if ( + version === MIAMVersion.V2 && + (pduAppType & 0x8) !== 0 && + pduAppType !== 0xd + ) { + appIdLen = (pduAppType & 0x7) + 1; + } else { + return { + decoded: false, + error: 'Invalid v' + version + ' appType: ' + pduAppType, + }; + } + } + + const pduIsACARS = + [ + MIAMCoreApp.ACARS2Char, + MIAMCoreApp.ACARS4Char, + MIAMCoreApp.ACARS6Char, + ].indexOf(pduAppType) >= 0; + + if (hdr.length < appIdLen + crcLen) { + return { + decoded: false, + error: 'Header too short for v' + version + ' appType: ' + pduAppType, + }; + } + + pduAppId = textDecoder.decode(hdr.subarray(0, appIdLen)); + hdr = hdr.subarray(appIdLen); + + if (crcLen === 4) { + pduCrc = (hdr[0] << 24) | (hdr[1] << 16) | (hdr[2] << 8) | hdr[3]; // crc + } else if (crcLen === 2) { + pduCrc = (hdr[0] << 8) | hdr[1]; // crc + } + hdr = hdr.subarray(crcLen); + + if (body !== undefined && body.length > 0) { + if ( + [MIAMCoreV1Compression.Deflate, MIAMCoreV2Compression.Deflate].indexOf( + pduCompression, + ) >= 0 + ) { + try { + const inflated = inflateData(body, true); + if (inflated === undefined) { + pduErrors.push('Inflation produced no output for body'); + } else { + pduData = inflated; + } + } catch (e) { + pduErrors.push('Inflation failed for body: ' + e); + } + } else if ( + [MIAMCoreV1Compression.None, MIAMCoreV2Compression.None].indexOf( + pduCompression, + ) >= 0 + ) { + pduData = body; + } else { + pduErrors.push( + 'Unsupported v' + version + ' compression type: ' + pduCompression, + ); + } + + if (pduData !== null) { + const crcAlgoHandlerByVersion: Record< + MIAMVersion, + (buf: Uint8Array, seed?: number) => number + > = { + [MIAMVersion.V1]: (buf: Uint8Array, seed?: number) => { + return ~this.arinc665Crc32(buf, seed); + }, + [MIAMVersion.V2]: this.arincCrc16, + }; + + const crcAlgoHandler = crcAlgoHandlerByVersion[version]; + if (crcAlgoHandler === undefined) { + return { + decoded: false, + error: 'No CRC handler for v' + version, + }; + } + + const crcCheck = crcAlgoHandler(pduData, 0xffffffff); + if (crcCheck === pduCrc) { + pduCrcIsOk = true; + } else { + pduErrors.push( + 'Body failed CRC check: provided=' + + pduCrc + + ', generated=' + + crcCheck, + ); + } + } + } else { + // No PDU body means we can't verify CRC checksum; however, the message should still be valid... + pduCrcIsOk = true; + } + + let pdu: Pdu = { + version, + crc: pduCrc, + crcOk: pduCrcIsOk, + complete: pduIsComplete, + compression: pduCompression, + encoding: pduEncoding, + msgNum, + ackOptions, + }; + + if (pduIsACARS) { + const label = pduAppId.substring(0, 2); + const sublabel = appIdLen >= 4 ? pduAppId.substring(2, 4) : undefined; + const mfi = appIdLen >= 6 ? pduAppId.substring(4, 6) : undefined; + + pdu.acars = { + ...(tail ? { tail } : {}), + label, + ...(sublabel ? { sublabel } : {}), + ...(mfi ? { mfi } : {}), + ...(pduData ? { text: textDecoder.decode(pduData) } : {}), + }; + } else { + pdu.non_acars = { + appId: pduAppId, + ...(pduData ? { text: textDecoder.decode(pduData) } : {}), + }; + } + + return { + decoded: true, + message: { + data: pdu, + }, + }; + } + + static VersionPduHandlerTable: Record< + MIAMVersion, + Record< + MIAMCorePdu, + (hdr: Uint8Array, body?: Uint8Array) => PduDecodingResult + > + > = { + [MIAMVersion.V1]: { + [MIAMCorePdu.Data]: (hdr: Uint8Array, body?: Uint8Array) => { + return this.corePduDataHandler( + MIAMVersion.V1, + 20, + MIAMCoreV1CRCLength, + hdr, + body, + ); + }, + [MIAMCorePdu.Ack]: () => { + return { decoded: false, error: 'v1 Ack PDU not implemented' }; + }, + [MIAMCorePdu.Aloha]: () => { + return { decoded: false, error: 'v1 Aloha PDU not implemented' }; + }, + [MIAMCorePdu.AlohaReply]: () => { + return { decoded: false, error: 'v1 AlohaReply PDU not implemented' }; + }, + }, + [MIAMVersion.V2]: { + [MIAMCorePdu.Data]: (hdr: Uint8Array, body?: Uint8Array) => { + return this.corePduDataHandler( + MIAMVersion.V2, + 7, + MIAMCoreV2CRCLength, + hdr, + body, + ); + }, + [MIAMCorePdu.Ack]: () => { + return { decoded: false, error: 'v2 Ack PDU not implemented' }; + }, + [MIAMCorePdu.Aloha]: () => { + return { decoded: false, error: 'v2 Aloha PDU not implemented' }; + }, + [MIAMCorePdu.AlohaReply]: () => { + return { decoded: false, error: 'v2 Aloha reply PDU not implemented' }; + }, + }, + }; +} diff --git a/runtimes/typescript/utils/result_formatter.ts b/runtimes/typescript/utils/result_formatter.ts new file mode 100644 index 0000000..bba9448 --- /dev/null +++ b/runtimes/typescript/utils/result_formatter.ts @@ -0,0 +1,694 @@ +import { DecodeResult } from '../DecoderPluginInterface'; +import { CoordinateUtils } from './coordinate_utils'; +import { DateTimeUtils } from '../DateTimeUtils'; +import { RouteUtils } from './route_utils'; +import { Route } from '../types/route'; +import { Wind } from '../types/wind'; + +/** + * Class to format the results of common fields + */ +export class ResultFormatter { + static route(decodeResult: DecodeResult, route: Route) { + decodeResult.raw.route = route; + decodeResult.formatted.items.push({ + type: 'aircraft_route', + code: 'ROUTE', + label: 'Aircraft Route', + value: RouteUtils.routeToString(route), + }); + } + + static state_change(decodeResult: DecodeResult, from: string, to: string) { + decodeResult.raw.state_change = { + from: from, + to: to, + }; + from = RouteUtils.formatFlightState(from); + to = RouteUtils.formatFlightState(to); + decodeResult.formatted.items.push({ + type: 'state_change', + code: 'STATE_CHANGE', + label: 'State Change', + value: `${from} -> ${to}`, + }); + } + + static door_event(decodeResult: DecodeResult, name: string, state: string) { + decodeResult.raw.door_event = { + door: name, + state: state, + }; + + decodeResult.formatted.items.push({ + type: 'door_event', + code: 'DOOR', + label: 'Door Event', + value: `${name} ${state}`, + }); + } + + static position( + decodeResult: DecodeResult, + value: { latitude: number; longitude: number } | undefined, + ) { + if (!value || isNaN(value.latitude) || isNaN(value.longitude)) { + return; + } + decodeResult.raw.position = value; + decodeResult.formatted.items.push({ + type: 'aircraft_position', + code: 'POS', + label: 'Aircraft Position', + value: CoordinateUtils.coordinateString(value), + }); + } + + static altitude(decodeResult: DecodeResult, value: number) { + if (isNaN(value)) { + return; + } + decodeResult.raw.altitude = value; + decodeResult.formatted.items.push({ + type: 'altitude', + code: 'ALT', + label: 'Altitude', + value: `${decodeResult.raw.altitude} feet`, + }); + } + + static flightNumber(decodeResult: DecodeResult, value: string) { + if (value.length === 0) { + return; + } + decodeResult.raw.flight_number = value; + decodeResult.formatted.items.push({ + type: 'flight_number', + code: 'FLIGHT', + label: 'Flight Number', + value: decodeResult.raw.flight_number, + }); + } + + static callsign(decodeResult: DecodeResult, value: string) { + decodeResult.raw.callsign = value; + decodeResult.formatted.items.push({ + type: 'callsign', + code: 'CALLSIGN', + label: 'Callsign', + value: decodeResult.raw.callsign, + }); + } + + static departureAirport( + decodeResult: DecodeResult, + value: string, + type: 'IATA' | 'ICAO' = 'ICAO', + ) { + if (type === 'ICAO') { + decodeResult.raw.departure_icao = value; + decodeResult.formatted.items.push({ + type: 'icao', + code: 'ORG', + label: 'Origin', + value: value, + }); + } else { + decodeResult.raw.departure_iata = value; + decodeResult.formatted.items.push({ + type: 'iata', + code: 'ORG', + label: 'Origin', + value: value, + }); + } + } + + static departureRunway(decodeResult: DecodeResult, value: string) { + decodeResult.raw.departure_runway = value; + decodeResult.formatted.items.push({ + type: 'runway', + code: 'DEPRWY', + label: 'Departure Runway', + value: decodeResult.raw.departure_runway, + }); + } + + static arrivalAirport( + decodeResult: DecodeResult, + value: string, + type: 'IATA' | 'ICAO' = 'ICAO', + ) { + if (type === 'ICAO') { + decodeResult.raw.arrival_icao = value; + decodeResult.formatted.items.push({ + type: 'icao', + code: 'DST', + label: 'Destination', + value: value, + }); + } else { + decodeResult.raw.arrival_iata = value; + decodeResult.formatted.items.push({ + type: 'iata', + code: 'DST', + label: 'Destination', + value: value, + }); + } + } + + static alternateAirport(decodeResult: DecodeResult, value: string) { + decodeResult.raw.alternate_icao = value; + decodeResult.formatted.items.push({ + type: 'icao', + code: 'ALT_DST', + label: 'Alternate Destination', + value: decodeResult.raw.alternate_icao, + }); + } + + static eta(decodeResult: DecodeResult, time: number) { + decodeResult.raw.eta_time = time; + decodeResult.formatted.items.push({ + type: 'time', + code: 'ETA', + label: 'Estimated Time of Arrival', + value: DateTimeUtils.timestampToString(time), + }); + } + + static arrivalRunway(decodeResult: DecodeResult, value: string) { + decodeResult.raw.arrival_runway = value; + decodeResult.formatted.items.push({ + type: 'runway', + code: 'ARWY', + label: 'Arrival Runway', + value: decodeResult.raw.arrival_runway, + }); + } + + static alternateRunway(decodeResult: DecodeResult, value: string) { + decodeResult.raw.alternate_runway = value; + decodeResult.formatted.items.push({ + type: 'runway', + code: 'ALT_ARWY', + label: 'Alternate Runway', + value: decodeResult.raw.alternate_runway, + }); + } + + static currentFuel(decodeResult: DecodeResult, value: number) { + decodeResult.raw.fuel_on_board = value; + decodeResult.formatted.items.push({ + type: 'fuel_on_board', + code: 'FOB', + label: 'Fuel On Board', + value: decodeResult.raw.fuel_on_board.toString(), + }); + } + + static burnedFuel(decodeResult: DecodeResult, value: number) { + decodeResult.raw.fuel_burned = value; + decodeResult.formatted.items.push({ + type: 'fuel_burned', + code: 'FB', + label: 'Fuel Burned', + value: decodeResult.raw.fuel_burned.toString(), + }); + } + + static remainingFuel(decodeResult: DecodeResult, value: number) { + decodeResult.raw.fuel_remaining = value; + decodeResult.formatted.items.push({ + type: 'fuel_remaining', + code: ' FUEL_REM', + label: 'Fuel Remaining', + value: decodeResult.raw.fuel_remaining.toString(), + }); + } + + static outFuel(decodeResult: DecodeResult, value: number) { + decodeResult.raw.out_fuel = value; + decodeResult.formatted.items.push({ + type: 'lbs', + code: 'FUEL_OUT', + label: 'Out of Gate Fuel', + value: decodeResult.raw.out_fuel.toString() + ' lbs', + }); + } + + static offFuel(decodeResult: DecodeResult, value: number) { + decodeResult.raw.off_fuel = value; + decodeResult.formatted.items.push({ + type: 'lbs', + code: 'FUEL_OFF', + label: 'Takeoff Fuel', + value: decodeResult.raw.off_fuel.toString() + ' lbs', + }); + } + + static onFuel(decodeResult: DecodeResult, value: number) { + decodeResult.raw.on_fuel = value; + decodeResult.formatted.items.push({ + type: 'lbs', + code: 'FUEL_ON', + label: 'Landing Fuel', + value: decodeResult.raw.on_fuel.toString() + ' lbs', + }); + } + + static inFuel(decodeResult: DecodeResult, value: number) { + decodeResult.raw.in_fuel = value; + decodeResult.formatted.items.push({ + type: 'lbs', + code: 'FUEL_IN', + label: 'In Gate Fuel', + value: decodeResult.raw.in_fuel.toString() + ' lbs', + }); + } + + static startFuel(decodeResult: DecodeResult, value: number) { + decodeResult.raw.start_fuel = value; + decodeResult.formatted.items.push({ + type: 'lbs', + code: 'FUEL_START', + label: 'Start Fuel', + value: decodeResult.raw.start_fuel.toString() + ' lbs', + }); + } + + static checksumAlgorithm(decodeResult: DecodeResult, value: string) { + decodeResult.raw.checksum_algorithm = value; + // decodeResult.formatted.items.push({ + // type: 'message_checksum_algorithm', + // code: 'CHECKSUM_ALGO', + // label: 'Checksum Algorithm', + // value: decodeResult.raw.checksum_algorithm, + // }); + } + + static checksum(decodeResult: DecodeResult, value: number) { + decodeResult.raw.checksum = value; + decodeResult.formatted.items.push({ + type: 'message_checksum', + code: 'CHECKSUM', + label: 'Message Checksum', + value: '0x' + ('0000' + decodeResult.raw.checksum.toString(16)).slice(-4), + }); + } + + static groundspeed(decodeResult: DecodeResult, value: number) { + decodeResult.raw.groundspeed = value; + decodeResult.formatted.items.push({ + type: 'aircraft_groundspeed', + code: 'GSPD', + label: 'Aircraft Groundspeed', + value: `${decodeResult.raw.groundspeed} knots`, + }); + } + + static airspeed(decodeResult: DecodeResult, value: number) { + decodeResult.raw.airspeed = value; + decodeResult.formatted.items.push({ + type: 'airspeed', + code: 'ASPD', + label: 'True Airspeed', + value: `${decodeResult.raw.airspeed} knots`, + }); + } + + static mach(decodeResult: DecodeResult, value: number) { + decodeResult.raw.mach = value; + decodeResult.formatted.items.push({ + type: 'mach', + code: 'MACH', + label: 'Mach Number', + value: `${decodeResult.raw.mach} mach`, + }); + } + + static temperature(decodeResult: DecodeResult, value: string) { + if (value.length === 0) { + return; + } + decodeResult.raw.outside_air_temperature = Number( + value.replace('M', '-').replace('P', '+'), + ); + decodeResult.formatted.items.push({ + type: 'outside_air_temperature', + code: 'OATEMP', + label: 'Outside Air Temperature (C)', + value: `${decodeResult.raw.outside_air_temperature} degrees`, + }); + } + + static totalAirTemp(decodeResult: DecodeResult, value: string) { + if (value.length === 0) { + return; + } + decodeResult.raw.total_air_temperature = Number( + value.replace('M', '-').replace('P', '+'), + ); + decodeResult.formatted.items.push({ + type: 'temperature', + code: 'TATEMP', + label: 'Total Air Temperature (C)', + value: `${decodeResult.raw.total_air_temperature} degrees`, + }); + } + + static heading(decodeResult: DecodeResult, value: number) { + decodeResult.raw.heading = value; + decodeResult.formatted.items.push({ + type: 'heading', + code: 'HDG', + label: 'Heading', + value: `${decodeResult.raw.heading}`, + }); + } + + static tail(decodeResult: DecodeResult, value: string) { + decodeResult.raw.tail = value; + decodeResult.formatted.items.push({ + type: 'tail', + code: 'TAIL', + label: 'Tail', + value: decodeResult.raw.tail, + }); + } + + static out(decodeResult: DecodeResult, time: number) { + decodeResult.raw.out_time = time; + decodeResult.formatted.items.push({ + type: 'time', + code: 'OUT', + label: 'Out of Gate Time', + value: DateTimeUtils.timestampToString(time), + }); + } + + static off(decodeResult: DecodeResult, time: number) { + decodeResult.raw.off_time = time; + decodeResult.formatted.items.push({ + type: 'time', + code: 'OFF', + label: 'Takeoff Time', + value: DateTimeUtils.timestampToString(time), + }); + } + + static on(decodeResult: DecodeResult, time: number) { + decodeResult.raw.on_time = time; + decodeResult.formatted.items.push({ + type: 'time', + code: 'ON', + label: 'Landing Time', + value: DateTimeUtils.timestampToString(time), + }); + } + + static in(decodeResult: DecodeResult, time: number) { + decodeResult.raw.in_time = time; + decodeResult.formatted.items.push({ + type: 'time', + code: 'IN', + label: 'In Gate Time', + value: DateTimeUtils.timestampToString(time), + }); + } + + static engineStart(decodeResult: DecodeResult, time: number) { + decodeResult.raw.engine_start_time = time; + decodeResult.formatted.items.push({ + type: 'time', + code: 'ENG_START', + label: 'Engine Start Time', + value: DateTimeUtils.timestampToString(time), + }); + } + static engineStop(decodeResult: DecodeResult, time: number) { + decodeResult.raw.engine_stop_time = time; + decodeResult.formatted.items.push({ + type: 'time', + code: 'ENG_STOP', + label: 'Engine Stop Time', + value: DateTimeUtils.timestampToString(time), + }); + } + + static day(decodeResult: DecodeResult, day: number) { + decodeResult.raw.day = day; + decodeResult.formatted.items.push({ + type: 'day', + code: 'MSG_DAY', + label: 'Day of Month', + value: `${day}`, + }); + } + + static month(decodeResult: DecodeResult, month: number) { + decodeResult.raw.month = month; + decodeResult.formatted.items.push({ + type: 'month', + code: 'MSG_MON', + label: 'Month of Year', + value: `${month}`, + }); + } + + static departureDay(decodeResult: DecodeResult, day: number) { + decodeResult.raw.departure_day = day; + decodeResult.formatted.items.push({ + type: 'day', + code: 'DEP_DAY', + label: 'Departure Day', + value: `${day}`, + }); + } + + static arrivalDay(decodeResult: DecodeResult, day: number) { + decodeResult.raw.arrival_day = day; + decodeResult.formatted.items.push({ + type: 'day', + code: 'ARR_DAY', + label: 'Arrival Day', + value: `${day}`, + }); + } + + static text(decodeResult: DecodeResult, text: string) { + decodeResult.raw.text = text; + decodeResult.formatted.items.push({ + type: 'text', + code: 'TEXT', + label: 'Text Message', + value: text, + }); + } + + static mac(decodeResult: DecodeResult, mac: number) { + decodeResult.raw.mac = mac; + decodeResult.formatted.items.push({ + type: 'mac', + code: 'MAC', + label: 'Mean Aerodynamic Chord', + value: `${mac} %`, + }); + } + + static trim(decodeResult: DecodeResult, trim: number) { + decodeResult.raw.trim = trim; + decodeResult.formatted.items.push({ + type: 'trim', + code: 'TRIM', + label: 'Trim', + value: `${trim} units`, + }); + } + + static windData(decodeResult: DecodeResult, windData: Wind[]) { + decodeResult.raw.wind_data = windData; + for (const wind of windData) { + let text = `${RouteUtils.waypointToString(wind.waypoint)} at FL${wind.flightLevel}: ${wind.windDirection}° at ${wind.windSpeed}kt`; + if (wind.temperature) { + text += `, ${wind.temperature.degreesC}°C at FL${wind.temperature.flightLevel}`; + } + decodeResult.formatted.items.push({ + type: 'wind_data', + code: 'WIND', + label: 'Wind Data', + value: text, + }); + } + } + + static cg( + decodeResult: DecodeResult, + value: number, + type: 'center' | 'lower' | 'upper' = 'center', + ) { + switch (type) { + case 'center': + decodeResult.raw.center_of_gravity = value; + decodeResult.formatted.items.push({ + type: 'center_of_gravity', + code: 'CG', + label: 'Center of Gravity', + value: `${decodeResult.raw.center_of_gravity} %`, + }); + break; + case 'lower': + decodeResult.raw.cg_lower_limit = value; + decodeResult.formatted.items.push({ + type: 'cg_lower_limit', + code: 'CG_LOWER', + label: 'Center of Gravity Lower Limit', + value: `${decodeResult.raw.cg_lower_limit} %`, + }); + break; + case 'upper': + decodeResult.raw.cg_upper_limit = value; + decodeResult.formatted.items.push({ + type: 'cg_upper_limit', + code: 'CG_UPPER', + label: 'Center of Gravity Upper Limit', + value: `${decodeResult.raw.cg_upper_limit} %`, + }); + break; + } + } + + static version(decodeResult: DecodeResult, value: number) { + decodeResult.raw.version = value; + decodeResult.formatted.items.push({ + type: 'version', + code: 'VERSION', + label: 'Message Version', + value: `v${decodeResult.raw.version.toFixed(1)}`, + }); + } + + static label(decodeResult: DecodeResult, value: string) { + decodeResult.raw.label = value; + decodeResult.formatted.items.push({ + type: 'label', + code: 'LABEL', + label: 'Message Label', + value: `${decodeResult.raw.label}`, + }); + } + + static sublabel(decodeResult: DecodeResult, value: string) { + decodeResult.raw.sublabel = value; + decodeResult.formatted.items.push({ + type: 'sublabel', + code: 'SUBLABEL', + label: 'Message Sublabel', + value: `${decodeResult.raw.sublabel}`, + }); + } + + static requestedAltitudes(decodeResult: DecodeResult, values: number[]) { + decodeResult.raw.requested_alts = values; + decodeResult.formatted.items.push({ + type: 'requested_altitudes', + code: 'REQ_ALTS', + label: 'Requested Altitudes', + value: `${decodeResult.raw.requested_alts.join(', ')}`, + }); + } + + static desiredAltitude(decodeResult: DecodeResult, value: number) { + decodeResult.raw.desired_alt = value; + decodeResult.formatted.items.push({ + type: 'desired_altitude', + code: 'DES_ALT', + label: 'Desired Altitude', + value: `${decodeResult.raw.desired_alt}`, + }); + } + + static startPoint(decodeResult: DecodeResult, value: string) { + decodeResult.raw.start_point = value; + decodeResult.formatted.items.push({ + type: 'start_point', + code: 'START', + label: 'Start Point', + value: `${decodeResult.raw.start_point}`, + }); + } + + static routeNumber(decodeResult: DecodeResult, value: string) { + decodeResult.raw.route_number = value; + decodeResult.formatted.items.push({ + type: 'route_number', + code: 'RTE_NUM', + label: 'Route Number', + value: `${decodeResult.raw.route_number}`, + }); + } + + static flightPlan(decodeResult: DecodeResult, value: string) { + decodeResult.raw.flight_plan = value; + decodeResult.formatted.items.push({ + type: 'flight_plan', + code: 'FPN', + label: 'Flight Plan', + value: `${decodeResult.raw.flight_plan}`, + }); + } + + static groundAddress(decodeResult: DecodeResult, value: string) { + decodeResult.raw.ground_address = value; + decodeResult.formatted.items.push({ + type: 'ground_address', + code: 'GND_ADDR', + label: 'Ground Address', + value: `${decodeResult.raw.ground_address}`, + }); + } + + static timestamp(decodeResult: DecodeResult, value: number) { + decodeResult.raw.message_timestamp = value; + decodeResult.formatted.items.push({ + type: 'time', + code: 'TIMESTAMP', + label: 'Message Timestamp', + value: DateTimeUtils.timestampToString(value), + }); + } + + static sequenceNumber(decodeResult: DecodeResult, value: number) { + decodeResult.raw.sequence_number = value; + decodeResult.formatted.items.push({ + type: 'sequence', + code: 'SEQ', + label: 'Sequence Number', + value: String(decodeResult.raw.sequence_number), + }); + } + + static sequenceResponse(decodeResult: DecodeResult, value: number) { + decodeResult.raw.sequence_response = value; + decodeResult.formatted.items.push({ + type: 'sequence', + code: 'SEQ_RESP', + label: 'Sequence Response', + value: String(decodeResult.raw.sequence_response), + }); + } + + static unknown(decodeResult: DecodeResult, value: string, sep: string = ',') { + if (!decodeResult.remaining.text) decodeResult.remaining.text = value; + else decodeResult.remaining.text += sep + value; + } + + static unknownArr( + decodeResult: DecodeResult, + value: string[], + sep: string = ',', + ) { + this.unknown(decodeResult, value.join(sep), sep); + } +} diff --git a/runtimes/typescript/utils/route_utils.ts b/runtimes/typescript/utils/route_utils.ts new file mode 100644 index 0000000..766c5b6 --- /dev/null +++ b/runtimes/typescript/utils/route_utils.ts @@ -0,0 +1,112 @@ +import { DateTimeUtils } from '../DateTimeUtils'; +import { Route } from '../types/route'; +import { Waypoint } from '../types/waypoint'; +import { CoordinateUtils } from './coordinate_utils'; + +export class RouteUtils { + public static formatFlightState(state: string): string { + switch (state) { + case 'TO': + return 'Takeoff'; + case 'IC': + return 'Initial Climb'; + case 'CL': + return 'Climb'; + case 'ER': + return 'En Route'; + case 'DC': + return 'Descent'; + case 'AP': + return 'Approach'; + default: + return `Unknown ${state}`; + } + } + + public static routeToString(route: Route): string { + let str = ''; + if (route.name) { + str += route.name; + } + if (route.runway) { + str += `(${route.runway})`; + } + if (str.length !== 0 && route.waypoints && route.waypoints.length === 1) { + str += ' starting at '; + } else if (str.length !== 0 && route.waypoints) { + str += ': '; + } + + if (route.waypoints) { + str += RouteUtils.waypointsToString(route.waypoints); + } + + return str; + } + + public static waypointToString(waypoint: Waypoint): string { + let s = waypoint.name; + if (waypoint.latitude && waypoint.longitude) { + s += `(${CoordinateUtils.coordinateString({ latitude: waypoint.latitude, longitude: waypoint.longitude })})`; + } + if (waypoint.offset) { + s += `[${waypoint.offset.bearing}° ${waypoint.offset.distance}nm]`; + } + if (waypoint.time) { + s += `@${DateTimeUtils.timestampToString(waypoint.time)}`; + } + return s; + } + + public static getWaypoint(leg: string): Waypoint { + const regex = leg.match(/^([A-Z]+)(\d{3})-(\d{4})$/); // {name}{bearing}-{distance} + if (regex?.length == 4) { + return { + name: regex[1], + offset: { + bearing: parseInt(regex[2]), + distance: parseInt(regex[3]) / 10, + }, + }; + } + + const waypoint = leg.split(','); + if (waypoint.length == 2) { + const position = CoordinateUtils.decodeStringCoordinatesDecimalMinutes( + waypoint[1], + ); + if (position) { + return { + name: waypoint[0], + latitude: position.latitude, + longitude: position.longitude, + }; + } + } + if (leg.length == 13 || leg.length == 14) { + //looks like coordinates + const position = + CoordinateUtils.decodeStringCoordinatesDecimalMinutes(leg); + const name = waypoint.length == 2 ? waypoint[0] : ''; + if (position) { + return { + name: name, + latitude: position.latitude, + longitude: position.longitude, + }; + } + } + return { name: leg }; + } + + private static waypointsToString(waypoints: Waypoint[]): string { + let str = waypoints + .map((x) => RouteUtils.waypointToString(x)) + .join(' > ') + .replaceAll('> >', '>>'); + if (str.startsWith(' > ')) { + str = '>>' + str.slice(2); + } + return str; + } +} From 0b312e410d5651b032cd709b22c5f66f22966647 Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Wed, 27 May 2026 22:25:40 -0700 Subject: [PATCH 04/23] =?UTF-8?q?Author=20runtimes/rust/=20=E2=80=94=20ads?= =?UTF-8?q?-runtime=20crate=20scaffold?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rust counterpart of runtimes/typescript/. The scaffold targets the import shape the codegen emits: use ads_runtime::{Plugin, Message, Options, DecodeResult, Qualifiers, ResultFormatter, helpers}; Modules: - plugin.rs — Plugin trait, Message/Options/Qualifiers, DecodeResult (with HashMap<&str, serde_json::Value> raw bag), DecodeLevel, fail_unknown() - helpers.rs — all decode-fn helpers the codegen calls (coordinate, coordinate_decimal_minutes, integer, float, string, timestamp_hhmmss, callsign, tail_number, airport, base64_decode, inflate, text_decode, decode_ascii85, hex_decode, bitslice, concat_bits, regex) - coordinate.rs — combined NS/EW DDDDD parser + DDMM.M decimal-minute parser, mirroring CoordinateUtils - ascii85.rs — pure-Rust ASCII85 decoder (Adobe delimiters + 'z' shorthand) - crc.rs — CRC-16/IBM-SDLC reversed-nibble + CRC-16/GENIBUS, plus an match_arinc_702_crc convenience that mirrors the TS algorithm-selection logic - result_formatter.rs — ResultFormatter associated fns (position, altitude, speed, heading, timestamp, callsign, flight_number, tail, departure/arrival_airport, fuel, unknown_arr, unknown) — output strings match TS formatting - types.rs — Route, Waypoint, Wind structs (serde Serialize) - escape_hatches/mod.rs — placeholder index for per-plugin custom fns Cargo deps: base64, flate2, regex, serde, serde_json, once_cell. Builds clean with `cargo build`. CRC tables themselves still TODO — emitted from spec/shared/crc_tables.yaml in a follow-up; current bitwise impl is correct and table-free. Args passed as JSON strings (codegen emits `JSON.stringify(args)`). v1.1 should switch to typed-args codegen for efficiency. Updates .gitignore to skip target/ and build/. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 2 + runtimes/rust/.gitkeep | 0 runtimes/rust/Cargo.lock | 208 ++++++++++++++++++++++++ runtimes/rust/Cargo.toml | 19 +++ runtimes/rust/README.md | 56 +++++++ runtimes/rust/src/ascii85.rs | 49 ++++++ runtimes/rust/src/coordinate.rs | 47 ++++++ runtimes/rust/src/crc.rs | 50 ++++++ runtimes/rust/src/escape_hatches/mod.rs | 19 +++ runtimes/rust/src/helpers.rs | 190 ++++++++++++++++++++++ runtimes/rust/src/lib.rs | 19 +++ runtimes/rust/src/plugin.rs | 127 +++++++++++++++ runtimes/rust/src/result_formatter.rs | 116 +++++++++++++ runtimes/rust/src/types.rs | 29 ++++ 14 files changed, 931 insertions(+) delete mode 100644 runtimes/rust/.gitkeep create mode 100644 runtimes/rust/Cargo.lock create mode 100644 runtimes/rust/Cargo.toml create mode 100644 runtimes/rust/README.md create mode 100644 runtimes/rust/src/ascii85.rs create mode 100644 runtimes/rust/src/coordinate.rs create mode 100644 runtimes/rust/src/crc.rs create mode 100644 runtimes/rust/src/escape_hatches/mod.rs create mode 100644 runtimes/rust/src/helpers.rs create mode 100644 runtimes/rust/src/lib.rs create mode 100644 runtimes/rust/src/plugin.rs create mode 100644 runtimes/rust/src/result_formatter.rs create mode 100644 runtimes/rust/src/types.rs diff --git a/.gitignore b/.gitignore index 1d3550f..0c362d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ node_modules/ dist/ +target/ +build/ *.log .DS_Store .vscode/ diff --git a/runtimes/rust/.gitkeep b/runtimes/rust/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/runtimes/rust/Cargo.lock b/runtimes/rust/Cargo.lock new file mode 100644 index 0000000..b93e037 --- /dev/null +++ b/runtimes/rust/Cargo.lock @@ -0,0 +1,208 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ads-runtime" +version = "0.1.0" +dependencies = [ + "base64", + "flate2", + "once_cell", + "regex", + "serde", + "serde_json", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/runtimes/rust/Cargo.toml b/runtimes/rust/Cargo.toml new file mode 100644 index 0000000..ed79517 --- /dev/null +++ b/runtimes/rust/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ads-runtime" +version = "0.1.0" +edition = "2021" +description = "Rust runtime library for ADS-generated ACARS decoder plugins." +license = "MIT" +repository = "https://github.com/airframesio/airframes-decoder" + +[lib] +name = "ads_runtime" +path = "src/lib.rs" + +[dependencies] +base64 = "0.22" +flate2 = "1.0" +regex = "1.11" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +once_cell = "1.20" diff --git a/runtimes/rust/README.md b/runtimes/rust/README.md new file mode 100644 index 0000000..d825024 --- /dev/null +++ b/runtimes/rust/README.md @@ -0,0 +1,56 @@ +# ads-runtime (Rust) + +Rust runtime library for ADS-generated ACARS decoder plugins. + +Consumed by `acars-decoder-rust` via a path dependency on the +`airframes-decoder` submodule: + +```toml +[dependencies] +ads-runtime = { path = "vendor/airframes-decoder/runtimes/rust" } +``` + +## Layout + +``` +runtimes/rust/ +├── Cargo.toml +├── README.md +└── src/ + ├── lib.rs # public exports: Plugin, Message, Options, DecodeResult, helpers, ... + ├── plugin.rs # Plugin trait + result types + ├── result_formatter.rs # ResultFormatter (position, altitude, timestamp, ...) + ├── helpers.rs # Decode-fn helpers the codegen calls into + ├── coordinate.rs # Combined + decimal-minutes coordinate parsers + ├── ascii85.rs # Pure-Rust ASCII85 decoder + ├── crc.rs # CRC-16 IBM-SDLC and GENIBUS implementations + ├── types.rs # Route, Waypoint, Wind + └── escape_hatches/mod.rs # Per-plugin custom decoder/formatter hatches +``` + +## Generated plugin import shape + +```rust +use ads_runtime::{Plugin, Message, Options, DecodeResult, Qualifiers, ResultFormatter, helpers}; +use crate::escape_hatches; + +pub struct Label_10_POS; + +impl Plugin for Label_10_POS { + fn name(&self) -> &'static str { "label-10-pos" } + fn qualifiers(&self) -> Qualifiers { /* ... */ } + fn decode(&self, message: &Message, options: &Options) -> DecodeResult { /* ... */ } +} +``` + +## Source of truth + +`acars-decoder-typescript` (in production). All decoders must produce +byte-identical output via the shared corpus at `airframes-decoder/corpus/`. + +## Build + +```bash +cargo build +cargo test +``` diff --git a/runtimes/rust/src/ascii85.rs b/runtimes/rust/src/ascii85.rs new file mode 100644 index 0000000..9829cac --- /dev/null +++ b/runtimes/rust/src/ascii85.rs @@ -0,0 +1,49 @@ +//! Pure-Rust ASCII85 / Base85 decoder. +//! +//! Accepts optional `<~...~>` Adobe delimiters and the `z` shorthand for four +//! zero bytes. Returns `None` on invalid input. +//! +//! Mirrors `runtimes/typescript/utils/ascii85.ts`. + +pub fn decode(input: &str) -> Option> { + let s = input.trim(); + let s = s.strip_prefix("<~").unwrap_or(s); + let s = s.strip_suffix("~>").unwrap_or(s); + + let mut out = Vec::with_capacity(s.len() * 4 / 5); + let mut buf: u32 = 0; + let mut count: u8 = 0; + + for ch in s.bytes() { + match ch { + b'z' if count == 0 => { + out.extend_from_slice(&[0, 0, 0, 0]); + } + b'\t' | b'\n' | b'\r' | b' ' => continue, + 33..=117 => { + buf = buf.wrapping_mul(85).wrapping_add((ch - 33) as u32); + count += 1; + if count == 5 { + out.push((buf >> 24) as u8); + out.push((buf >> 16) as u8); + out.push((buf >> 8) as u8); + out.push(buf as u8); + buf = 0; + count = 0; + } + } + _ => return None, + } + } + + if count > 0 { + for _ in count..5 { + buf = buf.wrapping_mul(85).wrapping_add(84); + } + out.push((buf >> 24) as u8); + if count >= 3 { out.push((buf >> 16) as u8); } + if count >= 4 { out.push((buf >> 8) as u8); } + } + + Some(out) +} diff --git a/runtimes/rust/src/coordinate.rs b/runtimes/rust/src/coordinate.rs new file mode 100644 index 0000000..507f203 --- /dev/null +++ b/runtimes/rust/src/coordinate.rs @@ -0,0 +1,47 @@ +//! Coordinate parsing — combined and decimal-minutes formats. +//! +//! Mirrors `runtimes/typescript/utils/coordinate_utils.ts`. + +/// Combined "N12345W123456" → (latitude_millidegrees, longitude_millidegrees). +pub fn decode_combined(s: &str) -> Option<(f64, f64)> { + let bytes = s.as_bytes(); + if bytes.len() < 13 { return None; } + let first = bytes[0] as char; + let (middle_idx, lon_start) = if bytes[6] == b' ' { (7, 8) } else { (6, 7) }; + if bytes.len() < lon_start + 6 { return None; } + let middle = bytes[middle_idx] as char; + if !(first == 'N' || first == 'S') || !(middle == 'E' || middle == 'W') { + return None; + } + let lat_str = std::str::from_utf8(&bytes[1..6]).ok()?; + let lon_str = std::str::from_utf8(&bytes[lon_start..lon_start + 6]).ok()?; + let lat: f64 = lat_str.parse().ok()?; + let lon: f64 = lon_str.parse().ok()?; + let lat_sign = if first == 'N' { 1.0 } else { -1.0 }; + let lon_sign = if middle == 'E' { 1.0 } else { -1.0 }; + Some((lat / 1000.0 * lat_sign, lon / 1000.0 * lon_sign)) +} + +/// DDMM.M decimal-minute parser. +pub fn decode_decimal_minutes(s: &str) -> Option<(f64, f64)> { + let bytes = s.as_bytes(); + if bytes.len() < 13 { return None; } + let first = bytes[0] as char; + let (middle_idx, lon_start) = if bytes[6] == b' ' { (7, 8) } else { (6, 7) }; + if bytes.len() < lon_start + 6 { return None; } + let middle = bytes[middle_idx] as char; + if !(first == 'N' || first == 'S') || !(middle == 'E' || middle == 'W') { + return None; + } + let lat_str = std::str::from_utf8(&bytes[1..6]).ok()?; + let lon_str = std::str::from_utf8(&bytes[lon_start..lon_start + 6]).ok()?; + let lat_n: f64 = lat_str.parse().ok()?; + let lon_n: f64 = lon_str.parse().ok()?; + let lat_deg = (lat_n / 1000.0).trunc(); + let lat_min = (lat_n % 1000.0) / 10.0; + let lon_deg = (lon_n / 1000.0).trunc(); + let lon_min = (lon_n % 1000.0) / 10.0; + let lat_sign = if first == 'N' { 1.0 } else { -1.0 }; + let lon_sign = if middle == 'E' { 1.0 } else { -1.0 }; + Some(((lat_deg + lat_min / 60.0) * lat_sign, (lon_deg + lon_min / 60.0) * lon_sign)) +} diff --git a/runtimes/rust/src/crc.rs b/runtimes/rust/src/crc.rs new file mode 100644 index 0000000..eb0dcae --- /dev/null +++ b/runtimes/rust/src/crc.rs @@ -0,0 +1,50 @@ +//! CRC algorithms used by ACARS / ARINC 702 / MIAM PDUs. +//! +//! These mirror `runtimes/typescript/utils/arinc_702_helper.ts`. The lookup +//! tables themselves will be emitted from `spec/shared/crc_tables.yaml` in a +//! follow-up; the bitwise implementations below are correct and table-free. + +/// CRC-16/IBM-SDLC with reversed nibbles, as ARINC 702 H1 messages use it. +pub fn crc16_ibm_sdlc_rev(data: &[u8]) -> u16 { + let mut crc: u32 = 0xffff; + for &byte in data { + crc ^= byte as u32; + for _ in 0..8 { + if crc & 0x0001 != 0 { + crc = (crc >> 1) ^ 0x8408; + } else { + crc >>= 1; + } + } + } + let crc = (crc ^ 0xffff) & 0xffff; + let n1 = (crc >> 12) & 0xf; + let n2 = (crc >> 8) & 0xf; + let n3 = (crc >> 4) & 0xf; + let n4 = crc & 0xf; + ((n4 << 12) | (n3 << 8) | (n2 << 4) | n1) as u16 +} + +/// CRC-16/GENIBUS (poly 0x1021, init 0xffff). +pub fn crc16_genibus(data: &[u8]) -> u16 { + let mut crc: u32 = 0xffff; + let polynomial: u32 = 0x1021; + for &byte in data { + crc ^= (byte as u32) << 8; + for _ in 0..8 { + if crc & 0x8000 != 0 { + crc = ((crc << 1) ^ polynomial) & 0xffff; + } else { + crc = (crc << 1) & 0xffff; + } + } + } + ((crc ^ 0xffff) & 0xffff) as u16 +} + +/// Convenience: try both CRC variants and report which (if any) matched. +pub fn match_arinc_702_crc(data: &[u8], expected: u16) -> Option<&'static str> { + if crc16_ibm_sdlc_rev(data) == expected { return Some("IBM-SDLC"); } + if crc16_genibus(data) == expected { return Some("GENIBUS"); } + None +} diff --git a/runtimes/rust/src/escape_hatches/mod.rs b/runtimes/rust/src/escape_hatches/mod.rs new file mode 100644 index 0000000..fa00797 --- /dev/null +++ b/runtimes/rust/src/escape_hatches/mod.rs @@ -0,0 +1,19 @@ +//! Per-plugin escape-hatch functions referenced from spec YAML via `custom: `. +//! +//! As specs are ported and reveal needed hatches, add the corresponding files +//! here and re-export. See `docs/ESCAPE_HATCHES.md` for the contract. +//! +//! Generated plugin code does: +//! use crate::escape_hatches; +//! escape_hatches::(...) +//! +//! Currently expected (from reference specs): +//! - arinc_702_dispatch (parse-level whole-plugin hatch) +//! - arinc_702_format (formatter-level) +//! - label_4a_variant_2_decode (field-level) +//! - label_4a_variant_3_position (field-level) +//! - label_4a_format (formatter-level) +//! - ohma_unwrap_message (field-level) +//! - ohma_message_item (formatter-level) +//! - parse_flight_level_or_ground (decode-fn level) +//! - flight_level_to_altitude_feet (decode-fn level) diff --git a/runtimes/rust/src/helpers.rs b/runtimes/rust/src/helpers.rs new file mode 100644 index 0000000..3ccdc41 --- /dev/null +++ b/runtimes/rust/src/helpers.rs @@ -0,0 +1,190 @@ +//! Decode-fn helpers the codegen emits calls into. +//! +//! Mirrors `runtimes/typescript/helpers.ts`. Each function corresponds to a +//! `decode.fn` value in spec YAML. Args arrive as JSON-encoded strings (the +//! emitter uses `JSON.stringify(args)` for portability); v1.1 should switch +//! to typed args. + +use serde_json::Value as JsonValue; +use crate::ascii85; +use crate::coordinate; + +fn parse_args(s: &str) -> JsonValue { + serde_json::from_str(s).unwrap_or(JsonValue::Null) +} + +// ─── coordinates ───────────────────────────────────────────────────────────── + +pub fn coordinate(value: &str, args_json: &str) -> JsonValue { + let args = parse_args(args_json); + let trimmed = value.trim(); + let style = args.get("style").and_then(JsonValue::as_str).unwrap_or("single_axis"); + if style == "combined" { + return match coordinate::decode_combined(trimmed) { + Some(c) => serde_json::json!({"latitude": c.0, "longitude": c.1}), + None => JsonValue::Null, + }; + } + let divisor = args.get("divisor").and_then(JsonValue::as_f64).unwrap_or(1000.0); + let positive = ["N", "E"]; + let prefix = match trimmed.chars().next() { + Some(c) => c.to_string(), + None => return JsonValue::Null, + }; + let sign: f64 = if positive.contains(&prefix.as_str()) { 1.0 } else { -1.0 }; + let digits = trimmed[1..].trim(); + let n: f64 = match digits.parse() { + Ok(n) => n, + Err(_) => return JsonValue::Null, + }; + serde_json::json!(sign * n / divisor) +} + +pub fn coordinate_decimal_minutes(value: &str, _args_json: &str) -> JsonValue { + match coordinate::decode_decimal_minutes(value) { + Some(c) => serde_json::json!({"latitude": c.0, "longitude": c.1}), + None => JsonValue::Null, + } +} + +// ─── numerics ──────────────────────────────────────────────────────────────── + +pub fn integer(value: &str) -> JsonValue { + integer_with_args(value, "{}") +} + +pub fn integer_with_args(value: &str, args_json: &str) -> JsonValue { + let args = parse_args(args_json); + let mut s = value; + let owned: String; + let start = args.get("substring_start").and_then(JsonValue::as_u64).unwrap_or(0) as usize; + if let Some(len) = args.get("substring_length").and_then(JsonValue::as_u64) { + owned = s.chars().skip(start).take(len as usize).collect(); + s = &owned; + } else if start > 0 { + owned = s.chars().skip(start).collect(); + s = &owned; + } + let n: f64 = s.parse().unwrap_or(0.0); + let mult = args.get("multiplier").and_then(JsonValue::as_f64).unwrap_or(1.0); + JsonValue::from(n * mult) +} + +pub fn float(value: &str) -> JsonValue { + let n: f64 = value.parse().unwrap_or(0.0); + JsonValue::from(n) +} + +// ─── strings ───────────────────────────────────────────────────────────────── + +pub fn string(value: &str) -> JsonValue { JsonValue::String(value.to_string()) } +pub fn trim(value: &str) -> JsonValue { JsonValue::String(value.trim().to_string()) } +pub fn uppercase(value: &str) -> JsonValue { JsonValue::String(value.to_uppercase()) } +pub fn lowercase(value: &str) -> JsonValue { JsonValue::String(value.to_lowercase()) } + +// ─── identifiers ───────────────────────────────────────────────────────────── + +pub fn callsign(value: &str) -> JsonValue { JsonValue::String(value.trim().to_string()) } +pub fn flight_number(value: &str) -> JsonValue { JsonValue::String(value.trim().to_string()) } +pub fn airport(value: &str) -> JsonValue { JsonValue::String(value.trim().to_uppercase()) } + +pub fn tail_number(value: &str, args_json: &str) -> JsonValue { + let args = parse_args(args_json); + let mut s = value.trim().to_string(); + if let Some(strip) = args.get("strip_chars").and_then(JsonValue::as_str) { + for c in strip.chars() { + s = s.replace(c, ""); + } + } + JsonValue::String(s) +} + +// ─── timestamps ────────────────────────────────────────────────────────────── + +pub fn timestamp_hhmmss(value: &str) -> JsonValue { + parse_hhmmss_to_tod(value) +} + +pub fn timestamp_hhmmss_with_args(value: &str, args_json: &str) -> JsonValue { + let args = parse_args(args_json); + let s = if let Some(app) = args.get("append").and_then(JsonValue::as_str) { + format!("{}{}", value, app) + } else { + value.to_string() + }; + parse_hhmmss_to_tod(&s) +} + +fn parse_hhmmss_to_tod(s: &str) -> JsonValue { + if s.len() < 6 { return JsonValue::Null; } + let h: i64 = s[0..2].parse().unwrap_or(0); + let m: i64 = s[2..4].parse().unwrap_or(0); + let sec: i64 = s[4..6].parse().unwrap_or(0); + JsonValue::from(h * 3600 + m * 60 + sec) +} + +// ─── binary / encoding ─────────────────────────────────────────────────────── + +pub fn base64_decode(value: &str) -> Vec { + use base64::{engine::general_purpose::STANDARD, Engine}; + STANDARD.decode(value.trim()).unwrap_or_default() +} + +pub fn inflate(bytes: &[u8], format: &str) -> Vec { + use flate2::read::{DeflateDecoder, GzDecoder, ZlibDecoder}; + use std::io::Read; + let mut out = Vec::new(); + match format { + "zlib" => { let _ = ZlibDecoder::new(bytes).read_to_end(&mut out); } + "gzip" => { let _ = GzDecoder::new(bytes).read_to_end(&mut out); } + _ => { let _ = DeflateDecoder::new(bytes).read_to_end(&mut out); } + } + out +} + +pub fn text_decode(bytes: &[u8], _encoding: &str) -> String { + // ASCII / Latin1 / UTF-8 all round-trip safely through from_utf8_lossy + // for ACARS message text; specialize when a real divergence appears. + String::from_utf8_lossy(bytes).into_owned() +} + +pub fn decode_ascii85(value: &str) -> Vec { + ascii85::decode(value).unwrap_or_default() +} + +pub fn hex_decode(value: &str) -> Vec { + let clean: String = value.chars().filter(|c| c.is_ascii_hexdigit()).collect(); + (0..clean.len() / 2) + .map(|i| u8::from_str_radix(&clean[i * 2..i * 2 + 2], 16).unwrap_or(0)) + .collect() +} + +// ─── bitfield ──────────────────────────────────────────────────────────────── + +/// Extract bits [start..end] (inclusive) from a byte with bit 0 = MSB. +pub fn bitslice(byte: u8, start: u8, end: u8) -> u32 { + let width = end - start + 1; + let shift = 8 - end - 1; + let mask = (1u32 << width) - 1; + ((byte as u32) >> shift) & mask +} + +pub fn concat_bits(values: &[u32]) -> u32 { + values.iter().fold(0u32, |acc, &v| (acc << 8) | v) +} + +// ─── regex ─────────────────────────────────────────────────────────────────── + +use once_cell::sync::Lazy; +use std::collections::HashMap; +use std::sync::Mutex; + +static REGEX_CACHE: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); + +pub fn regex(pattern: &str) -> regex::Regex { + let mut cache = REGEX_CACHE.lock().expect("regex cache poisoned"); + cache + .entry(pattern.to_string()) + .or_insert_with(|| regex::Regex::new(pattern).expect("invalid regex")) + .clone() +} diff --git a/runtimes/rust/src/lib.rs b/runtimes/rust/src/lib.rs new file mode 100644 index 0000000..2668be6 --- /dev/null +++ b/runtimes/rust/src/lib.rs @@ -0,0 +1,19 @@ +//! `ads-runtime` — Rust runtime library for ADS-generated ACARS decoder plugins. +//! +//! Generated plugin code (output of `ads-gen --target rust`) imports from this +//! crate. The Stage 2 Rust impl (`acars-decoder-rust`) depends on this crate +//! via a path dependency on the `airframes-decoder` submodule. +//! +//! Behavior matches `acars-decoder-typescript` byte-for-byte; the shared +//! corpus (`airframes-decoder/corpus/`) enforces parity. + +pub mod plugin; +pub mod result_formatter; +pub mod helpers; +pub mod ascii85; +pub mod coordinate; +pub mod crc; +pub mod types; + +pub use plugin::{DecodeResult, Message, Options, Plugin, Qualifiers, RawValue}; +pub use result_formatter::ResultFormatter; diff --git a/runtimes/rust/src/plugin.rs b/runtimes/rust/src/plugin.rs new file mode 100644 index 0000000..eaf5a55 --- /dev/null +++ b/runtimes/rust/src/plugin.rs @@ -0,0 +1,127 @@ +//! Core plugin trait + message/result types. + +use serde::Serialize; +use serde_json::Value as JsonValue; +use std::collections::HashMap; + +/// Runtime value attached to `DecodeResult.raw`. Backed by `serde_json::Value` +/// so plugins can stash arbitrary nested data without bespoke types. +pub type RawValue = JsonValue; + +#[derive(Debug, Clone, Serialize)] +pub struct Message { + pub label: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub sublabel: Option, + pub text: String, +} + +#[derive(Debug, Clone, Default, Serialize)] +pub struct Options { + #[serde(default)] + pub debug: bool, +} + +#[derive(Debug, Clone, Serialize)] +pub struct Qualifiers { + pub labels: Vec<&'static str>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub preambles: Vec<&'static str>, +} + +#[derive(Debug, Clone, Serialize)] +pub struct DecoderInfo { + pub name: &'static str, + pub kind: &'static str, + pub decode_level: DecodeLevel, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum DecodeLevel { + None, + Partial, + Full, +} + +#[derive(Debug, Clone, Serialize)] +pub struct FormattedItem { + pub kind: &'static str, + pub code: &'static str, + pub label: String, + pub value: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct Formatted { + pub description: String, + pub items: Vec, +} + +#[derive(Debug, Clone, Default, Serialize)] +pub struct Remaining { + #[serde(skip_serializing_if = "Option::is_none")] + pub text: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct DecodeResult { + pub decoded: bool, + pub decoder: DecoderInfo, + pub formatted: Formatted, + pub raw: HashMap<&'static str, RawValue>, + pub remaining: Remaining, + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, +} + +impl DecodeResult { + pub fn new(name: &'static str, description: &str) -> Self { + Self { + decoded: false, + decoder: DecoderInfo { + name, + kind: "pattern-match", + decode_level: DecodeLevel::None, + }, + formatted: Formatted { + description: description.to_string(), + items: Vec::new(), + }, + raw: HashMap::new(), + remaining: Remaining::default(), + message: None, + } + } + + pub fn set_message(&mut self, message: &Message) { + self.message = Some(message.clone()); + } + + pub fn set_decoded(&mut self, decoded: bool) { + self.decoded = decoded; + self.decoder.decode_level = if !decoded { + DecodeLevel::None + } else if self.remaining.text.is_some() { + DecodeLevel::Partial + } else { + DecodeLevel::Full + }; + } + + /// Consumes the result and returns a failure variant with `decoded=false` + /// and the remaining text set. Matches the TS `failUnknown` semantics. + pub fn fail_unknown(mut self, text: &str) -> Self { + self.decoded = false; + self.decoder.decode_level = DecodeLevel::None; + self.remaining.text = Some(text.to_string()); + self + } +} + +/// The trait every generated plugin implements. +pub trait Plugin: Send + Sync { + fn name(&self) -> &'static str; + fn qualifiers(&self) -> Qualifiers; + fn decode(&self, message: &Message, options: &Options) -> DecodeResult; +} diff --git a/runtimes/rust/src/result_formatter.rs b/runtimes/rust/src/result_formatter.rs new file mode 100644 index 0000000..6561377 --- /dev/null +++ b/runtimes/rust/src/result_formatter.rs @@ -0,0 +1,116 @@ +//! ResultFormatter — pushes structured items onto `DecodeResult.formatted.items`. +//! +//! Mirrors the TS `ResultFormatter` static-class API. Function names match +//! `runtimes/typescript/utils/result_formatter.ts` so the cross-language corpus +//! sees identical output shapes. + +use serde_json::Value as JsonValue; +use crate::plugin::{DecodeResult, FormattedItem}; + +pub struct ResultFormatter; + +impl ResultFormatter { + fn push(result: &mut DecodeResult, kind: &'static str, code: &'static str, label: &str, value: String) { + result.formatted.items.push(FormattedItem { + kind, + code, + label: label.to_string(), + value, + }); + } + + pub fn position(result: &mut DecodeResult, latitude: JsonValue, longitude: JsonValue) { + result.raw.insert("position", serde_json::json!({ + "latitude": latitude, + "longitude": longitude, + })); + let lat = latitude.as_f64().unwrap_or(0.0); + let lon = longitude.as_f64().unwrap_or(0.0); + let lat_dir = if lat >= 0.0 { "N" } else { "S" }; + let lon_dir = if lon >= 0.0 { "E" } else { "W" }; + let display = format!("{:.3} {}, {:.3} {}", lat.abs(), lat_dir, lon.abs(), lon_dir); + Self::push(result, "aircraft_position", "ARP", "Aircraft Position", display); + } + + /// Position from a pre-computed `{latitude, longitude}` value. + pub fn position_value(result: &mut DecodeResult, value: JsonValue) { + let lat = value.get("latitude").and_then(JsonValue::as_f64).unwrap_or(0.0); + let lon = value.get("longitude").and_then(JsonValue::as_f64).unwrap_or(0.0); + Self::position(result, lat.into(), lon.into()); + } + + pub fn altitude(result: &mut DecodeResult, value: JsonValue) { + let n = value.as_f64().unwrap_or(0.0); + result.raw.insert("altitude", value); + Self::push(result, "altitude", "ALT", "Altitude", format!("{} feet", n)); + } + + pub fn speed(result: &mut DecodeResult, value: JsonValue) { + let n = value.as_f64().unwrap_or(0.0); + result.raw.insert("speed", value); + Self::push(result, "speed", "SPD", "Speed", format!("{} knots", n)); + } + + pub fn heading(result: &mut DecodeResult, value: JsonValue) { + let n = value.as_f64().unwrap_or(0.0); + result.raw.insert("heading", value); + Self::push(result, "heading", "HDG", "Heading", format!("{}°", n)); + } + + pub fn timestamp(result: &mut DecodeResult, value: JsonValue) { + let n = value.as_i64().unwrap_or(0); + result.raw.insert("message_timestamp", value); + Self::push(result, "timestamp", "TS", "Timestamp", n.to_string()); + } + + pub fn callsign(result: &mut DecodeResult, value: JsonValue) { + let s = value.as_str().unwrap_or("").to_string(); + result.raw.insert("callsign", value); + Self::push(result, "callsign", "CS", "Callsign", s); + } + + pub fn flight_number(result: &mut DecodeResult, value: JsonValue) { + let s = value.as_str().unwrap_or("").to_string(); + result.raw.insert("flight_number", value); + Self::push(result, "flight_number", "FLT", "Flight Number", s); + } + + pub fn tail(result: &mut DecodeResult, value: JsonValue) { + let s = value.as_str().unwrap_or("").to_string(); + result.raw.insert("tail", value); + Self::push(result, "tail", "TAIL", "Tail Number", s); + } + + pub fn departure_airport(result: &mut DecodeResult, value: JsonValue) { + let s = value.as_str().unwrap_or("").to_string(); + result.raw.insert("departure_icao", value); + Self::push(result, "airport_origin", "DEP", "Origin", s); + } + + pub fn arrival_airport(result: &mut DecodeResult, value: JsonValue) { + let s = value.as_str().unwrap_or("").to_string(); + result.raw.insert("arrival_icao", value); + Self::push(result, "airport_destination", "ARR", "Destination", s); + } + + pub fn fuel(result: &mut DecodeResult, value: JsonValue) { + let n = value.as_f64().unwrap_or(0.0); + result.raw.insert("fuel_on_board", value); + Self::push(result, "fuel", "FUEL", "Fuel", n.to_string()); + } + + pub fn unknown_arr(result: &mut DecodeResult, values: Vec) { + let joined = values.join(","); + match result.remaining.text.as_mut() { + Some(t) => { t.push(','); t.push_str(&joined); } + None => result.remaining.text = Some(joined), + } + } + + pub fn unknown(result: &mut DecodeResult, value: String) { + match result.remaining.text.as_mut() { + Some(t) => { t.push(','); t.push_str(&value); } + None => result.remaining.text = Some(value), + } + } +} diff --git a/runtimes/rust/src/types.rs b/runtimes/rust/src/types.rs new file mode 100644 index 0000000..d15b70d --- /dev/null +++ b/runtimes/rust/src/types.rs @@ -0,0 +1,29 @@ +//! Cross-plugin data types (Route, Waypoint, Wind). Mirrors `runtimes/typescript/types/`. + +use serde::Serialize; + +#[derive(Debug, Clone, Serialize)] +pub struct Waypoint { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub time: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub latitude: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub longitude: Option, +} + +#[derive(Debug, Clone, Serialize)] +pub struct Route { + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + pub waypoints: Vec, +} + +#[derive(Debug, Clone, Serialize)] +pub struct Wind { + pub direction: f64, + pub speed: f64, + #[serde(skip_serializing_if = "Option::is_none")] + pub altitude: Option, +} From f38aee77517fd8507b36df186e3a630c70f5ee9a Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Wed, 27 May 2026 22:34:31 -0700 Subject: [PATCH 05/23] =?UTF-8?q?Author=20runtimes/c/=20=E2=80=94=20ads=5F?= =?UTF-8?q?runtime=5Fc=20library=20scaffold?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure-C runtime mirror of runtimes/typescript/ and runtimes/rust/. Targets the API surface the codegen emits: #include "ads_runtime.h" #include "ads_helpers.h" #include "ads_escape_hatches.h" ads_decode_result_t *_decode(...) ads_qualifiers_t _qualifiers(void) extern const ads_plugin_descriptor_t _descriptor; Files: - CMakeLists.txt — static lib (optional shared variant), Asan config, zlib detection, cjson link - include/ads_runtime.h — core types: Message, Options, Qualifiers, DecodeResult, value/string_list/bytes, plugin descriptor; result + value lifecycle API - include/ads_helpers.h — all decode-fn helpers, formatter helpers, split/regex/substring/in primitives - include/ads_escape_hatches.h — placeholder header - src/plugin.c — DecodeResult lifecycle + ads_value_*, backed by cJSON for the raw bag - src/helpers.c — numerics, strings, identifiers, timestamps, bitslice, concat_bits - src/coordinate.c — combined NS/EW + decimal-minutes parsers - src/ascii85.c — ASCII85 + base64 + hex + zlib inflate - src/crc.c — CRC-16 IBM-SDLC reversed-nibble + GENIBUS - src/result_formatter.c — ads_fmt_* item-push helpers - src/string_list.c — ads_split / ads_substring / ads_str_in - src/regex.c — POSIX-regex backed ads_regex_* (Stage 2: upgrade to PCRE2 for named-capture support) Deps: cjson (raw bag + JSON args parsing), zlib (optional, gates ads_inflate behind ADS_HAVE_ZLIB), POSIX regex (libc). Builds clean as libads_runtime_c.a via: mkdir build && cd build && cmake .. && cmake --build . Caveats: - POSIX regex doesn't support PCRE-style named groups. `ads_regex_group` resolves numeric indices only for v1; Stage 2 plans PCRE2 swap. - Args still flow as JSON strings; typed-args codegen pending for v1.1. - CRC lookup tables emitted from spec/shared/crc_tables.yaml are pending; current bitwise impl is correct and table-free. Co-Authored-By: Claude Opus 4.7 (1M context) --- runtimes/c/.gitkeep | 0 runtimes/c/CMakeLists.txt | 47 +++++++ runtimes/c/README.md | 78 +++++++++++ runtimes/c/include/ads_escape_hatches.h | 32 +++++ runtimes/c/include/ads_helpers.h | 81 +++++++++++ runtimes/c/include/ads_runtime.h | 119 ++++++++++++++++ runtimes/c/src/ascii85.c | 144 +++++++++++++++++++ runtimes/c/src/coordinate.c | 88 ++++++++++++ runtimes/c/src/crc.c | 35 +++++ runtimes/c/src/helpers.c | 158 +++++++++++++++++++++ runtimes/c/src/plugin.c | 175 ++++++++++++++++++++++++ runtimes/c/src/regex.c | 73 ++++++++++ runtimes/c/src/result_formatter.c | 115 ++++++++++++++++ runtimes/c/src/string_list.c | 55 ++++++++ 14 files changed, 1200 insertions(+) delete mode 100644 runtimes/c/.gitkeep create mode 100644 runtimes/c/CMakeLists.txt create mode 100644 runtimes/c/README.md create mode 100644 runtimes/c/include/ads_escape_hatches.h create mode 100644 runtimes/c/include/ads_helpers.h create mode 100644 runtimes/c/include/ads_runtime.h create mode 100644 runtimes/c/src/ascii85.c create mode 100644 runtimes/c/src/coordinate.c create mode 100644 runtimes/c/src/crc.c create mode 100644 runtimes/c/src/helpers.c create mode 100644 runtimes/c/src/plugin.c create mode 100644 runtimes/c/src/regex.c create mode 100644 runtimes/c/src/result_formatter.c create mode 100644 runtimes/c/src/string_list.c diff --git a/runtimes/c/.gitkeep b/runtimes/c/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/runtimes/c/CMakeLists.txt b/runtimes/c/CMakeLists.txt new file mode 100644 index 0000000..46d76ad --- /dev/null +++ b/runtimes/c/CMakeLists.txt @@ -0,0 +1,47 @@ +cmake_minimum_required(VERSION 3.16) +project(ads_runtime_c C) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +add_compile_options(-Wall -Wextra -Wpedantic -Werror=implicit-function-declaration) + +# Optional sanitizer build: cmake -DCMAKE_BUILD_TYPE=Asan +set(CMAKE_C_FLAGS_ASAN "-O0 -g -fsanitize=address,undefined -fno-omit-frame-pointer") +set(CMAKE_LINKER_FLAGS_ASAN "-fsanitize=address,undefined") + +find_package(ZLIB) +find_library(CJSON_LIB cjson) +find_path(CJSON_INCLUDE_DIR cjson/cJSON.h) + +add_library(ads_runtime_c STATIC + src/plugin.c + src/helpers.c + src/coordinate.c + src/ascii85.c + src/crc.c + src/result_formatter.c + src/string_list.c + src/regex.c +) +target_include_directories(ads_runtime_c PUBLIC include) +target_include_directories(ads_runtime_c PRIVATE ${CJSON_INCLUDE_DIR}) +target_link_libraries(ads_runtime_c PUBLIC ${CJSON_LIB}) +if(ZLIB_FOUND) + target_link_libraries(ads_runtime_c PUBLIC ZLIB::ZLIB) + target_compile_definitions(ads_runtime_c PRIVATE ADS_HAVE_ZLIB) +endif() + +# Optionally build a shared library too. +option(ADS_RUNTIME_SHARED "Also build a shared library variant" OFF) +if(ADS_RUNTIME_SHARED) + add_library(ads_runtime_c_shared SHARED ${ADS_RUNTIME_SOURCES}) + set_target_properties(ads_runtime_c_shared PROPERTIES OUTPUT_NAME ads_runtime_c) +endif() + +enable_testing() diff --git a/runtimes/c/README.md b/runtimes/c/README.md new file mode 100644 index 0000000..e291d27 --- /dev/null +++ b/runtimes/c/README.md @@ -0,0 +1,78 @@ +# ads_runtime_c + +Pure-C runtime library for ADS-generated ACARS decoder plugins. + +Consumed by `acars-decoder-c` via CMake `add_subdirectory()` on the +`airframes-decoder` submodule: + +```cmake +add_subdirectory(vendor/airframes-decoder/runtimes/c) +target_link_libraries(acars_decoder PRIVATE ads_runtime_c) +``` + +## Layout + +``` +runtimes/c/ +├── CMakeLists.txt +├── README.md +├── include/ +│ ├── ads_runtime.h # core types (Message, Options, DecodeResult, Qualifiers, plugin descriptor, value/bytes/string_list) +│ ├── ads_helpers.h # decode-fn + formatter + split/regex/substring helpers +│ └── ads_escape_hatches.h # placeholder header for per-plugin custom fns +└── src/ + ├── plugin.c # ads_result_* lifecycle + ads_value_* + ├── helpers.c # decode-fn helpers (integer, float, timestamp, callsign, airport, ...) + ├── coordinate.c # combined NS/EW + decimal-minutes parsers + ├── ascii85.c # ASCII85 + base64 + hex decoders, inflate + ├── crc.c # CRC-16 IBM-SDLC reversed + GENIBUS + ├── result_formatter.c # ads_fmt_* item push helpers + ├── string_list.c # ads_split, ads_substring, ads_str_in + └── regex.c # POSIX-regex based ads_regex_* (Stage 2: replace with PCRE2 for named groups) +``` + +## Dependencies + +- `cjson` — used internally for the raw-fields bag and JSON-encoded args parsing. +- `zlib` (optional, gates ads_inflate behind ADS_HAVE_ZLIB) +- POSIX regex (POSIX-2008+ libc). Stage 2 may replace with PCRE2 for named-capture support. + +## Build + +```bash +mkdir build && cd build +cmake .. -DCMAKE_BUILD_TYPE=Release +cmake --build . +``` + +For sanitizers: + +```bash +cmake .. -DCMAKE_BUILD_TYPE=Asan +cmake --build . +``` + +## Generated plugin shape + +```c +#include "ads_runtime.h" +#include "ads_helpers.h" +#include "ads_escape_hatches.h" + +ads_decode_result_t *label_10_pos_decode(const ads_message_t *msg, const ads_options_t *opts); +ads_qualifiers_t label_10_pos_qualifiers(void); +extern const ads_plugin_descriptor_t label_10_pos_descriptor; +``` + +## Caveats / Stage 2 follow-ups + +- POSIX regex doesn't support PCRE-style named groups (`(?...)`). For + the regex-using specs (Label_44_POS), `ads_regex_group("name")` currently + resolves numeric indices only. Plan to swap in PCRE2 in Stage 2. +- Args still flow as JSON strings (`JSON.stringify(args)`); typed-args + codegen is a v1.1 win for hot paths. +- CRC lookup tables emitted from `spec/shared/crc_tables.yaml` are pending; + current bitwise impl is correct and table-free. +- Memory-management: `ads_value_t *` returned by decode helpers transfers + ownership to the result on `ads_result_raw_set` (which frees the wrapper). + Be careful in escape hatches not to double-free. diff --git a/runtimes/c/include/ads_escape_hatches.h b/runtimes/c/include/ads_escape_hatches.h new file mode 100644 index 0000000..d0778ed --- /dev/null +++ b/runtimes/c/include/ads_escape_hatches.h @@ -0,0 +1,32 @@ +/* ads_escape_hatches.h — placeholder for per-plugin custom functions. + * + * As specs are ported and reveal needed hatches, add prototypes here and + * implementations under src/escape_hatches/. See docs/ESCAPE_HATCHES.md. + * + * Currently expected (from the 5 reference specs): + * ads_hatch_arinc_702_dispatch + * ads_hatch_arinc_702_format + * ads_hatch_label_4a_variant_2_decode + * ads_hatch_label_4a_variant_3_position + * ads_hatch_label_4a_format + * ads_hatch_ohma_unwrap_message + * ads_hatch_ohma_message_item + * ads_hatch_parse_flight_level_or_ground + * ads_hatch_flight_level_to_altitude_feet + */ +#ifndef ADS_ESCAPE_HATCHES_H +#define ADS_ESCAPE_HATCHES_H + +#include "ads_runtime.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Prototypes added per-plugin during Stage 2 implementation. */ + +#ifdef __cplusplus +} +#endif + +#endif /* ADS_ESCAPE_HATCHES_H */ diff --git a/runtimes/c/include/ads_helpers.h b/runtimes/c/include/ads_helpers.h new file mode 100644 index 0000000..a3413db --- /dev/null +++ b/runtimes/c/include/ads_helpers.h @@ -0,0 +1,81 @@ +/* ads_helpers.h — decode-fn helpers the codegen emits calls into. + * + * Each function corresponds to a `decode.fn` value in spec YAML. Mirrors + * runtimes/typescript/helpers.ts and runtimes/rust/src/helpers.rs. + * + * args_json is a JSON-encoded args object; helpers parse on demand. v1.1 + * should move to typed args. + */ +#ifndef ADS_HELPERS_H +#define ADS_HELPERS_H + +#include "ads_runtime.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ─── Strings + lists + regex ────────────────────────────────────────────── */ + +ads_str_list_t ads_split(const char *s, const char *delimiter); +const char *ads_substring(const char *s, int start, int end); /* end < 0 = to end */ +bool ads_str_in(const char *const *list, size_t count, const char *needle); + +ads_regex_match_t *ads_regex_match_new(const char *pattern, const char *input); +bool ads_regex_match_ok(const ads_regex_match_t *m); +const char *ads_regex_group(const ads_regex_match_t *m, const char *name); +void ads_regex_match_free(ads_regex_match_t *m); +/* Codegen-side convenience returning by value. Wraps _new + ok(). */ +#define ads_regex_match(pattern, input) (*ads_regex_match_new(pattern, input)) +bool ads_regex_test(const char *pattern, const char *input); + +/* ─── Decode-fn helpers ──────────────────────────────────────────────────── */ + +ads_value_t *ads_decode_coordinate(const char *value, const char *args_json); +ads_value_t *ads_decode_coordinate_decimal_minutes(const char *value, const char *args_json); +ads_value_t *ads_decode_integer(const char *value); +ads_value_t *ads_decode_integer_args(const char *value, const char *args_json); +ads_value_t *ads_decode_float(const char *value); +ads_value_t *ads_decode_string(const char *value); +ads_value_t *ads_decode_trim(const char *value); +ads_value_t *ads_decode_uppercase(const char *value); +ads_value_t *ads_decode_lowercase(const char *value); +ads_value_t *ads_decode_timestamp_hhmmss(const char *value); +ads_value_t *ads_decode_timestamp_hhmmss_args(const char *value, const char *args_json); +ads_value_t *ads_decode_callsign(const char *value); +ads_value_t *ads_decode_tail_number(const char *value, const char *args_json); +ads_value_t *ads_decode_flight_number(const char *value); +ads_value_t *ads_decode_airport(const char *value); + +/* ─── Binary / encoding ──────────────────────────────────────────────────── */ + +ads_bytes_t ads_base64_decode(const char *s); +ads_bytes_t ads_inflate(ads_bytes_t input, size_t offset, const char *format); +ads_bytes_t ads_decode_ascii85(const char *s); +ads_bytes_t ads_hex_decode(const char *s); +char *ads_text_decode(ads_bytes_t bytes, const char *encoding); /* malloc'd */ + +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) ──────── */ + +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); +void ads_fmt_altitude(ads_decode_result_t *r, ads_value_t *v); +void ads_fmt_speed(ads_decode_result_t *r, ads_value_t *v); +void ads_fmt_heading(ads_decode_result_t *r, ads_value_t *v); +void ads_fmt_timestamp(ads_decode_result_t *r, ads_value_t *v); +void ads_fmt_callsign(ads_decode_result_t *r, ads_value_t *v); +void ads_fmt_flight_number(ads_decode_result_t *r, ads_value_t *v); +void ads_fmt_tail(ads_decode_result_t *r, ads_value_t *v); +void ads_fmt_departure_airport(ads_decode_result_t *r, ads_value_t *v); +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); + +#ifdef __cplusplus +} +#endif + +#endif /* ADS_HELPERS_H */ diff --git a/runtimes/c/include/ads_runtime.h b/runtimes/c/include/ads_runtime.h new file mode 100644 index 0000000..7b50f63 --- /dev/null +++ b/runtimes/c/include/ads_runtime.h @@ -0,0 +1,119 @@ +/* ads_runtime.h — public API for the ADS C runtime. + * + * Generated plugin code (output of `ads-gen --target c`) includes this header + * plus ads_helpers.h and ads_escape_hatches.h: + * + * #include "ads_runtime.h" + * #include "ads_helpers.h" + * #include "ads_escape_hatches.h" + * + * ads_decode_result_t *label_10_pos_decode(const ads_message_t *msg, + * const ads_options_t *opts); + * + * Source of truth: acars-decoder-typescript. Behavior MUST match the TS + * reference byte-for-byte; the shared corpus enforces this. + */ +#ifndef ADS_RUNTIME_H +#define ADS_RUNTIME_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ─── Core types ─────────────────────────────────────────────────────────── */ + +typedef struct { + const char *label; /* required */ + const char *sublabel; /* optional, may be NULL */ + const char *text; /* required */ +} ads_message_t; + +typedef struct { + bool debug; +} ads_options_t; + +typedef struct { + const char *const *labels; /* NULL-terminated array of label literals */ + const char *const *preambles; /* NULL-terminated; may itself be NULL */ +} ads_qualifiers_t; + +typedef enum { + ADS_DECODE_LEVEL_NONE = 0, + ADS_DECODE_LEVEL_PARTIAL, + ADS_DECODE_LEVEL_FULL, +} ads_decode_level_t; + +/* Opaque value type. Backed by cJSON internally so plugins can store + * arbitrary nested data. Use ads_value_from_* / ads_value_to_* helpers. */ +typedef struct ads_value ads_value_t; + +/* Opaque dynamic string list. Holds parts produced by ads_split / regex + * matches. Items accessed via .items[N]; size via .count. */ +typedef struct { + char **items; + size_t count; + size_t capacity; +} ads_str_list_t; + +/* Opaque dynamic byte buffer (ads_bytes_t). */ +typedef struct { + uint8_t *data; + size_t len; +} ads_bytes_t; + +/* Regex match result. Backed by POSIX regex_t + match buffer. */ +typedef struct ads_regex_match ads_regex_match_t; + +/* DecodeResult — the structured output of a plugin's decode(). */ +typedef struct ads_decode_result ads_decode_result_t; + +/* Plugin descriptor used by the dispatcher's registry. */ +typedef struct { + const char *name; + ads_qualifiers_t (*qualifiers)(void); + ads_decode_result_t *(*decode)(const ads_message_t *msg, const ads_options_t *opts); +} ads_plugin_descriptor_t; + +/* ─── DecodeResult API ───────────────────────────────────────────────────── */ + +ads_decode_result_t *ads_result_new(const char *plugin_name, + const char *description, + const ads_message_t *msg); +void ads_result_free(ads_decode_result_t *result); + +void ads_result_set_decoded(ads_decode_result_t *result, bool decoded); +ads_decode_result_t *ads_result_fail_unknown(ads_decode_result_t *result, const char *text); + +void ads_result_raw_set(ads_decode_result_t *result, const char *key, ads_value_t *value); + +/* Accessors for callers (mainly tests, escape hatches, formatters). */ +ads_value_t *ads_result_raw_get(const ads_decode_result_t *result, const char *key); +char *ads_result_to_json(const ads_decode_result_t *result); /* malloc'd; caller frees */ + +/* ─── Value API ──────────────────────────────────────────────────────────── */ + +ads_value_t *ads_value_from_string(const char *s); +ads_value_t *ads_value_from_double(double n); +ads_value_t *ads_value_from_int(int64_t n); +ads_value_t *ads_value_from_bool(bool b); +ads_value_t *ads_value_null(void); +void ads_value_free(ads_value_t *v); + +bool ads_value_as_double(const ads_value_t *v, double *out); +bool ads_value_as_int(const ads_value_t *v, int64_t *out); +const char *ads_value_as_string(const ads_value_t *v); + +/* ─── Buffer / string helpers ────────────────────────────────────────────── */ + +void ads_str_list_free(ads_str_list_t *list); +void ads_bytes_free(ads_bytes_t *b); + +#ifdef __cplusplus +} +#endif + +#endif /* ADS_RUNTIME_H */ diff --git a/runtimes/c/src/ascii85.c b/runtimes/c/src/ascii85.c new file mode 100644 index 0000000..7f33d7a --- /dev/null +++ b/runtimes/c/src/ascii85.c @@ -0,0 +1,144 @@ +/* Pure-C ASCII85 / Base85 decoder + base64 + hex + inflate (zlib). */ + +#include "ads_helpers.h" +#include +#include +#include +#include + +#ifdef ADS_HAVE_ZLIB +#include +#endif + +ads_bytes_t ads_decode_ascii85(const char *s) { + ads_bytes_t out = {0}; + if (!s) return out; + const char *p = s; + while (*p && isspace((unsigned char)*p)) p++; + if (strncmp(p, "<~", 2) == 0) p += 2; + size_t cap = strlen(p) * 4 / 5 + 4; + uint8_t *buf = malloc(cap); + if (!buf) return out; + size_t blen = 0; + uint32_t accum = 0; + int count = 0; + + while (*p) { + char ch = *p++; + if (ch == '~') break; /* trailing ~> */ + if (isspace((unsigned char)ch)) continue; + if (ch == 'z' && count == 0) { + buf[blen++] = 0; buf[blen++] = 0; buf[blen++] = 0; buf[blen++] = 0; + continue; + } + if (ch < 33 || ch > 117) { free(buf); return out; } + accum = accum * 85u + (uint32_t)(ch - 33); + count++; + if (count == 5) { + buf[blen++] = (uint8_t)(accum >> 24); + buf[blen++] = (uint8_t)(accum >> 16); + buf[blen++] = (uint8_t)(accum >> 8); + buf[blen++] = (uint8_t)accum; + accum = 0; count = 0; + } + } + if (count > 0) { + for (int i = count; i < 5; i++) accum = accum * 85u + 84u; + buf[blen++] = (uint8_t)(accum >> 24); + if (count >= 3) buf[blen++] = (uint8_t)(accum >> 16); + if (count >= 4) buf[blen++] = (uint8_t)(accum >> 8); + } + out.data = buf; + out.len = blen; + return out; +} + +ads_bytes_t ads_base64_decode(const char *s) { + static int table[256]; + static int initialized = 0; + if (!initialized) { + for (int i = 0; i < 256; i++) table[i] = -1; + const char *alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + for (int i = 0; i < 64; i++) table[(int)alphabet[i]] = i; + initialized = 1; + } + ads_bytes_t out = {0}; + if (!s) return out; + size_t slen = strlen(s); + out.data = malloc(slen); + if (!out.data) return out; + uint32_t buf = 0; + int bits = 0; + for (size_t i = 0; i < slen; i++) { + unsigned char c = (unsigned char)s[i]; + if (c == '=') break; + if (isspace(c)) continue; + int v = table[c]; + if (v < 0) continue; + buf = (buf << 6) | (uint32_t)v; + bits += 6; + if (bits >= 8) { + bits -= 8; + out.data[out.len++] = (uint8_t)(buf >> bits) & 0xff; + } + } + return out; +} + +ads_bytes_t ads_hex_decode(const char *s) { + ads_bytes_t out = {0}; + if (!s) return out; + size_t slen = strlen(s); + out.data = malloc(slen / 2 + 1); + if (!out.data) return out; + char tmp[3] = {0}; + int half = 0; + for (size_t i = 0; i < slen; i++) { + unsigned char c = (unsigned char)s[i]; + if (!isxdigit(c)) continue; + tmp[half++] = (char)c; + if (half == 2) { + out.data[out.len++] = (uint8_t)strtoul(tmp, NULL, 16); + half = 0; + tmp[0] = tmp[1] = 0; + } + } + return out; +} + +char *ads_text_decode(ads_bytes_t bytes, const char *encoding) { + (void)encoding; /* ACARS messages are ASCII / UTF-8 safe */ + char *out = malloc(bytes.len + 1); + if (!out) return NULL; + memcpy(out, bytes.data, bytes.len); + out[bytes.len] = '\0'; + return out; +} + +ads_bytes_t ads_inflate(ads_bytes_t input, size_t offset, const char *format) { + ads_bytes_t out = {0}; +#ifdef ADS_HAVE_ZLIB + if (input.len <= offset) return out; + const uint8_t *src = input.data + offset; + size_t src_len = input.len - offset; + size_t cap = src_len * 8 + 64; + out.data = malloc(cap); + if (!out.data) return out; + z_stream zs = {0}; + int wbits = (strcmp(format ? format : "raw", "gzip") == 0) ? 31 + : (strcmp(format ? format : "raw", "zlib") == 0) ? 15 + : -15; + if (inflateInit2(&zs, wbits) != Z_OK) { free(out.data); out.data = NULL; return out; } + zs.next_in = (Bytef *)src; + zs.avail_in = (uInt)src_len; + zs.next_out = out.data; + zs.avail_out = (uInt)cap; + int rc = inflate(&zs, Z_FINISH); + out.len = zs.total_out; + inflateEnd(&zs); + if (rc != Z_STREAM_END) { free(out.data); out.data = NULL; out.len = 0; } +#else + (void)input; (void)offset; (void)format; +#endif + return out; +} diff --git a/runtimes/c/src/coordinate.c b/runtimes/c/src/coordinate.c new file mode 100644 index 0000000..db9cbc7 --- /dev/null +++ b/runtimes/c/src/coordinate.c @@ -0,0 +1,88 @@ +/* Coordinate parsers: combined NS/EW + decimal-minutes. + * Mirrors runtimes/typescript/utils/coordinate_utils.ts. */ + +#include "ads_helpers.h" +#include +#include +#include +#include + +struct ads_value { cJSON *node; bool owns; }; + +static ads_value_t *combined_to_value(double lat, double lon) { + cJSON *node = cJSON_CreateObject(); + cJSON_AddNumberToObject(node, "latitude", lat); + cJSON_AddNumberToObject(node, "longitude", lon); + ads_value_t *v = calloc(1, sizeof(*v)); + v->node = node; + v->owns = true; + return v; +} + +static bool parse_combined(const char *s, double *lat_out, double *lon_out, bool dec_min) { + size_t slen = strlen(s); + if (slen < 13) return false; + char first = s[0]; + int middle_idx = (s[6] == ' ') ? 7 : 6; + int lon_start = (s[6] == ' ') ? 8 : 7; + if (slen < (size_t)(lon_start + 6)) return false; + char middle = s[middle_idx]; + if ((first != 'N' && first != 'S') || (middle != 'E' && middle != 'W')) return false; + char lat_buf[6] = {0}, lon_buf[7] = {0}; + memcpy(lat_buf, s + 1, 5); + memcpy(lon_buf, s + lon_start, 6); + char *endp; + double lat_n = strtod(lat_buf, &endp); + if (*endp != '\0') return false; + double lon_n = strtod(lon_buf, &endp); + if (*endp != '\0') return false; + double lat_sign = (first == 'N') ? 1.0 : -1.0; + double lon_sign = (middle == 'E') ? 1.0 : -1.0; + if (dec_min) { + double lat_deg = (int64_t)(lat_n / 1000.0); + double lat_min = fmod(lat_n, 1000.0) / 10.0; + double lon_deg = (int64_t)(lon_n / 1000.0); + double lon_min = fmod(lon_n, 1000.0) / 10.0; + *lat_out = (lat_deg + lat_min / 60.0) * lat_sign; + *lon_out = (lon_deg + lon_min / 60.0) * lon_sign; + } else { + *lat_out = (lat_n / 1000.0) * lat_sign; + *lon_out = (lon_n / 1000.0) * lon_sign; + } + return true; +} + +ads_value_t *ads_decode_coordinate(const char *value, const char *args_json) { + cJSON *args = args_json ? cJSON_Parse(args_json) : NULL; + const cJSON *style = args ? cJSON_GetObjectItemCaseSensitive(args, "style") : NULL; + if (style && cJSON_IsString(style) && strcmp(style->valuestring, "combined") == 0) { + double lat, lon; + bool ok = parse_combined(value, &lat, &lon, false); + cJSON_Delete(args); + return ok ? combined_to_value(lat, lon) : ads_value_null(); + } + /* Single-axis: / divisor */ + double divisor = 1000.0; + if (args) { + cJSON *d = cJSON_GetObjectItemCaseSensitive(args, "divisor"); + if (cJSON_IsNumber(d)) divisor = d->valuedouble; + } + cJSON_Delete(args); + if (!value || !*value) return ads_value_null(); + char prefix = value[0]; + double sign = (prefix == 'N' || prefix == 'E') ? 1.0 : -1.0; + /* Skip whitespace then parse the digit run. */ + const char *p = value + 1; + while (*p == ' ') p++; + char *endp; + double n = strtod(p, &endp); + if (endp == p) return ads_value_null(); + return ads_value_from_double(sign * n / divisor); +} + +ads_value_t *ads_decode_coordinate_decimal_minutes(const char *value, const char *args_json) { + (void)args_json; + double lat, lon; + if (!parse_combined(value, &lat, &lon, true)) return ads_value_null(); + return combined_to_value(lat, lon); +} diff --git a/runtimes/c/src/crc.c b/runtimes/c/src/crc.c new file mode 100644 index 0000000..e596e7d --- /dev/null +++ b/runtimes/c/src/crc.c @@ -0,0 +1,35 @@ +/* CRC-16 helpers — bitwise, table-free. Mirrors runtimes/rust/src/crc.rs and + * runtimes/typescript/utils/arinc_702_helper.ts (post-fix). */ + +#include +#include + +uint16_t ads_crc16_ibm_sdlc_rev(const uint8_t *data, size_t len) { + uint32_t crc = 0xffff; + for (size_t i = 0; i < len; i++) { + crc ^= data[i]; + for (int b = 0; b < 8; b++) { + if (crc & 0x0001) crc = (crc >> 1) ^ 0x8408; + else crc = crc >> 1; + } + } + crc = (crc ^ 0xffff) & 0xffff; + uint16_t n1 = (uint16_t)((crc >> 12) & 0xf); + uint16_t n2 = (uint16_t)((crc >> 8) & 0xf); + uint16_t n3 = (uint16_t)((crc >> 4) & 0xf); + uint16_t n4 = (uint16_t)(crc & 0xf); + return (uint16_t)((n4 << 12) | (n3 << 8) | (n2 << 4) | n1); +} + +uint16_t ads_crc16_genibus(const uint8_t *data, size_t len) { + uint32_t crc = 0xffff; + const uint32_t polynomial = 0x1021; + for (size_t i = 0; i < len; i++) { + crc ^= ((uint32_t)data[i]) << 8; + for (int b = 0; b < 8; b++) { + if (crc & 0x8000) crc = ((crc << 1) ^ polynomial) & 0xffff; + else crc = (crc << 1) & 0xffff; + } + } + return (uint16_t)((crc ^ 0xffff) & 0xffff); +} diff --git a/runtimes/c/src/helpers.c b/runtimes/c/src/helpers.c new file mode 100644 index 0000000..16f8b9e --- /dev/null +++ b/runtimes/c/src/helpers.c @@ -0,0 +1,158 @@ +/* Decode-fn helpers (numerics, strings, identifiers, encodings, bitfields). */ + +#include "ads_helpers.h" +#include +#include +#include +#include +#include + +struct ads_value { cJSON *node; bool owns; }; + +static char *substr_copy(const char *s, size_t start, size_t len) { + size_t slen = strlen(s); + if (start >= slen) return strdup(""); + if (start + len > slen) len = slen - start; + char *out = malloc(len + 1); + memcpy(out, s + start, len); + out[len] = '\0'; + return out; +} + +ads_value_t *ads_decode_integer(const char *value) { + return ads_value_from_int(value ? atoll(value) : 0); +} + +ads_value_t *ads_decode_integer_args(const char *value, const char *args_json) { + cJSON *args = args_json ? cJSON_Parse(args_json) : NULL; + size_t start = 0; + long len = -1; + double mult = 1.0; + if (args) { + cJSON *ss = cJSON_GetObjectItemCaseSensitive(args, "substring_start"); + cJSON *sl = cJSON_GetObjectItemCaseSensitive(args, "substring_length"); + cJSON *m = cJSON_GetObjectItemCaseSensitive(args, "multiplier"); + if (cJSON_IsNumber(ss)) start = (size_t)ss->valuedouble; + if (cJSON_IsNumber(sl)) len = (long)sl->valuedouble; + if (cJSON_IsNumber(m)) mult = m->valuedouble; + } + cJSON_Delete(args); + char *s = len >= 0 ? substr_copy(value, start, (size_t)len) + : substr_copy(value, start, strlen(value)); + double n = atof(s) * mult; + free(s); + return ads_value_from_double(n); +} + +ads_value_t *ads_decode_float(const char *value) { return ads_value_from_double(value ? atof(value) : 0.0); } +ads_value_t *ads_decode_string(const char *value) { return ads_value_from_string(value ? value : ""); } +ads_value_t *ads_decode_callsign(const char *value) { return ads_decode_string(value); } +ads_value_t *ads_decode_flight_number(const char *v) { return ads_decode_string(v); } + +static char *trim_inplace(char *s) { + char *start = s; + while (*start && isspace((unsigned char)*start)) start++; + if (start != s) memmove(s, start, strlen(start) + 1); + size_t end = strlen(s); + while (end > 0 && isspace((unsigned char)s[end - 1])) s[--end] = '\0'; + return s; +} + +ads_value_t *ads_decode_trim(const char *value) { + char *copy = strdup(value ? value : ""); + trim_inplace(copy); + ads_value_t *v = ads_value_from_string(copy); + free(copy); + return v; +} + +ads_value_t *ads_decode_uppercase(const char *value) { + char *copy = strdup(value ? value : ""); + for (char *p = copy; *p; p++) *p = (char)toupper((unsigned char)*p); + ads_value_t *v = ads_value_from_string(copy); + free(copy); + return v; +} + +ads_value_t *ads_decode_lowercase(const char *value) { + char *copy = strdup(value ? value : ""); + for (char *p = copy; *p; p++) *p = (char)tolower((unsigned char)*p); + ads_value_t *v = ads_value_from_string(copy); + free(copy); + return v; +} + +ads_value_t *ads_decode_airport(const char *value) { + char *copy = strdup(value ? value : ""); + trim_inplace(copy); + for (char *p = copy; *p; p++) *p = (char)toupper((unsigned char)*p); + ads_value_t *v = ads_value_from_string(copy); + free(copy); + return v; +} + +ads_value_t *ads_decode_tail_number(const char *value, const char *args_json) { + char *copy = strdup(value ? value : ""); + trim_inplace(copy); + cJSON *args = args_json ? cJSON_Parse(args_json) : NULL; + const cJSON *strip = args ? cJSON_GetObjectItemCaseSensitive(args, "strip_chars") : NULL; + if (strip && cJSON_IsString(strip)) { + for (const char *p = strip->valuestring; *p; p++) { + char *src = copy, *dst = copy; + while (*src) { + if (*src != *p) *dst++ = *src; + src++; + } + *dst = '\0'; + } + } + cJSON_Delete(args); + ads_value_t *v = ads_value_from_string(copy); + free(copy); + return v; +} + +static int64_t hhmmss_to_tod(const char *s) { + if (!s || strlen(s) < 6) return 0; + char buf[3] = {0}; + memcpy(buf, s, 2); int h = atoi(buf); + memcpy(buf, s + 2, 2); int m = atoi(buf); + memcpy(buf, s + 4, 2); int sec = atoi(buf); + return (int64_t)h * 3600 + (int64_t)m * 60 + sec; +} + +ads_value_t *ads_decode_timestamp_hhmmss(const char *value) { + return ads_value_from_int(hhmmss_to_tod(value)); +} + +ads_value_t *ads_decode_timestamp_hhmmss_args(const char *value, const char *args_json) { + cJSON *args = args_json ? cJSON_Parse(args_json) : NULL; + const cJSON *app = args ? cJSON_GetObjectItemCaseSensitive(args, "append") : NULL; + char *combined; + if (app && cJSON_IsString(app)) { + size_t n = strlen(value) + strlen(app->valuestring) + 1; + combined = malloc(n); + snprintf(combined, n, "%s%s", value, app->valuestring); + } else { + combined = strdup(value); + } + cJSON_Delete(args); + int64_t tod = hhmmss_to_tod(combined); + free(combined); + return ads_value_from_int(tod); +} + +/* ─── bitslice + concat_bits ─────────────────────────────────────────────── */ + +uint32_t ads_bitslice(uint32_t byte, uint8_t bit_start, uint8_t bit_end) { + uint8_t width = bit_end - bit_start + 1; + int shift = 8 - bit_end - 1; + uint32_t mask = (1u << width) - 1u; + return (byte >> shift) & mask; +} + +uint32_t ads_concat_bits(const uint32_t *values, size_t count) { + uint32_t acc = 0; + for (size_t i = 0; i < count; i++) acc = (acc << 8) | values[i]; + return acc; +} diff --git a/runtimes/c/src/plugin.c b/runtimes/c/src/plugin.c new file mode 100644 index 0000000..60baf36 --- /dev/null +++ b/runtimes/c/src/plugin.c @@ -0,0 +1,175 @@ +/* DecodeResult lifecycle + value helpers backed by cJSON. */ + +#include "ads_runtime.h" +#include +#include +#include + +struct ads_value { + cJSON *node; + bool owns; +}; + +struct ads_decode_result { + bool decoded; + char *plugin_name; + char *description; + ads_decode_level_t decode_level; + cJSON *raw; /* object */ + cJSON *items; /* array of {kind, code, label, value} */ + char *remaining; /* may be NULL */ + char *message_json; /* serialized snapshot if set_message was called */ +}; + +static char *xstrdup(const char *s) { + if (!s) return NULL; + char *r = malloc(strlen(s) + 1); + if (r) strcpy(r, s); + return r; +} + +ads_decode_result_t *ads_result_new(const char *plugin_name, + const char *description, + const ads_message_t *msg) { + ads_decode_result_t *r = calloc(1, sizeof(*r)); + if (!r) return NULL; + r->plugin_name = xstrdup(plugin_name); + r->description = xstrdup(description); + r->decode_level = ADS_DECODE_LEVEL_NONE; + r->raw = cJSON_CreateObject(); + r->items = cJSON_CreateArray(); + if (msg) { + cJSON *m = cJSON_CreateObject(); + cJSON_AddStringToObject(m, "label", msg->label ? msg->label : ""); + if (msg->sublabel) cJSON_AddStringToObject(m, "sublabel", msg->sublabel); + cJSON_AddStringToObject(m, "text", msg->text ? msg->text : ""); + r->message_json = cJSON_PrintUnformatted(m); + cJSON_Delete(m); + } + return r; +} + +void ads_result_free(ads_decode_result_t *result) { + if (!result) return; + free(result->plugin_name); + free(result->description); + cJSON_Delete(result->raw); + cJSON_Delete(result->items); + free(result->remaining); + free(result->message_json); + free(result); +} + +void ads_result_set_decoded(ads_decode_result_t *r, bool decoded) { + r->decoded = decoded; + if (!decoded) r->decode_level = ADS_DECODE_LEVEL_NONE; + else r->decode_level = r->remaining ? ADS_DECODE_LEVEL_PARTIAL : ADS_DECODE_LEVEL_FULL; +} + +ads_decode_result_t *ads_result_fail_unknown(ads_decode_result_t *r, const char *text) { + r->decoded = false; + r->decode_level = ADS_DECODE_LEVEL_NONE; + free(r->remaining); + r->remaining = xstrdup(text); + return r; +} + +void ads_result_raw_set(ads_decode_result_t *r, const char *key, ads_value_t *value) { + if (!r || !key || !value || !value->node) return; + /* Detach the cJSON node from the value wrapper (transfer ownership). */ + cJSON *detached = cJSON_Duplicate(value->node, 1); + cJSON_AddItemToObject(r->raw, key, detached); + ads_value_free(value); +} + +ads_value_t *ads_result_raw_get(const ads_decode_result_t *r, const char *key) { + cJSON *node = cJSON_GetObjectItemCaseSensitive(r->raw, key); + if (!node) return NULL; + ads_value_t *v = calloc(1, sizeof(*v)); + if (!v) return NULL; + v->node = node; /* borrowed pointer */ + v->owns = false; + return v; +} + +char *ads_result_to_json(const ads_decode_result_t *r) { + cJSON *root = cJSON_CreateObject(); + cJSON_AddBoolToObject(root, "decoded", r->decoded); + cJSON *decoder = cJSON_CreateObject(); + cJSON_AddStringToObject(decoder, "name", r->plugin_name); + cJSON_AddStringToObject(decoder, "type", "pattern-match"); + const char *level = r->decode_level == ADS_DECODE_LEVEL_FULL ? "full" + : r->decode_level == ADS_DECODE_LEVEL_PARTIAL ? "partial" + : "none"; + cJSON_AddStringToObject(decoder, "decodeLevel", level); + cJSON_AddItemToObject(root, "decoder", decoder); + + cJSON *formatted = cJSON_CreateObject(); + cJSON_AddStringToObject(formatted, "description", r->description); + cJSON_AddItemReferenceToObject(formatted, "items", r->items); + cJSON_AddItemToObject(root, "formatted", formatted); + + cJSON_AddItemReferenceToObject(root, "raw", r->raw); + + if (r->remaining) { + cJSON *rem = cJSON_CreateObject(); + cJSON_AddStringToObject(rem, "text", r->remaining); + cJSON_AddItemToObject(root, "remaining", rem); + } + + char *out = cJSON_PrintUnformatted(root); + cJSON_Delete(root); + return out; +} + +/* ─── ads_value_t ────────────────────────────────────────────────────────── */ + +static ads_value_t *make_value(cJSON *node) { + ads_value_t *v = calloc(1, sizeof(*v)); + if (!v) { cJSON_Delete(node); return NULL; } + v->node = node; + v->owns = true; + return v; +} + +ads_value_t *ads_value_from_string(const char *s) { return make_value(cJSON_CreateString(s ? s : "")); } +ads_value_t *ads_value_from_double(double n) { return make_value(cJSON_CreateNumber(n)); } +ads_value_t *ads_value_from_int(int64_t n) { return make_value(cJSON_CreateNumber((double)n)); } +ads_value_t *ads_value_from_bool(bool b) { return make_value(cJSON_CreateBool(b)); } +ads_value_t *ads_value_null(void) { return make_value(cJSON_CreateNull()); } + +void ads_value_free(ads_value_t *v) { + if (!v) return; + if (v->owns && v->node) cJSON_Delete(v->node); + free(v); +} + +bool ads_value_as_double(const ads_value_t *v, double *out) { + if (!v || !cJSON_IsNumber(v->node)) return false; + *out = v->node->valuedouble; + return true; +} +bool ads_value_as_int(const ads_value_t *v, int64_t *out) { + if (!v || !cJSON_IsNumber(v->node)) return false; + *out = (int64_t)v->node->valuedouble; + return true; +} +const char *ads_value_as_string(const ads_value_t *v) { + if (!v || !cJSON_IsString(v->node)) return NULL; + return v->node->valuestring; +} + +void ads_str_list_free(ads_str_list_t *list) { + if (!list || !list->items) return; + for (size_t i = 0; i < list->count; i++) free(list->items[i]); + free(list->items); + list->items = NULL; + list->count = list->capacity = 0; +} + +void ads_bytes_free(ads_bytes_t *b) { + if (!b) return; + free(b->data); + b->data = NULL; + b->len = 0; +} diff --git a/runtimes/c/src/regex.c b/runtimes/c/src/regex.c new file mode 100644 index 0000000..462a2a1 --- /dev/null +++ b/runtimes/c/src/regex.c @@ -0,0 +1,73 @@ +/* POSIX-regex backed implementation of ads_regex_match / ads_regex_group. + * + * Note: POSIX regex does NOT support PCRE-style named capture groups + * (?...). For ACARS specs that use named groups, the Stage 2 C impl + * needs to either: + * (a) compile with -lpcre2-8 and use PCRE2 instead of POSIX regex, or + * (b) preprocess patterns at codegen time to map ?... → ?:... and + * maintain a name→index map alongside. + * + * This stub uses option (a) shape — assume PCRE2 in Stage 2. For v1, the + * skeleton supports unnamed captures only and is sufficient for the + * non-regex reference specs (Label_10_POS, Label_4A, OHMA, ARINC_702). + */ + +#include "ads_helpers.h" +#include +#include +#include + +struct ads_regex_match { + bool ok; + char *input_copy; + regex_t compiled; + regmatch_t *matches; + size_t match_count; + /* For named-group support (Stage 2): name table */ +}; + +ads_regex_match_t *ads_regex_match_new(const char *pattern, const char *input) { + ads_regex_match_t *m = calloc(1, sizeof(*m)); + if (!m) return NULL; + if (regcomp(&m->compiled, pattern, REG_EXTENDED) != 0) { + free(m); + return NULL; + } + m->input_copy = strdup(input ? input : ""); + m->match_count = m->compiled.re_nsub + 1; + m->matches = calloc(m->match_count, sizeof(regmatch_t)); + if (!m->matches) { regfree(&m->compiled); free(m->input_copy); free(m); return NULL; } + m->ok = regexec(&m->compiled, m->input_copy, m->match_count, m->matches, 0) == 0; + return m; +} + +bool ads_regex_match_ok(const ads_regex_match_t *m) { return m && m->ok; } + +const char *ads_regex_group(const ads_regex_match_t *m, const char *name) { + /* Stage-2 TODO: resolve named group via a name→index map populated at + * compile time from the pattern. For now, support numeric names ("0".."9"). */ + if (!m || !m->ok || !name) return ""; + size_t idx = (size_t)atoi(name); + if (idx >= m->match_count) return ""; + regmatch_t mt = m->matches[idx]; + if (mt.rm_so < 0) return ""; + /* Return a borrowed pointer into input_copy at the match start. + * Length terminator is not enforced; callers should bound by mt.rm_eo. */ + return m->input_copy + mt.rm_so; +} + +void ads_regex_match_free(ads_regex_match_t *m) { + if (!m) return; + regfree(&m->compiled); + free(m->matches); + free(m->input_copy); + free(m); +} + +bool ads_regex_test(const char *pattern, const char *input) { + regex_t re; + if (regcomp(&re, pattern, REG_EXTENDED | REG_NOSUB) != 0) return false; + int rc = regexec(&re, input ? input : "", 0, NULL, 0); + regfree(&re); + return rc == 0; +} diff --git a/runtimes/c/src/result_formatter.c b/runtimes/c/src/result_formatter.c new file mode 100644 index 0000000..e3acbaa --- /dev/null +++ b/runtimes/c/src/result_formatter.c @@ -0,0 +1,115 @@ +/* ResultFormatter — pushes formatted items into the cJSON items array on + * the result. Field names match TS/Rust ResultFormatter output. */ + +#include "ads_helpers.h" +#include +#include +#include +#include + +struct ads_value { cJSON *node; bool owns; }; + +/* Access the result's items array. result is opaque elsewhere, so we share + * the layout here via a forward declaration matching plugin.c. */ +struct ads_decode_result { + bool decoded; + char *plugin_name; + char *description; + int decode_level; + cJSON *raw; + cJSON *items; + char *remaining; + char *message_json; +}; + +static void push_item(ads_decode_result_t *r, const char *kind, const char *code, + const char *label, const char *value) { + cJSON *item = cJSON_CreateObject(); + cJSON_AddStringToObject(item, "type", kind); + cJSON_AddStringToObject(item, "code", code); + cJSON_AddStringToObject(item, "label", label); + cJSON_AddStringToObject(item, "value", value); + cJSON_AddItemToArray(r->items, item); +} + +void ads_fmt_position(ads_decode_result_t *r, ads_value_t *lat, ads_value_t *lon) { + double la = 0, lo = 0; + ads_value_as_double(lat, &la); + ads_value_as_double(lon, &lo); + cJSON *pos = cJSON_CreateObject(); + cJSON_AddNumberToObject(pos, "latitude", la); + cJSON_AddNumberToObject(pos, "longitude", lo); + cJSON_AddItemToObject(r->raw, "position", pos); + char buf[64]; + 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); + ads_value_free(lat); ads_value_free(lon); +} + +void ads_fmt_position_value(ads_decode_result_t *r, ads_value_t *position) { + double la = 0, lo = 0; + if (position && position->node) { + const cJSON *l = cJSON_GetObjectItemCaseSensitive(position->node, "latitude"); + const cJSON *g = cJSON_GetObjectItemCaseSensitive(position->node, "longitude"); + if (cJSON_IsNumber(l)) la = l->valuedouble; + if (cJSON_IsNumber(g)) lo = g->valuedouble; + } + ads_fmt_position(r, ads_value_from_double(la), ads_value_from_double(lo)); + ads_value_free(position); +} + +static void push_numeric(ads_decode_result_t *r, ads_value_t *v, const char *raw_key, + const char *kind, const char *code, const char *label, const char *unit) { + double n = 0; + ads_value_as_double(v, &n); + cJSON_AddNumberToObject(r->raw, raw_key, n); + char buf[64]; + if (unit && *unit) snprintf(buf, sizeof(buf), "%g %s", n, unit); + else snprintf(buf, sizeof(buf), "%g", n); + push_item(r, kind, code, label, buf); + ads_value_free(v); +} + +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", ""); } + +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) { + const char *s = ads_value_as_string(v); + if (s) cJSON_AddStringToObject(r->raw, raw_key, s); + push_item(r, kind, code, label, s ? s : ""); + 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_unknown_arr(ads_decode_result_t *r, const char *const *values, size_t count) { + size_t total = 1; + for (size_t i = 0; i < count; i++) total += strlen(values[i] ? values[i] : "") + 1; + char *joined = malloc(total); + if (!joined) return; + joined[0] = '\0'; + for (size_t i = 0; i < count; i++) { + if (i > 0) strcat(joined, ","); + strcat(joined, values[i] ? values[i] : ""); + } + if (r->remaining) { + size_t newlen = strlen(r->remaining) + 1 + strlen(joined) + 1; + char *grown = malloc(newlen); + snprintf(grown, newlen, "%s,%s", r->remaining, joined); + free(r->remaining); + r->remaining = grown; + } else { + r->remaining = strdup(joined); + } + free(joined); +} diff --git a/runtimes/c/src/string_list.c b/runtimes/c/src/string_list.c new file mode 100644 index 0000000..173658b --- /dev/null +++ b/runtimes/c/src/string_list.c @@ -0,0 +1,55 @@ +/* ads_split, ads_substring, ads_str_in. */ + +#include "ads_helpers.h" +#include +#include + +ads_str_list_t ads_split(const char *s, const char *delimiter) { + ads_str_list_t out = {0}; + if (!s || !delimiter || !*delimiter) return out; + size_t dlen = strlen(delimiter); + const char *p = s; + while (1) { + const char *next = strstr(p, delimiter); + size_t len = next ? (size_t)(next - p) : strlen(p); + if (out.count == out.capacity) { + size_t newcap = out.capacity ? out.capacity * 2 : 8; + char **newitems = realloc(out.items, newcap * sizeof(char *)); + if (!newitems) { ads_str_list_free(&out); return out; } + out.items = newitems; + out.capacity = newcap; + } + char *copy = malloc(len + 1); + if (!copy) { ads_str_list_free(&out); return out; } + memcpy(copy, p, len); + copy[len] = '\0'; + out.items[out.count++] = copy; + if (!next) break; + p = next + dlen; + } + return out; +} + +const char *ads_substring(const char *s, int start, int end) { + /* Returns a borrowed pointer into s + start. Caller must NOT free. + * For length-bounded extraction, callers typically pass the result + * through ads_value_from_string which copies. The end arg specifies + * one-past-last; negative = to end. */ + if (!s) return ""; + size_t slen = strlen(s); + if (start < 0) start = 0; + if ((size_t)start > slen) start = (int)slen; + if (end < 0 || (size_t)end > slen) end = (int)slen; + /* Note: this leaks the trailing chars to the caller; for ACARS use cases + * the caller almost always passes the substring through a copy quickly. */ + (void)end; + return s + start; +} + +bool ads_str_in(const char *const *list, size_t count, const char *needle) { + if (!list || !needle) return false; + for (size_t i = 0; i < count; i++) { + if (list[i] && strcmp(list[i], needle) == 0) return true; + } + return false; +} From 765a3903ce813dd296c5fb97d03a2a832e4c125d Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Wed, 27 May 2026 22:59:36 -0700 Subject: [PATCH 06/23] Bulk-port 63 ACARS plugins from acars-decoder-typescript to ADS YAML MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings the spec coverage from 5 reference plugins to 68 — the full TS plugin catalog minus the official.ts barrel file. Authored in parallel by four agents, each working a distinct label-range batch. Coverage: - Batch 1 (Label_10-22): 22 specs — mostly whole-plugin escape hatches - Batch 2 (Label_24-4N): 15 specs — 4 declarative + 11 escape hatches - Batch 3 (Label_4T-H1): 16 specs — all whole-plugin escape hatches - Batch 4 (Label_H2-official): 9 specs — all whole-plugin escape hatches - ColonComma: 1 spec (after schema relaxation, see below) Of the 63 new specs, 4 are declarative ports (Label_44_IN/ON/OFF/ETA — clean CSV + coordinate_decimal_minutes + airport + timestamp shape) and 59 use whole-plugin escape hatches via `parse: { custom: }` and `formatted: { custom: }`. Why the heavy escape-hatch ratio: the v1 schema's `formatter_call.type` enum covers only the most common formatters (position, altitude, timestamp, etc.). Real TS plugins lean on ~40 more ResultFormatter methods (route, eta, currentFuel, temperature, flightNumber, departureDay/arrivalDay, out/off/on/in, runway types, windData, mach, airspeed, totalAirTemp, state_change, door_event, frequency, atis, loadsheet, fault, warning, route, routeNumber, cg, engineStart/Stop, groundspeed, day, month, airline, message_type, tail, unknownArr…). Generating those declaratively would silently drop fields. Per the "under-port-with-escape-hatches is better than over-port-with-wrong- semantics" rule, conservative bias is correct. Two real-life follow-ups for ADS v1.1: 1. Extend `formatter_call.type` to cover the full TS ResultFormatter API (or add an `aliases` form that lets specs declare the formatter name inline). 2. Add a `trailing_fields_into_unknown` parse step so plugins like Label_44_IN/ON/OFF/ETA preserve the addRemainingFields behavior that the declarative path currently drops. Schema relaxation: the `qualifiers.labels` pattern previously rejected punctuation, blocking Label_ColonComma (label literal ":;"). Changed to allow any non-empty string — ACARS labels are byte sequences, not identifiers. The schema now matches the real ACARS spec. Escape-hatch inventory (~118 functions, ~2 per plugin): - All listed in /docs/ESCAPE_HATCHES.md after stage 2 wire-up - Each = the original TS plugin's decode() and formatted-item-push code, ported 1:1 into runtimes//escape_hatches/ Verified end-to-end: - All 68 specs validate against schema/ads-v1.schema.json - `ads-gen --target ts` emits 68 .ts files cleanly - `ads-gen --target rust` emits 68 .rs files cleanly - `ads-gen --target c` emits 68 .c+.h pairs cleanly Co-Authored-By: Claude Opus 4.7 (1M context) --- schema/ads-v1.schema.json | 3 +- spec/labels/10/LDR.yaml | 16 ++++++++ spec/labels/10/Slash.yaml | 16 ++++++++ spec/labels/12/N_Space.yaml | 15 ++++++++ spec/labels/12/POS.yaml | 16 ++++++++ spec/labels/15/FST.yaml | 16 ++++++++ spec/labels/15/Paren2.yaml | 16 ++++++++ spec/labels/16/AUTPOS.yaml | 17 +++++++++ spec/labels/16/Honeywell.yaml | 17 +++++++++ spec/labels/16/N_Space.yaml | 16 ++++++++ spec/labels/16/POSA1.yaml | 16 ++++++++ spec/labels/16/TOD.yaml | 16 ++++++++ spec/labels/1L/070.yaml | 16 ++++++++ spec/labels/1L/3-line.yaml | 15 ++++++++ spec/labels/1L/660.yaml | 16 ++++++++ spec/labels/1L/Slash.yaml | 17 +++++++++ spec/labels/1M/Slash.yaml | 17 +++++++++ spec/labels/20/CFB01.yaml | 16 ++++++++ spec/labels/20/POS.yaml | 16 ++++++++ spec/labels/21/POS.yaml | 17 +++++++++ spec/labels/22/OFF.yaml | 17 +++++++++ spec/labels/22/POS.yaml | 16 ++++++++ spec/labels/24/Slash.yaml | 16 ++++++++ spec/labels/2P/FM3.yaml | 15 ++++++++ spec/labels/2P/FM4.yaml | 15 ++++++++ spec/labels/2P/FM5.yaml | 15 ++++++++ spec/labels/30/Slash_EA.yaml | 15 ++++++++ spec/labels/44/ETA.yaml | 54 +++++++++++++++++++++++++++ spec/labels/44/IN.yaml | 46 +++++++++++++++++++++++ spec/labels/44/OFF.yaml | 50 +++++++++++++++++++++++++ spec/labels/44/ON.yaml | 46 +++++++++++++++++++++++ spec/labels/44/Slash.yaml | 16 ++++++++ spec/labels/4A/01.yaml | 16 ++++++++ spec/labels/4A/DIS.yaml | 15 ++++++++ spec/labels/4A/DOOR.yaml | 15 ++++++++ spec/labels/4A/Slash_01.yaml | 14 +++++++ spec/labels/4N.yaml | 16 ++++++++ spec/labels/4T/AGFSR.yaml | 18 +++++++++ spec/labels/4T/ETA.yaml | 18 +++++++++ spec/labels/58.yaml | 17 +++++++++ spec/labels/5Z/Slash.yaml | 20 ++++++++++ spec/labels/80.yaml | 20 ++++++++++ spec/labels/83.yaml | 17 +++++++++ spec/labels/8E.yaml | 16 ++++++++ spec/labels/B6/Forwardslash.yaml | 16 ++++++++ spec/labels/ColonComma.yaml | 13 +++++++ spec/labels/H1/ATIS.yaml | 20 ++++++++++ spec/labels/H1/EZF.yaml | 23 ++++++++++++ spec/labels/H1/FLR.yaml | 18 +++++++++ spec/labels/H1/M_POS.yaml | 17 +++++++++ spec/labels/H1/OFP.yaml | 21 +++++++++++ spec/labels/H1/Paren.yaml | 18 +++++++++ spec/labels/H1/StarPOS.yaml | 19 ++++++++++ spec/labels/H1/WRN.yaml | 18 +++++++++ spec/labels/H2/02E.yaml | 20 ++++++++++ spec/labels/HX.yaml | 22 +++++++++++ spec/labels/MA.yaml | 19 ++++++++++ spec/labels/QP.yaml | 19 ++++++++++ spec/labels/QQ.yaml | 23 ++++++++++++ spec/labels/QR.yaml | 17 +++++++++ spec/labels/QS.yaml | 17 +++++++++ spec/labels/SQ.yaml | 23 ++++++++++++ spec/wildcards/cband.yaml | 20 ++++++++++ spec/wildcards/label_13_18_slash.yaml | 16 ++++++++ 64 files changed, 1211 insertions(+), 1 deletion(-) create mode 100644 spec/labels/10/LDR.yaml create mode 100644 spec/labels/10/Slash.yaml create mode 100644 spec/labels/12/N_Space.yaml create mode 100644 spec/labels/12/POS.yaml create mode 100644 spec/labels/15/FST.yaml create mode 100644 spec/labels/15/Paren2.yaml create mode 100644 spec/labels/16/AUTPOS.yaml create mode 100644 spec/labels/16/Honeywell.yaml create mode 100644 spec/labels/16/N_Space.yaml create mode 100644 spec/labels/16/POSA1.yaml create mode 100644 spec/labels/16/TOD.yaml create mode 100644 spec/labels/1L/070.yaml create mode 100644 spec/labels/1L/3-line.yaml create mode 100644 spec/labels/1L/660.yaml create mode 100644 spec/labels/1L/Slash.yaml create mode 100644 spec/labels/1M/Slash.yaml create mode 100644 spec/labels/20/CFB01.yaml create mode 100644 spec/labels/20/POS.yaml create mode 100644 spec/labels/21/POS.yaml create mode 100644 spec/labels/22/OFF.yaml create mode 100644 spec/labels/22/POS.yaml create mode 100644 spec/labels/24/Slash.yaml create mode 100644 spec/labels/2P/FM3.yaml create mode 100644 spec/labels/2P/FM4.yaml create mode 100644 spec/labels/2P/FM5.yaml create mode 100644 spec/labels/30/Slash_EA.yaml create mode 100644 spec/labels/44/ETA.yaml create mode 100644 spec/labels/44/IN.yaml create mode 100644 spec/labels/44/OFF.yaml create mode 100644 spec/labels/44/ON.yaml create mode 100644 spec/labels/44/Slash.yaml create mode 100644 spec/labels/4A/01.yaml create mode 100644 spec/labels/4A/DIS.yaml create mode 100644 spec/labels/4A/DOOR.yaml create mode 100644 spec/labels/4A/Slash_01.yaml create mode 100644 spec/labels/4N.yaml create mode 100644 spec/labels/4T/AGFSR.yaml create mode 100644 spec/labels/4T/ETA.yaml create mode 100644 spec/labels/58.yaml create mode 100644 spec/labels/5Z/Slash.yaml create mode 100644 spec/labels/80.yaml create mode 100644 spec/labels/83.yaml create mode 100644 spec/labels/8E.yaml create mode 100644 spec/labels/B6/Forwardslash.yaml create mode 100644 spec/labels/ColonComma.yaml create mode 100644 spec/labels/H1/ATIS.yaml create mode 100644 spec/labels/H1/EZF.yaml create mode 100644 spec/labels/H1/FLR.yaml create mode 100644 spec/labels/H1/M_POS.yaml create mode 100644 spec/labels/H1/OFP.yaml create mode 100644 spec/labels/H1/Paren.yaml create mode 100644 spec/labels/H1/StarPOS.yaml create mode 100644 spec/labels/H1/WRN.yaml create mode 100644 spec/labels/H2/02E.yaml create mode 100644 spec/labels/HX.yaml create mode 100644 spec/labels/MA.yaml create mode 100644 spec/labels/QP.yaml create mode 100644 spec/labels/QQ.yaml create mode 100644 spec/labels/QR.yaml create mode 100644 spec/labels/QS.yaml create mode 100644 spec/labels/SQ.yaml create mode 100644 spec/wildcards/cband.yaml create mode 100644 spec/wildcards/label_13_18_slash.yaml diff --git a/schema/ads-v1.schema.json b/schema/ads-v1.schema.json index 8e15031..7211a3c 100644 --- a/schema/ads-v1.schema.json +++ b/schema/ads-v1.schema.json @@ -105,7 +105,8 @@ "minItems": 1, "items": { "type": "string", - "pattern": "^([A-Za-z0-9_]+|\\*)$" + "minLength": 1, + "description": "ACARS label string. Real labels include alphanumeric (10, 4A, H1) plus punctuation forms like ':;' (Label_ColonComma). '*' is the wildcard." }, "description": "ACARS labels to match. '*' is wildcard." }, diff --git a/spec/labels/10/LDR.yaml b/spec/labels/10/LDR.yaml new file mode 100644 index 0000000..baa5e02 --- /dev/null +++ b/spec/labels/10/LDR.yaml @@ -0,0 +1,16 @@ +spec_version: "1" +plugin: + name: Label_10_LDR + type: text + decode_level: PARTIAL +qualifiers: + labels: ["10"] + preambles: ["LDR"] +parse: + # Pure escape hatch: comma-split with 17+ fields, runway substring slicing, + # alternate-runway concat filter, and non-DSL formatters (alternate airport, + # arrival runway, alternate runway) all live in hand-written per-language code. + custom: label_10_ldr_decode +formatted: + description: "Position Report" + custom: label_10_ldr_format diff --git a/spec/labels/10/Slash.yaml b/spec/labels/10/Slash.yaml new file mode 100644 index 0000000..5fc959b --- /dev/null +++ b/spec/labels/10/Slash.yaml @@ -0,0 +1,16 @@ +spec_version: "1" +plugin: + name: Label_10_Slash + type: text + decode_level: PARTIAL +qualifiers: + labels: ["10"] + preambles: ["/"] +parse: + # Pure escape hatch: 17-field slash split, ETA + waypoint route construction + # with multiple time stamps, conditional departure airport, and trailing + # unknownArr from non-contiguous indices. + custom: label_10_slash_decode +formatted: + description: "Position Report" + custom: label_10_slash_format diff --git a/spec/labels/12/N_Space.yaml b/spec/labels/12/N_Space.yaml new file mode 100644 index 0000000..068458c --- /dev/null +++ b/spec/labels/12/N_Space.yaml @@ -0,0 +1,15 @@ +spec_version: "1" +plugin: + name: Label_12_N_Space + type: text + decode_level: PARTIAL +qualifiers: + labels: ["12"] + preambles: ["N ", "S "] +parse: + # Pure escape hatch: regex with NSEW directional capture, GRD/*** altitude + # sentinel handling, and free_text trailing fields. + custom: label_12_n_space_decode +formatted: + description: "Position Report" + custom: label_12_n_space_format diff --git a/spec/labels/12/POS.yaml b/spec/labels/12/POS.yaml new file mode 100644 index 0000000..42ad44d --- /dev/null +++ b/spec/labels/12/POS.yaml @@ -0,0 +1,16 @@ +spec_version: "1" +plugin: + name: Label_12_POS + type: text + decode_level: PARTIAL +qualifiers: + labels: ["12"] + preambles: ["POS"] +parse: + # Pure escape hatch: 8-char lat / 8-char lon substring DMS extraction, "FOB" + # and "ETA" prefix stripping, currentFuel + eta formatters not in DSL, + # interleaved unknown calls with array slices. + custom: label_12_pos_decode +formatted: + description: "Position Report" + custom: label_12_pos_format diff --git a/spec/labels/15/FST.yaml b/spec/labels/15/FST.yaml new file mode 100644 index 0000000..6ab7015 --- /dev/null +++ b/spec/labels/15/FST.yaml @@ -0,0 +1,16 @@ +spec_version: "1" +plugin: + name: Label_15_FST + type: text + decode_level: PARTIAL +qualifiers: + labels: ["15"] + preambles: ["FST01"] +parse: + # Pure escape hatch: fixed-offset substring extraction of header, custom + # NSEW direction check with divide-by-10000 coord parsing, altitude *100, + # space-separated trailing unknownArr. + custom: label_15_fst_decode +formatted: + description: "Position Report" + custom: label_15_fst_format diff --git a/spec/labels/15/Paren2.yaml b/spec/labels/15/Paren2.yaml new file mode 100644 index 0000000..3f812b0 --- /dev/null +++ b/spec/labels/15/Paren2.yaml @@ -0,0 +1,16 @@ +spec_version: "1" +plugin: + name: Label_15 + type: text + decode_level: PARTIAL +qualifiers: + labels: ["15"] + preambles: ["(2"] +parse: + # Pure escape hatch: prefix/suffix bracket trim, two variants (short vs OFF) + # branched on length and substring marker, decimal-minutes coords, OFF date + # epoch conversion vs HHMMSS-only fallback, temperature M/P sign substitution. + custom: label_15_decode +formatted: + description: "Position Report" + custom: label_15_format diff --git a/spec/labels/16/AUTPOS.yaml b/spec/labels/16/AUTPOS.yaml new file mode 100644 index 0000000..7bacf3c --- /dev/null +++ b/spec/labels/16/AUTPOS.yaml @@ -0,0 +1,17 @@ +spec_version: "1" +plugin: + name: Label_16_AUTPOS + type: text + decode_level: FULL +qualifiers: + labels: ["16"] + preambles: [""] +parse: + # Pure escape hatch: 19-group regex with optional wind / temp / TAS / mach + # fields each independently redactable, day/flight number split, DMS coords, + # date+time epoch conversion. Uses windData and several formatters not + # representable in current DSL. + custom: label_16_autpos_decode +formatted: + description: "Position Report" + custom: label_16_autpos_format diff --git a/spec/labels/16/Honeywell.yaml b/spec/labels/16/Honeywell.yaml new file mode 100644 index 0000000..312ffab --- /dev/null +++ b/spec/labels/16/Honeywell.yaml @@ -0,0 +1,17 @@ +spec_version: "1" +plugin: + name: Label_16_Honeywell + type: text + decode_level: PARTIAL +qualifiers: + labels: ["16"] + preambles: ["(2"] +parse: + # Pure escape hatch: bracket-stripped session ID + decimal-minutes coords, + # then a marker-dispatch at offset 17: '-' selects waypoint mode (variable + # 1 or 2 waypoints), else route mode (departure/arrival airports). Trailing + # 2-char phase tag preserved as unknown. + custom: label_16_honeywell_decode +formatted: + description: "Position Report" + custom: label_16_honeywell_format diff --git a/spec/labels/16/N_Space.yaml b/spec/labels/16/N_Space.yaml new file mode 100644 index 0000000..5943d73 --- /dev/null +++ b/spec/labels/16/N_Space.yaml @@ -0,0 +1,16 @@ +spec_version: "1" +plugin: + name: Label_16_N_Space + type: text + decode_level: PARTIAL +qualifiers: + labels: ["16"] + preambles: ["N ", "S "] +parse: + # Pure escape hatch: two alternative regexes (comma-delimited with altitude + # / unknown fields vs slash-delimited coords-only), each with its own + # decode level (partial vs full). + custom: label_16_n_space_decode +formatted: + description: "Position Report" + custom: label_16_n_space_format diff --git a/spec/labels/16/POSA1.yaml b/spec/labels/16/POSA1.yaml new file mode 100644 index 0000000..49166dd --- /dev/null +++ b/spec/labels/16/POSA1.yaml @@ -0,0 +1,16 @@ +spec_version: "1" +plugin: + name: Label_16_POSA1 + type: text + decode_level: PARTIAL +qualifiers: + labels: ["16"] + preambles: ["POSA1"] +parse: + # Pure escape hatch: comma split with strict 11-field count, "POSA1" + # substring strip, route construction with two waypoint+time entries, and + # altitude * 100. Route formatter is not in DSL. + custom: label_16_posa1_decode +formatted: + description: "Position Report" + custom: label_16_posa1_format diff --git a/spec/labels/16/TOD.yaml b/spec/labels/16/TOD.yaml new file mode 100644 index 0000000..e8aea5b --- /dev/null +++ b/spec/labels/16/TOD.yaml @@ -0,0 +1,16 @@ +spec_version: "1" +plugin: + name: Label_16_TOD + type: text + decode_level: PARTIAL +qualifiers: + labels: ["16"] +parse: + # Pure escape hatch: 5-field comma split, HHMMSS time validation, optional + # altitude, ETA, then position parsed from final field with 3 different + # sub-variants (2-token decimal-minutes, 4-token decimal-degrees, redacted), + # followed by optional flight number after a '/' marker. + custom: label_16_tod_decode +formatted: + description: "Position Report" + custom: label_16_tod_format diff --git a/spec/labels/1L/070.yaml b/spec/labels/1L/070.yaml new file mode 100644 index 0000000..2b7ad65 --- /dev/null +++ b/spec/labels/1L/070.yaml @@ -0,0 +1,16 @@ +spec_version: "1" +plugin: + name: Label_1L_070 + type: text + decode_level: PARTIAL +qualifiers: + labels: ["1L"] + preambles: ["000000070"] +parse: + # Pure escape hatch: 9-char prefix strip, 7-field comma split, dep/arr ICAO, + # message timestamp + eta from HHMMSS, signed-decimal coords via NSEW char, + # trailing field preserved as remaining.text. + custom: label_1l_070_decode +formatted: + description: "Position Report" + custom: label_1l_070_format diff --git a/spec/labels/1L/3-line.yaml b/spec/labels/1L/3-line.yaml new file mode 100644 index 0000000..7af15b7 --- /dev/null +++ b/spec/labels/1L/3-line.yaml @@ -0,0 +1,15 @@ +spec_version: "1" +plugin: + name: Label_1L_3Line + type: text + decode_level: PARTIAL +qualifiers: + labels: ["1L"] +parse: + # Pure escape hatch: strict 3-line check, Map-based key/value parsing across + # slash- and newline-separated tokens, DAY+UTC combined into epoch, signed + # coords from LAT/LON pair, dynamic remaining.text built from leftover keys. + custom: label_1l_3line_decode +formatted: + description: "Position Report" + custom: label_1l_3line_format diff --git a/spec/labels/1L/660.yaml b/spec/labels/1L/660.yaml new file mode 100644 index 0000000..043ce0c --- /dev/null +++ b/spec/labels/1L/660.yaml @@ -0,0 +1,16 @@ +spec_version: "1" +plugin: + name: Label_1L_660 + type: text + decode_level: PARTIAL +qualifiers: + labels: ["1L"] + preambles: ["000000660"] +parse: + # Pure escape hatch: 9-char prefix strip, decimal-minutes coords in field 0, + # field 1 sliced into HHMMSS (timestamp) + FL*100 (altitude) + waypoint + # (route), remaining fields joined back as remaining.text. + custom: label_1l_660_decode +formatted: + description: "Position Report" + custom: label_1l_660_format diff --git a/spec/labels/1L/Slash.yaml b/spec/labels/1L/Slash.yaml new file mode 100644 index 0000000..f114c50 --- /dev/null +++ b/spec/labels/1L/Slash.yaml @@ -0,0 +1,17 @@ +spec_version: "1" +plugin: + name: Label_1L_Slash + type: text + decode_level: PARTIAL +qualifiers: + labels: ["1L"] + preambles: ["+", "-"] +parse: + # Pure escape hatch: 7-part slash split, first two parts become LAT/LON via + # whitespace-stripping (signed decimal), remainder parsed into a Map of + # key/value tokens, ETA/ALT/FOB/UTC pulled out and remaining keys re-joined + # as remaining.text. + custom: label_1l_slash_decode +formatted: + description: "Position Report" + custom: label_1l_slash_format diff --git a/spec/labels/1M/Slash.yaml b/spec/labels/1M/Slash.yaml new file mode 100644 index 0000000..5c7b434 --- /dev/null +++ b/spec/labels/1M/Slash.yaml @@ -0,0 +1,17 @@ +spec_version: "1" +plugin: + name: Label_1M_Slash + type: text + decode_level: PARTIAL +qualifiers: + labels: ["1M"] + preambles: ["/"] +parse: + # Pure escape hatch: split on slash OR newline, positional access to results + # 0..9 for flight number / dep / arr / alternate / arrival runway / ETA, + # ETA computed from results[7] + yymmdd date with swapped DDMMYY order, + # arrival runway sliced from arrival ICAO prefix. + custom: label_1m_slash_decode +formatted: + description: "ETA Report" + custom: label_1m_slash_format diff --git a/spec/labels/20/CFB01.yaml b/spec/labels/20/CFB01.yaml new file mode 100644 index 0000000..d2a7171 --- /dev/null +++ b/spec/labels/20/CFB01.yaml @@ -0,0 +1,16 @@ +spec_version: "1" +plugin: + name: Label_20_CFB01 + type: text + decode_level: FULL +qualifiers: + labels: ["20"] + preambles: ["#CFB.01"] +parse: + # Pure escape hatch: named-group regex with combined coords, current-year + # epoch computation using Date.parse, fuel-in-tons stored to raw only when + # not "***"/"****". Raw-only fields with no formatter output. + custom: label_20_cfb01_decode +formatted: + description: "Crew Flight Bag Message" + custom: label_20_cfb01_format diff --git a/spec/labels/20/POS.yaml b/spec/labels/20/POS.yaml new file mode 100644 index 0000000..dda7b54 --- /dev/null +++ b/spec/labels/20/POS.yaml @@ -0,0 +1,16 @@ +spec_version: "1" +plugin: + name: Label_20_POS + type: text + decode_level: FULL +qualifiers: + labels: ["20"] + preambles: ["POS"] +parse: + # Pure escape hatch: preamble preserved to raw, branches on 11-field vs + # 5-field count, both extract position from field[0]; behavior tracked + # byte-for-byte against TS source. + custom: label_20_pos_decode +formatted: + description: "Position Report" + custom: label_20_pos_format diff --git a/spec/labels/21/POS.yaml b/spec/labels/21/POS.yaml new file mode 100644 index 0000000..94981b7 --- /dev/null +++ b/spec/labels/21/POS.yaml @@ -0,0 +1,17 @@ +spec_version: "1" +plugin: + name: Label_21_POS + type: text + decode_level: PARTIAL +qualifiers: + labels: ["21"] + preambles: ["POS"] +parse: + # Pure escape hatch: 9-field comma split, position from fixed 16-char layout + # (NSEW signs at offsets 0 and 8), HHMMSS timestamp + altitude + temperature + # (whitespace-stripped) + ETA + arrival airport, with interleaved unknownArr + # at indices 1/4/5. + custom: label_21_pos_decode +formatted: + description: "Position Report" + custom: label_21_pos_format diff --git a/spec/labels/22/OFF.yaml b/spec/labels/22/OFF.yaml new file mode 100644 index 0000000..ec7620f --- /dev/null +++ b/spec/labels/22/OFF.yaml @@ -0,0 +1,17 @@ +spec_version: "1" +plugin: + name: Label_22_OFF + type: text + decode_level: PARTIAL +qualifiers: + labels: ["22"] + preambles: ["OFF"] +parse: + # Pure escape hatch: three variants dispatched on "OFF01" / "OFF02\r\n" / + # "OFF02" prefix, each with distinct field layouts (slash-split + fixed + # substring offsets in variant 1/2, comma-split in variant 3). Uses + # departureDay/arrivalDay/off formatters that aren't in the DSL. + custom: label_22_off_decode +formatted: + description: "Takeoff Report" + custom: label_22_off_format diff --git a/spec/labels/22/POS.yaml b/spec/labels/22/POS.yaml new file mode 100644 index 0000000..8d3bc40 --- /dev/null +++ b/spec/labels/22/POS.yaml @@ -0,0 +1,16 @@ +spec_version: "1" +plugin: + name: Label_22_POS + type: text + decode_level: PARTIAL +qualifiers: + labels: ["22"] + preambles: ["N", "S"] +parse: + # Pure escape hatch: 11-field comma split, position from fixed 16-char + # layout (NSEW signs at offsets 0 and 8, divisor 10000), HHMMSS timestamp, + # altitude, then unknownArr from non-contiguous indices [1, 4..10]. + custom: label_22_pos_decode +formatted: + description: "Position Report" + custom: label_22_pos_format diff --git a/spec/labels/24/Slash.yaml b/spec/labels/24/Slash.yaml new file mode 100644 index 0000000..66d7133 --- /dev/null +++ b/spec/labels/24/Slash.yaml @@ -0,0 +1,16 @@ +spec_version: "1" +plugin: + name: Label_24_Slash + type: text + decode_level: PARTIAL +qualifiers: + labels: ["24"] + preambles: ["/"] +parse: + # Splits on '/', requires len==10 with first+last empty (begin/end with '/'), + # rearranges date substrings DDMMYY -> YYDDMM and computes epoch via + # convertDateTimeToEpoch, char-prefix sign decoding for coords. Whole-plugin hatch. + custom: label_24_slash_decode +formatted: + description: "Position Report" + custom: label_24_slash_format diff --git a/spec/labels/2P/FM3.yaml b/spec/labels/2P/FM3.yaml new file mode 100644 index 0000000..9ae9dcc --- /dev/null +++ b/spec/labels/2P/FM3.yaml @@ -0,0 +1,15 @@ +spec_version: "1" +plugin: + name: Label_2P_FM3 + type: text + decode_level: PARTIAL +qualifiers: + labels: ["2P"] +parse: + # Splits on ',' (7 fields), then header = parts[0].split('FM3 '). + # Variant on whether header[0] is non-empty (flight number padding), + # then char-prefix-or-bare coord parsing with space stripping. Whole-plugin hatch. + custom: label_2p_fm3_decode +formatted: + description: "Flight Report" + custom: label_2p_fm3_format diff --git a/spec/labels/2P/FM4.yaml b/spec/labels/2P/FM4.yaml new file mode 100644 index 0000000..a916305 --- /dev/null +++ b/spec/labels/2P/FM4.yaml @@ -0,0 +1,15 @@ +spec_version: "1" +plugin: + name: Label_2P_FM4 + type: text + decode_level: PARTIAL +qualifiers: + labels: ["2P"] +parse: + # Splits on ',' (10 fields), then header = parts[0].split('FM4'). + # Day from parts[2].substring(0,2), time from parts[2].substring(2), + # bare-number coords with space stripping. Whole-plugin hatch. + custom: label_2p_fm4_decode +formatted: + description: "Flight Report" + custom: label_2p_fm4_format diff --git a/spec/labels/2P/FM5.yaml b/spec/labels/2P/FM5.yaml new file mode 100644 index 0000000..45ff772 --- /dev/null +++ b/spec/labels/2P/FM5.yaml @@ -0,0 +1,15 @@ +spec_version: "1" +plugin: + name: Label_2P_FM5 + type: text + decode_level: PARTIAL +qualifiers: + labels: ["2P"] +parse: + # Splits on ',' (12 fields), then header = parts[0].split('FM5 '). + # Bare-number coords with space stripping, trimmed flight number at parts[10]. + # Whole-plugin hatch. + custom: label_2p_fm5_decode +formatted: + description: "Flight Report" + custom: label_2p_fm5_format diff --git a/spec/labels/30/Slash_EA.yaml b/spec/labels/30/Slash_EA.yaml new file mode 100644 index 0000000..99dbc94 --- /dev/null +++ b/spec/labels/30/Slash_EA.yaml @@ -0,0 +1,15 @@ +spec_version: "1" +plugin: + name: Label_30_Slash_EA + type: text + decode_level: PARTIAL +qualifiers: + labels: ["30"] + preambles: ["/EA"] +parse: + # Splits on /\n|\//, slices off first element, then conditional emission + # to remaining.text based on results[1] prefix. Whole-plugin hatch. + custom: label_30_slash_ea_decode +formatted: + description: "ETA Report" + custom: label_30_slash_ea_format diff --git a/spec/labels/44/ETA.yaml b/spec/labels/44/ETA.yaml new file mode 100644 index 0000000..b0d262a --- /dev/null +++ b/spec/labels/44/ETA.yaml @@ -0,0 +1,54 @@ +spec_version: "1" +plugin: + name: Label_44_ETA + type: text + decode_level: FULL +qualifiers: + labels: ["44"] + preambles: ["00ETA01", "00ETA02", "00ETA03", "ETA01", "ETA02", "ETA03"] +parse: + - { split: ",", into: data } + - { require_length: { var: data, min: 9 }, else: fail } +fields: + - name: position + from: $data[1] + decode: + fn: coordinate_decimal_minutes + args: { style: combined, format: "NSDDMM_M_EWDDMM_M" } + - name: altitude + from: $data[2] + decode: { fn: integer, args: { multiplier: 100 } } + - name: departure_icao + from: $data[3] + decode: { fn: airport } + - name: arrival_icao + from: $data[4] + decode: { fn: airport } + - name: month + from: $data[5] + decode: { fn: integer, args: { substring_start: 0, substring_length: 2 } } + - name: day + from: $data[5] + decode: { fn: integer, args: { substring_start: 2, substring_length: 2 } } + - name: timestamp + from: $data[6] + decode: { fn: timestamp_hhmmss } + - name: eta_time + from: $data[7] + decode: { fn: timestamp_hhmmss } + - name: fuel_remaining + from: $data[8] + when: { not: { in: ["$data[8]", ["---.-"]] } } + decode: { fn: float } +formatted: + description: "ETA Report" + items: + - { type: position, value: $position } + - { type: altitude, value: $altitude } + - { type: airport_origin, value: $departure_icao } + - { type: airport_destination, value: $arrival_icao } + - { type: timestamp, kind: month, value: $month } + - { type: timestamp, kind: day, value: $day } + - { type: timestamp, kind: current, value: $timestamp } + - { type: timestamp, kind: eta, value: $eta_time } + - { type: fuel, kind: remaining, value: $fuel_remaining, when_present: true } diff --git a/spec/labels/44/IN.yaml b/spec/labels/44/IN.yaml new file mode 100644 index 0000000..8af0805 --- /dev/null +++ b/spec/labels/44/IN.yaml @@ -0,0 +1,46 @@ +spec_version: "1" +plugin: + name: Label_44_IN + type: text + decode_level: FULL +qualifiers: + labels: ["44"] + preambles: ["00IN01", "00IN02", "00IN03", "IN01", "IN02", "IN03"] +parse: + - { split: ",", into: data } + - { require_length: { var: data, min: 7 }, else: fail } +fields: + - name: position + from: $data[1] + decode: + fn: coordinate_decimal_minutes + args: { style: combined, format: "NSDDMM_M_EWDDMM_M" } + - name: departure_icao + from: $data[2] + decode: { fn: airport } + - name: arrival_icao + from: $data[3] + decode: { fn: airport } + - name: month + from: $data[4] + decode: { fn: integer, args: { substring_start: 0, substring_length: 2 } } + - name: day + from: $data[4] + decode: { fn: integer, args: { substring_start: 2, substring_length: 2 } } + - name: in_time + from: $data[5] + decode: { fn: timestamp_hhmmss } + - name: fuel_remaining + from: $data[6] + when: { not: { in: ["$data[6]", ["---.-"]] } } + decode: { fn: float } +formatted: + description: "In Gate Report" + items: + - { type: position, value: $position } + - { type: airport_origin, value: $departure_icao } + - { type: airport_destination, value: $arrival_icao } + - { type: timestamp, kind: month, value: $month } + - { type: timestamp, kind: day, value: $day } + - { type: timestamp, kind: in, value: $in_time } + - { type: fuel, kind: remaining, value: $fuel_remaining, when_present: true } diff --git a/spec/labels/44/OFF.yaml b/spec/labels/44/OFF.yaml new file mode 100644 index 0000000..4d5c35e --- /dev/null +++ b/spec/labels/44/OFF.yaml @@ -0,0 +1,50 @@ +spec_version: "1" +plugin: + name: Label_44_OFF + type: text + decode_level: FULL +qualifiers: + labels: ["44"] + preambles: ["00OFF01", "00OFF02", "00OFF03", "OFF01", "OFF02", "OFF03"] +parse: + - { split: ",", into: data } + - { require_length: { var: data, min: 8 }, else: fail } +fields: + - name: position + from: $data[1] + decode: + fn: coordinate_decimal_minutes + args: { style: combined, format: "NSDDMM_M_EWDDMM_M" } + - name: departure_icao + from: $data[2] + decode: { fn: airport } + - name: arrival_icao + from: $data[3] + decode: { fn: airport } + - name: month + from: $data[4] + decode: { fn: integer, args: { substring_start: 0, substring_length: 2 } } + - name: day + from: $data[4] + decode: { fn: integer, args: { substring_start: 2, substring_length: 2 } } + - name: off_time + from: $data[5] + decode: { fn: timestamp_hhmmss } + - name: eta_time + from: $data[6] + decode: { fn: timestamp_hhmmss } + - name: fuel_remaining + from: $data[7] + when: { not: { in: ["$data[7]", ["---.-"]] } } + decode: { fn: float } +formatted: + description: "Off Runway Report" + items: + - { type: position, value: $position } + - { type: airport_origin, value: $departure_icao } + - { type: airport_destination, value: $arrival_icao } + - { type: timestamp, kind: month, value: $month } + - { type: timestamp, kind: day, value: $day } + - { type: timestamp, kind: off, value: $off_time } + - { type: timestamp, kind: eta, value: $eta_time } + - { type: fuel, kind: remaining, value: $fuel_remaining, when_present: true } diff --git a/spec/labels/44/ON.yaml b/spec/labels/44/ON.yaml new file mode 100644 index 0000000..18b9999 --- /dev/null +++ b/spec/labels/44/ON.yaml @@ -0,0 +1,46 @@ +spec_version: "1" +plugin: + name: Label_44_ON + type: text + decode_level: FULL +qualifiers: + labels: ["44"] + preambles: ["00ON01", "00ON02", "00ON03", "ON01", "ON02", "ON03"] +parse: + - { split: ",", into: data } + - { require_length: { var: data, min: 7 }, else: fail } +fields: + - name: position + from: $data[1] + decode: + fn: coordinate_decimal_minutes + args: { style: combined, format: "NSDDMM_M_EWDDMM_M" } + - name: departure_icao + from: $data[2] + decode: { fn: airport } + - name: arrival_icao + from: $data[3] + decode: { fn: airport } + - name: month + from: $data[4] + decode: { fn: integer, args: { substring_start: 0, substring_length: 2 } } + - name: day + from: $data[4] + decode: { fn: integer, args: { substring_start: 2, substring_length: 2 } } + - name: on_time + from: $data[5] + decode: { fn: timestamp_hhmmss } + - name: fuel_remaining + from: $data[6] + when: { not: { in: ["$data[6]", ["---.-"]] } } + decode: { fn: float } +formatted: + description: "On Runway Report" + items: + - { type: position, value: $position } + - { type: airport_origin, value: $departure_icao } + - { type: airport_destination, value: $arrival_icao } + - { type: timestamp, kind: month, value: $month } + - { type: timestamp, kind: day, value: $day } + - { type: timestamp, kind: on, value: $on_time } + - { type: fuel, kind: remaining, value: $fuel_remaining, when_present: true } diff --git a/spec/labels/44/Slash.yaml b/spec/labels/44/Slash.yaml new file mode 100644 index 0000000..6e8ce3d --- /dev/null +++ b/spec/labels/44/Slash.yaml @@ -0,0 +1,16 @@ +spec_version: "1" +plugin: + name: Label_44_Slash + type: text + decode_level: PARTIAL +qualifiers: + labels: ["44"] + preambles: [" /FB"] +parse: + # Splits on '/' (4 fields), then inner split of fields[3] on ',' with + # nested length-based branch for 18-field extended form (runway + procedure + # via FlightPlanUtils). Char-prefix sign decoding for coords. Whole-plugin hatch. + custom: label_44_slash_decode +formatted: + description: "Flight Briefing" + custom: label_44_slash_format diff --git a/spec/labels/4A/01.yaml b/spec/labels/4A/01.yaml new file mode 100644 index 0000000..97ef075 --- /dev/null +++ b/spec/labels/4A/01.yaml @@ -0,0 +1,16 @@ +spec_version: "1" +plugin: + name: Label_4A_01 + type: text + decode_level: MESSAGE +qualifiers: + labels: ["4A"] + preambles: ["01"] +parse: + # Single multiline regex with 9 capture groups, plus state_change/temperature + # emission (non-canonical formatter types) and sign-stripping on altitude/temp. + # Whole-plugin hatch. + custom: label_4a_01_decode +formatted: + description: "Latest New Format" + custom: label_4a_01_format diff --git a/spec/labels/4A/DIS.yaml b/spec/labels/4A/DIS.yaml new file mode 100644 index 0000000..c0c5f2b --- /dev/null +++ b/spec/labels/4A/DIS.yaml @@ -0,0 +1,15 @@ +spec_version: "1" +plugin: + name: Label_4A_DIS + type: text + decode_level: FULL +qualifiers: + labels: ["4A"] + preambles: ["DIS"] +parse: + # Imperative: split on comma, slice 2-char timestamp prefix off fields[1], + # join fields.slice(3) without separator. Done as whole-plugin hatch. + custom: label_4a_dis_decode +formatted: + description: "Latest New Format" + custom: label_4a_dis_format diff --git a/spec/labels/4A/DOOR.yaml b/spec/labels/4A/DOOR.yaml new file mode 100644 index 0000000..11e7002 --- /dev/null +++ b/spec/labels/4A/DOOR.yaml @@ -0,0 +1,15 @@ +spec_version: "1" +plugin: + name: Label_4A_DOOR + type: text + decode_level: FULL +qualifiers: + labels: ["4A"] + preambles: ["DOOR"] +parse: + # Imperative: split on space, then split fields[0] on '/' to get door name, + # emit door_event (non-canonical formatter type). Done as whole-plugin hatch. + custom: label_4a_door_decode +formatted: + description: "Latest New Format" + custom: label_4a_door_format diff --git a/spec/labels/4A/Slash_01.yaml b/spec/labels/4A/Slash_01.yaml new file mode 100644 index 0000000..452d892 --- /dev/null +++ b/spec/labels/4A/Slash_01.yaml @@ -0,0 +1,14 @@ +spec_version: "1" +plugin: + name: Label_4A_Slash_01 + type: text + decode_level: MESSAGE +qualifiers: + labels: ["4A"] + preambles: ["/01"] +parse: + # Trivial but writes to remaining.text (no declarative primitive for that). + custom: label_4a_slash_01_decode +formatted: + description: "Latest New Format" + custom: label_4a_slash_01_format diff --git a/spec/labels/4N.yaml b/spec/labels/4N.yaml new file mode 100644 index 0000000..2bd864f --- /dev/null +++ b/spec/labels/4N.yaml @@ -0,0 +1,16 @@ +spec_version: "1" +plugin: + name: Label_4N + type: text + decode_level: MESSAGE +qualifiers: + labels: ["4N"] +parse: + # Two completely different variants dispatched by raw text length (51) vs + # CSV field count (33). Variant 1 uses fixed-offset substrings with regex + # massage of coords. Variant 2 uses CSV with sub-splits, filter, and + # multi-field unknownArr aggregation. Whole-plugin hatch. + custom: label_4n_decode +formatted: + description: "Airline Defined" + custom: label_4n_format diff --git a/spec/labels/4T/AGFSR.yaml b/spec/labels/4T/AGFSR.yaml new file mode 100644 index 0000000..5653b7e --- /dev/null +++ b/spec/labels/4T/AGFSR.yaml @@ -0,0 +1,18 @@ +spec_version: "1" +plugin: + name: Label_4T_AGFSR + type: text + decode_level: PARTIAL +qualifiers: + labels: ["4T"] + preambles: ["AGFSR"] +parse: + # Substring slice + split on '/' with 20-part length gate, plus inline + # lat/lon parsing (deg from digits, minutes from sub-digits, N/S/E/W sign + # via CoordinateUtils.getDirection). Many positional fields with + # IATA-coded airport prefixes and unknown-array tails — beyond the + # declarative spec without adding new primitives. + custom: label_4t_agfsr_parse +formatted: + description: "Position Report" + custom: label_4t_agfsr_format diff --git a/spec/labels/4T/ETA.yaml b/spec/labels/4T/ETA.yaml new file mode 100644 index 0000000..6bb5ca3 --- /dev/null +++ b/spec/labels/4T/ETA.yaml @@ -0,0 +1,18 @@ +spec_version: "1" +plugin: + name: Label_4T_ETA + type: text + decode_level: FULL +qualifiers: + labels: ["4T"] + preambles: ["ETA"] +parse: + # message.text.substring(3).split('/') with strict 3-part length, then a + # second split-on-space inside data[2]. Uses IATA-coded airport + # ResultFormatter and HHMM→TOD timestamp via convertHHMMSSToTod on a + # 4-char prefix. Departure/arrival "day" formatters aren't represented in + # the formatter type enum — escape hatch keeps byte-for-byte parity. + custom: label_4t_eta_parse +formatted: + description: "ETA Report" + custom: label_4t_eta_format diff --git a/spec/labels/58.yaml b/spec/labels/58.yaml new file mode 100644 index 0000000..65456e1 --- /dev/null +++ b/spec/labels/58.yaml @@ -0,0 +1,17 @@ +spec_version: "1" +plugin: + name: Label_58 + type: text + decode_level: PARTIAL +qualifiers: + labels: ["58"] +parse: + # split('/') with strict 8-part length gate; coordinate parsing uses + # CoordinateUtils.getDirection(first-char) * Number(rest) for both axes, + # which doesn't fit the existing `coordinate` decode args (no divisor here). + # Also uses `day` formatter (not in formatter type enum) and trailing + # unknown('/') tails for parts[6]/[7]. Whole-plugin hatch for parity. + custom: label_58_parse +formatted: + description: "Position Report" + custom: label_58_format diff --git a/spec/labels/5Z/Slash.yaml b/spec/labels/5Z/Slash.yaml new file mode 100644 index 0000000..641b1c5 --- /dev/null +++ b/spec/labels/5Z/Slash.yaml @@ -0,0 +1,20 @@ +spec_version: "1" +plugin: + name: Label_5Z_Slash + type: text + decode_level: MESSAGE +qualifiers: + labels: ["5Z"] + preambles: ["/"] +parse: + # Multi-format airline-defined downlink: + # - /TXT line + remainder as free text (full decode) + # - 25-entry descriptions table dispatch (B1/B3/CD/CG/CM/C3/C4/C5/C6/10/C11/DS/D3/D6/D7/EO/ET/PW/RL/R3/R4/TC/WB/W1) + # - per-type branches with magic-string discriminants + # (e.g. "B3 TO DATA REQ ", "C3 GATE REQ ") + # - injects airline=United Airlines + message_type items + # Too many specialized branches and magic substrings for declarative variants. + custom: label_5z_slash_parse +formatted: + description: "Airline Designated Downlink" + custom: label_5z_slash_format diff --git a/spec/labels/80.yaml b/spec/labels/80.yaml new file mode 100644 index 0000000..87dcecb --- /dev/null +++ b/spec/labels/80.yaml @@ -0,0 +1,20 @@ +spec_version: "1" +plugin: + name: Label_80 + type: text + decode_level: MESSAGE +qualifiers: + labels: ["80"] +parse: + # Two top-level formats: + # - Single-line CSV with 9 comma-separated parts → parseCsvFormat + # - Multi-line with header + tag-prefixed key/value pairs + # (POS / ALT / FL / MCH / SPD / TAS / SAT / FB / FOB / UTC / ETA / + # HDG / NWYP / SWN / DWN / AD / default) + # Multiple inline regex captures (POS lat/lon), unit transforms + # (MCH/1000, FL*100, FOB strip non-numeric), and per-tag dispatch make a + # whole-plugin hatch the safest path to byte-for-byte parity. + custom: label_80_parse +formatted: + description: "Airline Defined Position Report" + custom: label_80_format diff --git a/spec/labels/83.yaml b/spec/labels/83.yaml new file mode 100644 index 0000000..d87d45e --- /dev/null +++ b/spec/labels/83.yaml @@ -0,0 +1,17 @@ +spec_version: "1" +plugin: + name: Label_83 + type: text + decode_level: MESSAGE +qualifiers: + labels: ["83"] +parse: + # Three discriminated variants: + # - Prefix "4DH3 ETAT2" → split on whitespace, eta from fields[6]+"00" + # - Prefix "001PR" → fixed-offset substrings: day, decimal-min coords (with '.' stripped), altitude, tail unknown + # - Default → whitespace-stripped then split(',') with 9-part shape: airports + lat/lon as raw Number(), altitude/groundspeed/heading + # Mixed substring slicing + variant discrimination by message prefix. + custom: label_83_parse +formatted: + description: "Airline Defined" + custom: label_83_format diff --git a/spec/labels/8E.yaml b/spec/labels/8E.yaml new file mode 100644 index 0000000..5b42b59 --- /dev/null +++ b/spec/labels/8E.yaml @@ -0,0 +1,16 @@ +spec_version: "1" +plugin: + name: Label_8E + type: text + decode_level: FULL +qualifiers: + labels: ["8E"] +parse: + # Regex match is best-effort: TS unconditionally marks decoded=true even if + # the regex doesn't match (no fields are formatted in that case). The + # `convertHHMMSSToTod` call is also fed a 4-digit HHMM string verbatim + # (no '00' append). Whole-plugin hatch preserves that exact behavior. + custom: label_8e_parse +formatted: + description: "ETA Report" + custom: label_8e_format diff --git a/spec/labels/B6/Forwardslash.yaml b/spec/labels/B6/Forwardslash.yaml new file mode 100644 index 0000000..218e251 --- /dev/null +++ b/spec/labels/B6/Forwardslash.yaml @@ -0,0 +1,16 @@ +spec_version: "1" +plugin: + name: Label_B6_Forwardslash + type: text + decode_level: NONE +qualifiers: + labels: ["B6"] + preambles: ["/"] +parse: + # TS source is a placeholder: returns defaultResult with description only, + # no fields parsed, decoded stays false. The escape hatch matches that + # exactly (also preserves the debug log path). + custom: label_b6_forwardslash_parse +formatted: + description: "CPDLC Message" + custom: label_b6_forwardslash_format diff --git a/spec/labels/ColonComma.yaml b/spec/labels/ColonComma.yaml new file mode 100644 index 0000000..6c5494d --- /dev/null +++ b/spec/labels/ColonComma.yaml @@ -0,0 +1,13 @@ +spec_version: "1" +plugin: + name: Label_ColonComma + type: text + docs: https://github.com/airframesio/acars-message-documentation/blob/main/research/colon_comma/README.md + decode_level: FULL +qualifiers: + labels: [":;"] +parse: + custom: label_colon_comma_parse +formatted: + description: "Aircraft Transceiver Frequency Change" + custom: label_colon_comma_format diff --git a/spec/labels/H1/ATIS.yaml b/spec/labels/H1/ATIS.yaml new file mode 100644 index 0000000..a2c9bc2 --- /dev/null +++ b/spec/labels/H1/ATIS.yaml @@ -0,0 +1,20 @@ +spec_version: "1" +plugin: + name: Label_H1_ATIS + type: text + decode_level: FULL +qualifiers: + # NOTE: TS qualifier returns labels=['H1','5Z'] both routed to preamble 'L'. + # Encoding both here so the spec qualifier matches the plugin's intent. + labels: ["H1", "5Z"] + preambles: ["L"] +parse: + # Regex with 6 named groups (seq, flight, facility, code, airport, checksum) + # plus conditional ATIS-airport item (only when airport != facility) and + # hex-parsed checksum item (parseInt(checksum, 16)). The conditional + # second airport item and `atis_code` raw key don't fit the formatter + # type enum cleanly — escape hatch preserves item set and ordering. + custom: label_h1_atis_parse +formatted: + description: "ATIS Subscription" + custom: label_h1_atis_format diff --git a/spec/labels/H1/EZF.yaml b/spec/labels/H1/EZF.yaml new file mode 100644 index 0000000..6177a81 --- /dev/null +++ b/spec/labels/H1/EZF.yaml @@ -0,0 +1,23 @@ +spec_version: "1" +plugin: + name: Label_H1_EZF + type: text + decode_level: PARTIAL +qualifiers: + # TS qualifier returns labels=['H1','1M'] with preamble 'EZF'. + labels: ["H1", "1M"] + preambles: ["EZF"] +parse: + # Multi-line load-sheet parser: + # - split on '\n', trim, filter empties + # - first line must equal 'EZF' (else failUnknown) + # - second line is flight identifier "NO0246/10/EI-NEO" → flight + tail + # - third line is config string (stored as unknown) + # - subsequent '-KEY/VALUE' lines parsed into a loadsheet dict + # - SCT key further split on '-' into IATA dep/arr airports + # - 8 known loadsheet keys (STD, FLT STATUS, UOM, ZFW, PAX, TOW, DOW, FWT) + # are formatted into custom 'loadsheet'-type items. + custom: label_h1_ezf_parse +formatted: + description: "Load Sheet" + custom: label_h1_ezf_format diff --git a/spec/labels/H1/FLR.yaml b/spec/labels/H1/FLR.yaml new file mode 100644 index 0000000..30d77f2 --- /dev/null +++ b/spec/labels/H1/FLR.yaml @@ -0,0 +1,18 @@ +spec_version: "1" +plugin: + name: Label_H1_FLR + type: text + decode_level: PARTIAL +qualifiers: + labels: ["H1"] + preambles: ["FLR", "#CFBFLR"] +parse: + # Split on '/FR', then header parts before and fault body after. + # Body[0..20] is metadata with YYMMDDHHMMSS at [0..12]; date is rebuilt + # as DDMMYY by slicing (4,6)+(2,4)+(0,2), then convertDateTimeToEpoch. + # Body[20..] is the free-text fault message stored as a custom 'fault'-type + # item with code 'FR'. Header loop pushes leading fields as unknown('/'). + custom: label_h1_flr_parse +formatted: + description: "Fault Log Report" + custom: label_h1_flr_format diff --git a/spec/labels/H1/M_POS.yaml b/spec/labels/H1/M_POS.yaml new file mode 100644 index 0000000..079d7c1 --- /dev/null +++ b/spec/labels/H1/M_POS.yaml @@ -0,0 +1,17 @@ +spec_version: "1" +plugin: + name: Label_H1_M_POS + type: text + decode_level: MESSAGE +qualifiers: + labels: ["H1"] +parse: + # M[NN]A[AA][NNNN] header regex; remainder split on ',' with a 7-field + # minimum (origin, dest, DDHHMM, lat, lon, alt, hdg + optional tail). + # Coordinates are raw parseFloat after stripping whitespace. + # Decode level is computed per-message: 'full' if no trailing unknowns, + # 'partial' if any. Tail unknown args use default separator (no '/'). + custom: label_h1_m_pos_parse +formatted: + description: "M-Series Periodic Position Report" + custom: label_h1_m_pos_format diff --git a/spec/labels/H1/OFP.yaml b/spec/labels/H1/OFP.yaml new file mode 100644 index 0000000..7105400 --- /dev/null +++ b/spec/labels/H1/OFP.yaml @@ -0,0 +1,21 @@ +spec_version: "1" +plugin: + name: Label_H1_OFP + type: text + decode_level: FULL +qualifiers: + # TS qualifier returns labels=['H1','1M'] (no preambles); guard is the + # '(FPL-' substring presence check inside decode(). + labels: ["H1", "1M"] +parse: + # Delegates to parseIcaoFpl (icao_fpl_utils.ts) — a multi-stage ICAO + # flight-plan parser with state-machine bracket matching and rich field + # extraction (callsign, aircraftType, departure, destination, alternates, + # route, REG, flightRules, cruiseSpeed, cruiseLevel, otherInfo dict). + # Result is mapped to a fixed item list with rules-code translation map + # (I/V/Y/Z → IFR/VFR/IFR_VFR/VFR_IFR). Far too complex to express + # declaratively — whole-plugin hatch. + custom: label_h1_ofp_parse +formatted: + description: "Operational Flight Plan" + custom: label_h1_ofp_format diff --git a/spec/labels/H1/Paren.yaml b/spec/labels/H1/Paren.yaml new file mode 100644 index 0000000..5290676 --- /dev/null +++ b/spec/labels/H1/Paren.yaml @@ -0,0 +1,18 @@ +spec_version: "1" +plugin: + name: Label_H1_Paren + type: text + decode_level: PARTIAL +qualifiers: + labels: ["H1"] + preambles: ["("] +parse: + # Multi-line regex (POS-FLT LAT LON / TIMESTAMP F-ALT \n RMK/FUEL F.F M.MM) + # with embedded lat/lon parsers (parseLat/parseLon do deg-min decode with + # NSEW sign). On a successful match, also pushes a literal 'RMK' unknown + # marker. Fuel uses parseFloat (decimal value), mach uses parseFloat, and + # altitude is parsed * 100. Whole-plugin hatch for parity. + custom: label_h1_paren_parse +formatted: + description: "Position Report" + custom: label_h1_paren_format diff --git a/spec/labels/H1/StarPOS.yaml b/spec/labels/H1/StarPOS.yaml new file mode 100644 index 0000000..d713c09 --- /dev/null +++ b/spec/labels/H1/StarPOS.yaml @@ -0,0 +1,19 @@ +spec_version: "1" +plugin: + name: Label_H1_StarPOS + type: text + decode_level: PARTIAL +qualifiers: + labels: ["H1"] + preambles: ["*POS"] +parse: + # Fixed-length (43 char) message with mandatory '*POS' prefix; fixed + # substring offsets for: month [4..6], day [6..8], time HHMM [8..12], + # lat NSDM [12..17] split as N|S sign + 2-digit deg + 2-digit decimal-min, + # lon EWDM [17..23] split as E|W sign + 3-digit deg + 2-digit decimal-min, + # altitude [23..28], trailing unknown [28..]. The lat/lon math is + # deg + min/60 (not the standard decodeStringCoordinates). + custom: label_h1_starpos_parse +formatted: + description: "Position Report" + custom: label_h1_starpos_format diff --git a/spec/labels/H1/WRN.yaml b/spec/labels/H1/WRN.yaml new file mode 100644 index 0000000..d73d8d1 --- /dev/null +++ b/spec/labels/H1/WRN.yaml @@ -0,0 +1,18 @@ +spec_version: "1" +plugin: + name: Label_H1_WRN + type: text + decode_level: PARTIAL +qualifiers: + labels: ["H1"] + preambles: ["WRN", "#CFBWRN"] +parse: + # Split on '/WN'; header fields[1..] become unknown items. Body[0..20] is + # metadata: YYMMDDHHMMSS at [0..12] reassembled as DDMMYY (slices + # (4,6)+(2,4)+(0,2)) for convertDateTimeToEpoch. Body[20..] is the + # warning message stored as a custom 'warning'-type item with code 'WRN'. + # Mirrors Label_H1_FLR structure but with different prefix and item type. + custom: label_h1_wrn_parse +formatted: + description: "Warning Message" + custom: label_h1_wrn_format diff --git a/spec/labels/H2/02E.yaml b/spec/labels/H2/02E.yaml new file mode 100644 index 0000000..07e928f --- /dev/null +++ b/spec/labels/H2/02E.yaml @@ -0,0 +1,20 @@ +spec_version: "1" +plugin: + name: Label_H2_02E + type: text + decode_level: MESSAGE +qualifiers: + labels: ["H2"] + preambles: ["02E"] +parse: + # Whole-plugin escape hatch: + # - Space-split message, last part must be "Q" sentinel. + # - 45-char header parses 02E + day + departure ICAO + arrival ICAO + first wind. + # - Each subsequent space-separated chunk must start with "Q" and contains a 32-char weather record. + # - Failed parses get appended (space-joined) to remaining.text. + # - decode level is "full" if remaining.text empty, else "partial". + # The iterative accumulation of remaining.text and dynamic wind-data array don't fit DSL primitives. + custom: label_h2_02e_dispatch +formatted: + description: "Weather Report" + custom: label_h2_02e_format diff --git a/spec/labels/HX.yaml b/spec/labels/HX.yaml new file mode 100644 index 0000000..fa0d620 --- /dev/null +++ b/spec/labels/HX.yaml @@ -0,0 +1,22 @@ +spec_version: "1" +plugin: + name: Label_HX + type: text + decode_level: MESSAGE +qualifiers: + labels: ["HX"] + preambles: ["RA FMT LOCATION", "RA FMT 43"] +parse: + # Whole-plugin escape hatch: + # - Space-split message, dispatch on parts[2] = "LOCATION" or "43". + # - LOCATION variant: parts[3]/parts[4] hold packed coords (e.g. "N4009.6" + "W07540.8"). + # Position computed as deg + min/60, signed by N/S and E/W. parts.slice(5) accumulated + # into remaining.text joined by " ". + # - 43 variant: parts[3] is departure airport, parts.slice(4) accumulated into remaining. + # - Other parts[2] => decoded=false, decodeLevel=none. + # The packed-coord substring math + unknownArr(parts.slice(N), " ") accumulation is not + # cleanly expressible with current DSL primitives, so the whole plugin is a hatch. + custom: label_hx_dispatch +formatted: + description: "Undelivered Uplink Report" + custom: label_hx_format diff --git a/spec/labels/MA.yaml b/spec/labels/MA.yaml new file mode 100644 index 0000000..49a0dc3 --- /dev/null +++ b/spec/labels/MA.yaml @@ -0,0 +1,19 @@ +spec_version: "1" +plugin: + name: Label_MA + type: text + decode_level: MESSAGE +qualifiers: + labels: ["MA"] +parse: + # Whole-plugin escape hatch: + # - Calls MIAMCoreUtils.parse(message.text) -- ASCII85 decode, deflate, CRC checks, + # ACARS PDU framing (version-dependent). + # - If MIAM-decoded message has acars data with crcOk & complete & text, recursively + # invokes this.decoder.decode({label, sublabel, text}) and merges raw/items/remaining. + # - Decode level flips between "full" (CRC OK + complete + nested decode) and "partial". + # Recursive MessageDecoder use and version-dependent MIAM dispatch live in escape hatches. + custom: label_ma_dispatch +formatted: + description: "Unknown" + custom: label_ma_format diff --git a/spec/labels/QP.yaml b/spec/labels/QP.yaml new file mode 100644 index 0000000..2282a06 --- /dev/null +++ b/spec/labels/QP.yaml @@ -0,0 +1,19 @@ +spec_version: "1" +plugin: + name: Label_QP + type: text + decode_level: MESSAGE +qualifiers: + labels: ["QP"] +parse: + # Whole-plugin escape hatch: + # - departure_icao = text[0:4], arrival_icao = text[4:8] + # - out_time = HHMMSSToTod(text[8:12]) (4-char HHMM, internally padded "00") + # - remaining.text = text[12:] (via ResultFormatter.unknown side effect) + # - decodeLevel "full" iff remaining empty, else "partial". + # The OUT time formatter + remaining.text side-effect from .unknown() doesn't map cleanly + # to declarative formatter items, so kept as escape hatch. + custom: label_qp_dispatch +formatted: + description: "OUT Report" + custom: label_qp_format diff --git a/spec/labels/QQ.yaml b/spec/labels/QQ.yaml new file mode 100644 index 0000000..8441cb5 --- /dev/null +++ b/spec/labels/QQ.yaml @@ -0,0 +1,23 @@ +spec_version: "1" +plugin: + name: Label_QQ + type: text + decode_level: MESSAGE +qualifiers: + labels: ["QQ"] +parse: + # Whole-plugin escape hatch: + # - departure_icao = text[0:4], arrival_icao = text[4:8] + # - Variant A: text[12:19] == "\r\n001FE": + # day=text[19:21]; OFF time=HHMMSSToTod(text[21:27]); + # position from text[27:34]/[34:42] (NSDDMM.M / EWDDDMM.M packed) + # unknown(text[42:45]) then position; if that unknown != "---" then groundspeed=int(text[45:48]) + # else unknown(text[45:48]); finally unknown(text[48:]). + # - Variant B (fallback): OFF time=HHMMSSToTod(text[8:12]+"00"); unknown(text[12:]). + # - Order-sensitive side effects on remaining.text via ResultFormatter.unknown. + # The position-vs-groundspeed branching keyed on prior "remaining" content makes this + # whole-plugin escape hatch the cleanest route to byte-for-byte parity. + custom: label_qq_dispatch +formatted: + description: "OFF Report" + custom: label_qq_format diff --git a/spec/labels/QR.yaml b/spec/labels/QR.yaml new file mode 100644 index 0000000..e1cc8f6 --- /dev/null +++ b/spec/labels/QR.yaml @@ -0,0 +1,17 @@ +spec_version: "1" +plugin: + name: Label_QR + type: text + decode_level: MESSAGE +qualifiers: + labels: ["QR"] +parse: + # Whole-plugin escape hatch (mirrors QP, but emits ON time instead of OUT): + # - departure_icao = text[0:4], arrival_icao = text[4:8] + # - on_time = HHMMSSToTod(text[8:12]) + # - remaining.text = text[12:] + # - decodeLevel "full" iff remaining empty, else "partial". + custom: label_qr_dispatch +formatted: + description: "ON Report" + custom: label_qr_format diff --git a/spec/labels/QS.yaml b/spec/labels/QS.yaml new file mode 100644 index 0000000..07009fc --- /dev/null +++ b/spec/labels/QS.yaml @@ -0,0 +1,17 @@ +spec_version: "1" +plugin: + name: Label_QS + type: text + decode_level: MESSAGE +qualifiers: + labels: ["QS"] +parse: + # Whole-plugin escape hatch (mirrors QP, but emits IN time instead of OUT): + # - departure_icao = text[0:4], arrival_icao = text[4:8] + # - in_time = HHMMSSToTod(text[8:12]) + # - remaining.text = text[12:] + # - decodeLevel "full" iff remaining empty, else "partial". + custom: label_qs_dispatch +formatted: + description: "IN Report" + custom: label_qs_format diff --git a/spec/labels/SQ.yaml b/spec/labels/SQ.yaml new file mode 100644 index 0000000..326b78d --- /dev/null +++ b/spec/labels/SQ.yaml @@ -0,0 +1,23 @@ +spec_version: "1" +plugin: + name: Label_SQ + type: text + decode_level: MESSAGE +qualifiers: + labels: ["SQ"] +parse: + # Whole-plugin escape hatch: + # - preamble=text[0:4]; version=Number(text[1:2]); network=text[3:4] + # - When version === 2, run the v2 regex + # /0(\d)X(?\w)(?\w{3})(?\w{4})(?\d) + # (?\d+)(?[NS])(?\d+)(?[EW])V(?\d+)\/.*/ + # and populate groundStation + vdlFrequency. + # - Network formatted text from A/S/other -> ARINC/SITA/Unknown. + # - Many optional formatted.items only emitted when their respective fields exist + # (groundStation, iataCode, icaoCode, coordinates.latitude, airport, vdlFrequency). + # The optional-emit logic, custom NETT/VER/GNDSTN/IATA/ICAO/COORD/APT/VDLFRQ item types + # all live in escape hatches for byte-for-byte parity. + custom: label_sq_dispatch +formatted: + description: "Ground Station Squitter" + custom: label_sq_format diff --git a/spec/wildcards/cband.yaml b/spec/wildcards/cband.yaml new file mode 100644 index 0000000..1a537c1 --- /dev/null +++ b/spec/wildcards/cband.yaml @@ -0,0 +1,20 @@ +spec_version: "1" +plugin: + name: CBand + type: text + decode_level: MESSAGE +qualifiers: + labels: ["*"] +parse: + # Whole-plugin escape hatch (per ESCAPE_HATCHES.md guidance): + # - Match leading 10-char C-Band header: + # /^(?[A-Z]\d{2}[A-Z])(?[A-Z0-9]{2})(?[0-9]{4})/ + # - If matched, strip first 10 chars, recursively invoke MessageDecoder on the + # remainder with the same label/sublabel, then merge raw/items/remaining and + # prepend flight number "airline + Number(number)". + # - decoder.name becomes "c-band-" + nested decoder name; decode level inherited. + # Recursive MessageDecoder use is explicitly called out as a full escape-hatch case. + custom: cband_dispatch +formatted: + description: "C-Band Message" + custom: cband_format diff --git a/spec/wildcards/label_13_18_slash.yaml b/spec/wildcards/label_13_18_slash.yaml new file mode 100644 index 0000000..392b068 --- /dev/null +++ b/spec/wildcards/label_13_18_slash.yaml @@ -0,0 +1,16 @@ +spec_version: "1" +plugin: + name: Label_13Through18_Slash + type: text + decode_level: PARTIAL +qualifiers: + labels: ["13", "14", "15", "16", "17", "18"] + preambles: ["/"] +parse: + # Pure escape hatch: matches labels 13..18 with different field counts (4 vs 7), + # dispatches to out/off/on/in formatter based on numeric label, line-by-line + # LOC parsing with two DMS variants and signed decimal degrees variant. + custom: label_13_18_slash_decode +formatted: + description: "OOOI Report" + custom: label_13_18_slash_format From 6da198f085a6ae56eb0291fe938c239aef8faa93 Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Wed, 27 May 2026 23:17:26 -0700 Subject: [PATCH 07/23] Bulk-extract 288 corpus samples across 57 specs from TS test suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the 2 manual reference samples with an auto-extracted corpus captured by running the full acars-decoder-typescript test suite (88 suites, 407 passing tests, 9 skipped) through a Jest setup hook that wraps every plugin's prototype.decode and records (input → DecodeResult) pairs to JSONL. A post-processing script groups records by plugin slug and writes per-spec sample files at corpus//sample-NNN.json. Coverage: - 288 samples across 57 of the 68 plugin specs - Sample-001 typically the canonical "successful decode" case; later samples cover variants and failure modes - The extracted "expected" field is the *actual* DecodeResult shape the TS production code returns, so cross-implementation parity is enforced against real production behavior, not assertion fragments Unmatched (need v1.1 follow-up — 6 plugin slugs): - c-band (spec slug: cband — TS hyphenates) - label-1l-3-line (spec slug: label-1l-3line — slug function doesn't split digit→lowercase boundaries) - label-h1-star-pos (spec slug: label-h1-starpos — uppercase-run boundary) - label-13-18-slash (spec slug match issue) - label-1l-1-line, test-label-44 (no matching spec; possibly stale TS test fixtures) Recommended v1.1 fix: add an explicit `slug` field on `plugin` in the spec for the divergent cases (escape hatch), or use a camelCase-aware slugger (with care not to regress Label_4A → label-4a). Tooling (intentionally NOT committed): - /tmp/corpus-extractor.js — Jest setupFilesAfterEnv hook that monkey- patches each plugin class's prototype.decode to log records - /tmp/corpus-postprocess.js — reads JSONL, groups by slug, writes sample files Rerun: cd acars-decoder-typescript rm -f /tmp/extracted-corpus.jsonl && touch /tmp/extracted-corpus.jsonl npm test -- --setupFilesAfterEnv=/tmp/corpus-extractor.js --runInBand --silent node /tmp/corpus-postprocess.js (These belong as committed tooling in codegen/scripts/ in a follow-up.) Co-Authored-By: Claude Opus 4.7 (1M context) --- corpus/labels/10/LDR/sample-001.json | 72 ++++ corpus/labels/10/LDR/sample-002.json | 79 +++++ corpus/labels/10/LDR/sample-003.json | 25 ++ corpus/labels/10/POS/sample-001.json | 13 +- ...ample-002-invalid.json => sample-002.json} | 11 +- corpus/labels/10/Slash/sample-001.json | 86 +++++ corpus/labels/10/Slash/sample-002.json | 93 ++++++ corpus/labels/10/Slash/sample-003.json | 25 ++ corpus/labels/12/N_Space/sample-001.json | 44 +++ corpus/labels/12/N_Space/sample-002.json | 44 +++ corpus/labels/12/N_Space/sample-003.json | 25 ++ corpus/labels/12/POS/sample-001.json | 79 +++++ corpus/labels/12/POS/sample-002.json | 25 ++ corpus/labels/15/FST/sample-001.json | 58 ++++ corpus/labels/15/FST/sample-002.json | 27 ++ corpus/labels/15/Paren2/sample-001.json | 51 +++ corpus/labels/15/Paren2/sample-002.json | 51 +++ corpus/labels/15/Paren2/sample-003.json | 44 +++ corpus/labels/15/Paren2/sample-004.json | 44 +++ corpus/labels/15/Paren2/sample-005.json | 44 +++ corpus/labels/15/Paren2/sample-006.json | 44 +++ corpus/labels/15/Paren2/sample-007.json | 25 ++ corpus/labels/16/AUTPOS/sample-001.json | 70 ++++ corpus/labels/16/AUTPOS/sample-002.json | 114 +++++++ corpus/labels/16/AUTPOS/sample-003.json | 23 ++ corpus/labels/16/Honeywell/sample-001.json | 51 +++ corpus/labels/16/Honeywell/sample-002.json | 53 +++ corpus/labels/16/Honeywell/sample-003.json | 53 +++ corpus/labels/16/Honeywell/sample-004.json | 25 ++ corpus/labels/16/N_Space/sample-001.json | 44 +++ corpus/labels/16/N_Space/sample-002.json | 35 ++ corpus/labels/16/N_Space/sample-003.json | 44 +++ corpus/labels/16/N_Space/sample-004.json | 25 ++ corpus/labels/16/POSA1/sample-001.json | 62 ++++ corpus/labels/16/POSA1/sample-002.json | 62 ++++ corpus/labels/16/POSA1/sample-003.json | 25 ++ corpus/labels/16/TOD/sample-001.json | 58 ++++ corpus/labels/16/TOD/sample-002.json | 65 ++++ corpus/labels/16/TOD/sample-003.json | 41 +++ corpus/labels/16/TOD/sample-004.json | 58 ++++ corpus/labels/16/TOD/sample-005.json | 25 ++ corpus/labels/1L/070/sample-001.json | 65 ++++ corpus/labels/1L/070/sample-002.json | 25 ++ corpus/labels/1L/660/sample-001.json | 64 ++++ corpus/labels/1L/660/sample-002.json | 25 ++ corpus/labels/1M/Slash/sample-001.json | 61 ++++ corpus/labels/20/POS/sample-001.json | 36 ++ corpus/labels/20/POS/sample-002.json | 36 ++ corpus/labels/20/POS/sample-003.json | 25 ++ corpus/labels/21/POS/sample-001.json | 73 ++++ corpus/labels/21/POS/sample-002.json | 25 ++ corpus/labels/22/OFF/sample-001.json | 76 +++++ corpus/labels/22/OFF/sample-002.json | 86 +++++ corpus/labels/22/OFF/sample-003.json | 48 +++ corpus/labels/22/OFF/sample-004.json | 23 ++ corpus/labels/22/POS/sample-001.json | 51 +++ corpus/labels/22/POS/sample-002.json | 23 ++ corpus/labels/24/Slash/sample-001.json | 59 ++++ corpus/labels/24/Slash/sample-002.json | 23 ++ corpus/labels/2P/FM3/sample-001.json | 58 ++++ corpus/labels/2P/FM3/sample-002.json | 65 ++++ corpus/labels/2P/FM3/sample-003.json | 58 ++++ corpus/labels/2P/FM3/sample-004.json | 25 ++ corpus/labels/2P/FM4/sample-001.json | 86 +++++ corpus/labels/2P/FM4/sample-002.json | 93 ++++++ corpus/labels/2P/FM4/sample-003.json | 25 ++ corpus/labels/2P/FM5/sample-001.json | 79 +++++ corpus/labels/2P/FM5/sample-002.json | 25 ++ corpus/labels/30/Slash_EA/sample-001.json | 41 +++ corpus/labels/44/ETA/sample-001.json | 91 +++++ corpus/labels/44/ETA/sample-002.json | 25 ++ corpus/labels/44/IN/sample-001.json | 70 ++++ corpus/labels/44/IN/sample-002.json | 77 +++++ corpus/labels/44/IN/sample-003.json | 25 ++ corpus/labels/44/OFF/sample-001.json | 84 +++++ corpus/labels/44/OFF/sample-002.json | 25 ++ corpus/labels/44/ON/sample-001.json | 72 ++++ corpus/labels/44/ON/sample-002.json | 77 +++++ corpus/labels/44/ON/sample-003.json | 25 ++ corpus/labels/44/ON/sample-004.json | 72 ++++ corpus/labels/44/POS/sample-001.json | 85 +++++ corpus/labels/44/POS/sample-002.json | 85 +++++ corpus/labels/44/Slash/sample-001.json | 58 ++++ corpus/labels/44/Slash/sample-002.json | 86 +++++ corpus/labels/44/Slash/sample-003.json | 25 ++ corpus/labels/4A/01/sample-001.json | 79 +++++ corpus/labels/4A/DIS/sample-001.json | 46 +++ corpus/labels/4A/DOOR/sample-001.json | 42 +++ corpus/labels/4A/Slash_01/sample-001.json | 25 ++ corpus/labels/4A/sample-001.json | 69 ++++ corpus/labels/4A/sample-002.json | 62 ++++ corpus/labels/4A/sample-003.json | 55 ++++ corpus/labels/4A/sample-004.json | 69 ++++ corpus/labels/4A/sample-005.json | 58 ++++ corpus/labels/4A/sample-006.json | 25 ++ corpus/labels/4N/sample-001.json | 70 ++++ corpus/labels/4N/sample-002.json | 70 ++++ corpus/labels/4N/sample-003.json | 70 ++++ corpus/labels/4N/sample-004.json | 59 ++++ corpus/labels/4N/sample-005.json | 87 +++++ corpus/labels/4N/sample-006.json | 63 ++++ corpus/labels/4N/sample-007.json | 25 ++ corpus/labels/4T/AGFSR/sample-001.json | 86 +++++ corpus/labels/4T/AGFSR/sample-002.json | 86 +++++ corpus/labels/4T/AGFSR/sample-003.json | 25 ++ corpus/labels/4T/ETA/sample-001.json | 60 ++++ corpus/labels/4T/ETA/sample-002.json | 25 ++ corpus/labels/58/sample-001.json | 65 ++++ corpus/labels/58/sample-002.json | 25 ++ corpus/labels/5Z/Slash/sample-001.json | 32 ++ corpus/labels/5Z/Slash/sample-002.json | 63 ++++ corpus/labels/5Z/Slash/sample-003.json | 61 ++++ corpus/labels/5Z/Slash/sample-004.json | 70 ++++ corpus/labels/5Z/Slash/sample-005.json | 70 ++++ corpus/labels/5Z/Slash/sample-006.json | 53 +++ corpus/labels/5Z/Slash/sample-007.json | 63 ++++ corpus/labels/5Z/Slash/sample-008.json | 25 ++ corpus/labels/80/sample-001.json | 100 ++++++ corpus/labels/80/sample-002.json | 122 +++++++ corpus/labels/80/sample-003.json | 107 ++++++ corpus/labels/80/sample-004.json | 65 ++++ corpus/labels/80/sample-005.json | 62 ++++ corpus/labels/80/sample-006.json | 69 ++++ corpus/labels/80/sample-007.json | 23 ++ corpus/labels/80/sample-008.json | 25 ++ corpus/labels/80/sample-009.json | 62 ++++ corpus/labels/83/sample-001.json | 74 +++++ corpus/labels/83/sample-002.json | 45 +++ corpus/labels/83/sample-003.json | 74 +++++ corpus/labels/83/sample-004.json | 56 ++++ corpus/labels/83/sample-005.json | 45 +++ corpus/labels/83/sample-006.json | 25 ++ corpus/labels/8E/sample-001.json | 39 +++ corpus/labels/H1/ATIS/sample-001.json | 53 +++ corpus/labels/H1/ATIS/sample-002.json | 53 +++ corpus/labels/H1/ATIS/sample-003.json | 53 +++ corpus/labels/H1/ATIS/sample-004.json | 53 +++ corpus/labels/H1/ATIS/sample-005.json | 25 ++ corpus/labels/H1/ATIS/sample-006.json | 25 ++ corpus/labels/H1/EZF/sample-001.json | 120 +++++++ corpus/labels/H1/EZF/sample-002.json | 70 ++++ corpus/labels/H1/EZF/sample-003.json | 63 ++++ corpus/labels/H1/EZF/sample-004.json | 25 ++ corpus/labels/H1/EZF/sample-005.json | 25 ++ corpus/labels/H1/FLR/sample-001.json | 35 ++ corpus/labels/H1/FLR/sample-002.json | 35 ++ corpus/labels/H1/FLR/sample-003.json | 35 ++ corpus/labels/H1/FLR/sample-004.json | 35 ++ corpus/labels/H1/FLR/sample-005.json | 35 ++ corpus/labels/H1/FLR/sample-006.json | 25 ++ corpus/labels/H1/M_POS/sample-001.json | 86 +++++ corpus/labels/H1/M_POS/sample-002.json | 86 +++++ corpus/labels/H1/M_POS/sample-003.json | 86 +++++ corpus/labels/H1/M_POS/sample-004.json | 25 ++ corpus/labels/H1/M_POS/sample-005.json | 25 ++ corpus/labels/H1/M_POS/sample-006.json | 26 ++ corpus/labels/H1/M_POS/sample-007.json | 26 ++ corpus/labels/H1/OFP/sample-001.json | 26 ++ corpus/labels/H1/OFP/sample-002.json | 164 +++++++++ corpus/labels/H1/OFP/sample-003.json | 145 ++++++++ corpus/labels/H1/OFP/sample-004.json | 25 ++ corpus/labels/H1/OFP/sample-005.json | 145 ++++++++ corpus/labels/H1/OFP/sample-006.json | 26 ++ corpus/labels/H1/OHMA/sample-001.json | 32 ++ corpus/labels/H1/OHMA/sample-002.json | 32 ++ corpus/labels/H1/OHMA/sample-003.json | 32 ++ corpus/labels/H1/OHMA/sample-004.json | 25 ++ corpus/labels/H1/Paren/sample-001.json | 72 ++++ corpus/labels/H1/Paren/sample-002.json | 23 ++ corpus/labels/H1/Paren/sample-003.json | 23 ++ corpus/labels/H1/WRN/sample-001.json | 35 ++ corpus/labels/H1/WRN/sample-002.json | 35 ++ corpus/labels/H1/WRN/sample-003.json | 35 ++ corpus/labels/H1/WRN/sample-004.json | 35 ++ corpus/labels/H1/WRN/sample-005.json | 35 ++ corpus/labels/H1/WRN/sample-006.json | 25 ++ corpus/labels/H2/02E/sample-001.json | 176 ++++++++++ corpus/labels/H2/02E/sample-002.json | 155 +++++++++ corpus/labels/H2/02E/sample-003.json | 134 ++++++++ corpus/labels/H2/02E/sample-004.json | 25 ++ corpus/labels/HX/sample-001.json | 37 +++ corpus/labels/HX/sample-002.json | 34 ++ corpus/labels/HX/sample-003.json | 23 ++ corpus/labels/MA/sample-001.json | 55 ++++ corpus/labels/MA/sample-002.json | 48 +++ corpus/labels/MA/sample-003.json | 75 +++++ corpus/labels/MA/sample-004.json | 55 ++++ corpus/labels/QQ/sample-001.json | 66 ++++ corpus/labels/QQ/sample-002.json | 59 ++++ corpus/labels/QQ/sample-003.json | 48 +++ corpus/labels/SQ/sample-001.json | 80 +++++ corpus/labels/SQ/sample-002.json | 40 +++ corpus/labels/SQ/sample-003.json | 40 +++ corpus/wildcards/arinc_702/sample-001.json | 142 ++++++++ corpus/wildcards/arinc_702/sample-002.json | 142 ++++++++ corpus/wildcards/arinc_702/sample-003.json | 25 ++ corpus/wildcards/arinc_702/sample-004.json | 25 ++ corpus/wildcards/arinc_702/sample-005.json | 25 ++ corpus/wildcards/arinc_702/sample-006.json | 25 ++ corpus/wildcards/arinc_702/sample-007.json | 25 ++ corpus/wildcards/arinc_702/sample-008.json | 25 ++ corpus/wildcards/arinc_702/sample-009.json | 25 ++ corpus/wildcards/arinc_702/sample-010.json | 25 ++ corpus/wildcards/arinc_702/sample-011.json | 25 ++ corpus/wildcards/arinc_702/sample-012.json | 25 ++ corpus/wildcards/arinc_702/sample-013.json | 25 ++ corpus/wildcards/arinc_702/sample-014.json | 25 ++ corpus/wildcards/arinc_702/sample-015.json | 25 ++ corpus/wildcards/arinc_702/sample-016.json | 25 ++ corpus/wildcards/arinc_702/sample-017.json | 64 ++++ corpus/wildcards/arinc_702/sample-018.json | 64 ++++ corpus/wildcards/arinc_702/sample-019.json | 25 ++ corpus/wildcards/arinc_702/sample-020.json | 64 ++++ corpus/wildcards/arinc_702/sample-021.json | 79 +++++ corpus/wildcards/arinc_702/sample-022.json | 25 ++ corpus/wildcards/arinc_702/sample-023.json | 57 ++++ corpus/wildcards/arinc_702/sample-024.json | 56 ++++ corpus/wildcards/arinc_702/sample-025.json | 35 ++ corpus/wildcards/arinc_702/sample-026.json | 35 ++ corpus/wildcards/arinc_702/sample-027.json | 92 ++++++ corpus/wildcards/arinc_702/sample-028.json | 92 ++++++ corpus/wildcards/arinc_702/sample-029.json | 25 ++ corpus/wildcards/arinc_702/sample-030.json | 71 ++++ corpus/wildcards/arinc_702/sample-031.json | 63 ++++ corpus/wildcards/arinc_702/sample-032.json | 72 ++++ corpus/wildcards/arinc_702/sample-033.json | 121 +++++++ corpus/wildcards/arinc_702/sample-034.json | 73 ++++ corpus/wildcards/arinc_702/sample-035.json | 25 ++ corpus/wildcards/arinc_702/sample-036.json | 35 ++ corpus/wildcards/arinc_702/sample-037.json | 43 +++ corpus/wildcards/arinc_702/sample-038.json | 36 ++ corpus/wildcards/arinc_702/sample-039.json | 35 ++ corpus/wildcards/arinc_702/sample-040.json | 124 +++++++ corpus/wildcards/arinc_702/sample-041.json | 25 ++ corpus/wildcards/arinc_702/sample-042.json | 49 +++ corpus/wildcards/arinc_702/sample-043.json | 49 +++ corpus/wildcards/arinc_702/sample-044.json | 25 ++ corpus/wildcards/arinc_702/sample-045.json | 26 ++ corpus/wildcards/arinc_702/sample-046.json | 25 ++ corpus/wildcards/arinc_702/sample-047.json | 25 ++ corpus/wildcards/arinc_702/sample-048.json | 26 ++ corpus/wildcards/arinc_702/sample-049.json | 25 ++ corpus/wildcards/arinc_702/sample-050.json | 81 +++++ corpus/wildcards/arinc_702/sample-051.json | 85 +++++ corpus/wildcards/arinc_702/sample-052.json | 88 +++++ corpus/wildcards/arinc_702/sample-053.json | 81 +++++ corpus/wildcards/arinc_702/sample-054.json | 25 ++ corpus/wildcards/arinc_702/sample-055.json | 81 +++++ corpus/wildcards/arinc_702/sample-056.json | 80 +++++ corpus/wildcards/arinc_702/sample-057.json | 81 +++++ corpus/wildcards/arinc_702/sample-058.json | 80 +++++ corpus/wildcards/arinc_702/sample-059.json | 80 +++++ corpus/wildcards/arinc_702/sample-060.json | 109 ++++++ corpus/wildcards/arinc_702/sample-061.json | 25 ++ corpus/wildcards/arinc_702/sample-062.json | 80 +++++ corpus/wildcards/arinc_702/sample-063.json | 152 +++++++++ corpus/wildcards/arinc_702/sample-064.json | 165 ++++++++++ corpus/wildcards/arinc_702/sample-065.json | 92 ++++++ corpus/wildcards/arinc_702/sample-066.json | 81 +++++ corpus/wildcards/arinc_702/sample-067.json | 141 ++++++++ corpus/wildcards/arinc_702/sample-068.json | 85 +++++ corpus/wildcards/arinc_702/sample-069.json | 104 ++++++ corpus/wildcards/arinc_702/sample-070.json | 25 ++ corpus/wildcards/arinc_702/sample-071.json | 79 +++++ corpus/wildcards/arinc_702/sample-072.json | 153 +++++++++ corpus/wildcards/arinc_702/sample-073.json | 195 +++++++++++ corpus/wildcards/arinc_702/sample-074.json | 96 ++++++ corpus/wildcards/arinc_702/sample-075.json | 111 +++++++ corpus/wildcards/arinc_702/sample-076.json | 67 ++++ corpus/wildcards/arinc_702/sample-077.json | 148 +++++++++ corpus/wildcards/arinc_702/sample-078.json | 143 ++++++++ corpus/wildcards/arinc_702/sample-079.json | 153 +++++++++ corpus/wildcards/arinc_702/sample-080.json | 149 +++++++++ corpus/wildcards/arinc_702/sample-081.json | 25 ++ corpus/wildcards/arinc_702/sample-082.json | 143 ++++++++ corpus/wildcards/arinc_702/sample-083.json | 35 ++ corpus/wildcards/arinc_702/sample-084.json | 311 ++++++++++++++++++ corpus/wildcards/arinc_702/sample-085.json | 229 +++++++++++++ corpus/wildcards/arinc_702/sample-086.json | 25 ++ corpus/wildcards/arinc_702/sample-087.json | 163 +++++++++ corpus/wildcards/arinc_702/sample-088.json | 25 ++ corpus/wildcards/arinc_702/sample-089.json | 36 ++ corpus/wildcards/arinc_702/sample-090.json | 35 ++ corpus/wildcards/arinc_702/sample-091.json | 79 +++++ corpus/wildcards/arinc_702/sample-092.json | 122 +++++++ corpus/wildcards/arinc_702/sample-093.json | 35 ++ corpus/wildcards/arinc_702/sample-094.json | 57 ++++ corpus/wildcards/arinc_702/sample-095.json | 56 ++++ 288 files changed, 17129 insertions(+), 6 deletions(-) create mode 100644 corpus/labels/10/LDR/sample-001.json create mode 100644 corpus/labels/10/LDR/sample-002.json create mode 100644 corpus/labels/10/LDR/sample-003.json rename corpus/labels/10/POS/{sample-002-invalid.json => sample-002.json} (50%) create mode 100644 corpus/labels/10/Slash/sample-001.json create mode 100644 corpus/labels/10/Slash/sample-002.json create mode 100644 corpus/labels/10/Slash/sample-003.json create mode 100644 corpus/labels/12/N_Space/sample-001.json create mode 100644 corpus/labels/12/N_Space/sample-002.json create mode 100644 corpus/labels/12/N_Space/sample-003.json create mode 100644 corpus/labels/12/POS/sample-001.json create mode 100644 corpus/labels/12/POS/sample-002.json create mode 100644 corpus/labels/15/FST/sample-001.json create mode 100644 corpus/labels/15/FST/sample-002.json create mode 100644 corpus/labels/15/Paren2/sample-001.json create mode 100644 corpus/labels/15/Paren2/sample-002.json create mode 100644 corpus/labels/15/Paren2/sample-003.json create mode 100644 corpus/labels/15/Paren2/sample-004.json create mode 100644 corpus/labels/15/Paren2/sample-005.json create mode 100644 corpus/labels/15/Paren2/sample-006.json create mode 100644 corpus/labels/15/Paren2/sample-007.json create mode 100644 corpus/labels/16/AUTPOS/sample-001.json create mode 100644 corpus/labels/16/AUTPOS/sample-002.json create mode 100644 corpus/labels/16/AUTPOS/sample-003.json create mode 100644 corpus/labels/16/Honeywell/sample-001.json create mode 100644 corpus/labels/16/Honeywell/sample-002.json create mode 100644 corpus/labels/16/Honeywell/sample-003.json create mode 100644 corpus/labels/16/Honeywell/sample-004.json create mode 100644 corpus/labels/16/N_Space/sample-001.json create mode 100644 corpus/labels/16/N_Space/sample-002.json create mode 100644 corpus/labels/16/N_Space/sample-003.json create mode 100644 corpus/labels/16/N_Space/sample-004.json create mode 100644 corpus/labels/16/POSA1/sample-001.json create mode 100644 corpus/labels/16/POSA1/sample-002.json create mode 100644 corpus/labels/16/POSA1/sample-003.json create mode 100644 corpus/labels/16/TOD/sample-001.json create mode 100644 corpus/labels/16/TOD/sample-002.json create mode 100644 corpus/labels/16/TOD/sample-003.json create mode 100644 corpus/labels/16/TOD/sample-004.json create mode 100644 corpus/labels/16/TOD/sample-005.json create mode 100644 corpus/labels/1L/070/sample-001.json create mode 100644 corpus/labels/1L/070/sample-002.json create mode 100644 corpus/labels/1L/660/sample-001.json create mode 100644 corpus/labels/1L/660/sample-002.json create mode 100644 corpus/labels/1M/Slash/sample-001.json create mode 100644 corpus/labels/20/POS/sample-001.json create mode 100644 corpus/labels/20/POS/sample-002.json create mode 100644 corpus/labels/20/POS/sample-003.json create mode 100644 corpus/labels/21/POS/sample-001.json create mode 100644 corpus/labels/21/POS/sample-002.json create mode 100644 corpus/labels/22/OFF/sample-001.json create mode 100644 corpus/labels/22/OFF/sample-002.json create mode 100644 corpus/labels/22/OFF/sample-003.json create mode 100644 corpus/labels/22/OFF/sample-004.json create mode 100644 corpus/labels/22/POS/sample-001.json create mode 100644 corpus/labels/22/POS/sample-002.json create mode 100644 corpus/labels/24/Slash/sample-001.json create mode 100644 corpus/labels/24/Slash/sample-002.json create mode 100644 corpus/labels/2P/FM3/sample-001.json create mode 100644 corpus/labels/2P/FM3/sample-002.json create mode 100644 corpus/labels/2P/FM3/sample-003.json create mode 100644 corpus/labels/2P/FM3/sample-004.json create mode 100644 corpus/labels/2P/FM4/sample-001.json create mode 100644 corpus/labels/2P/FM4/sample-002.json create mode 100644 corpus/labels/2P/FM4/sample-003.json create mode 100644 corpus/labels/2P/FM5/sample-001.json create mode 100644 corpus/labels/2P/FM5/sample-002.json create mode 100644 corpus/labels/30/Slash_EA/sample-001.json create mode 100644 corpus/labels/44/ETA/sample-001.json create mode 100644 corpus/labels/44/ETA/sample-002.json create mode 100644 corpus/labels/44/IN/sample-001.json create mode 100644 corpus/labels/44/IN/sample-002.json create mode 100644 corpus/labels/44/IN/sample-003.json create mode 100644 corpus/labels/44/OFF/sample-001.json create mode 100644 corpus/labels/44/OFF/sample-002.json create mode 100644 corpus/labels/44/ON/sample-001.json create mode 100644 corpus/labels/44/ON/sample-002.json create mode 100644 corpus/labels/44/ON/sample-003.json create mode 100644 corpus/labels/44/ON/sample-004.json create mode 100644 corpus/labels/44/POS/sample-001.json create mode 100644 corpus/labels/44/POS/sample-002.json create mode 100644 corpus/labels/44/Slash/sample-001.json create mode 100644 corpus/labels/44/Slash/sample-002.json create mode 100644 corpus/labels/44/Slash/sample-003.json create mode 100644 corpus/labels/4A/01/sample-001.json create mode 100644 corpus/labels/4A/DIS/sample-001.json create mode 100644 corpus/labels/4A/DOOR/sample-001.json create mode 100644 corpus/labels/4A/Slash_01/sample-001.json create mode 100644 corpus/labels/4A/sample-001.json create mode 100644 corpus/labels/4A/sample-002.json create mode 100644 corpus/labels/4A/sample-003.json create mode 100644 corpus/labels/4A/sample-004.json create mode 100644 corpus/labels/4A/sample-005.json create mode 100644 corpus/labels/4A/sample-006.json create mode 100644 corpus/labels/4N/sample-001.json create mode 100644 corpus/labels/4N/sample-002.json create mode 100644 corpus/labels/4N/sample-003.json create mode 100644 corpus/labels/4N/sample-004.json create mode 100644 corpus/labels/4N/sample-005.json create mode 100644 corpus/labels/4N/sample-006.json create mode 100644 corpus/labels/4N/sample-007.json create mode 100644 corpus/labels/4T/AGFSR/sample-001.json create mode 100644 corpus/labels/4T/AGFSR/sample-002.json create mode 100644 corpus/labels/4T/AGFSR/sample-003.json create mode 100644 corpus/labels/4T/ETA/sample-001.json create mode 100644 corpus/labels/4T/ETA/sample-002.json create mode 100644 corpus/labels/58/sample-001.json create mode 100644 corpus/labels/58/sample-002.json create mode 100644 corpus/labels/5Z/Slash/sample-001.json create mode 100644 corpus/labels/5Z/Slash/sample-002.json create mode 100644 corpus/labels/5Z/Slash/sample-003.json create mode 100644 corpus/labels/5Z/Slash/sample-004.json create mode 100644 corpus/labels/5Z/Slash/sample-005.json create mode 100644 corpus/labels/5Z/Slash/sample-006.json create mode 100644 corpus/labels/5Z/Slash/sample-007.json create mode 100644 corpus/labels/5Z/Slash/sample-008.json create mode 100644 corpus/labels/80/sample-001.json create mode 100644 corpus/labels/80/sample-002.json create mode 100644 corpus/labels/80/sample-003.json create mode 100644 corpus/labels/80/sample-004.json create mode 100644 corpus/labels/80/sample-005.json create mode 100644 corpus/labels/80/sample-006.json create mode 100644 corpus/labels/80/sample-007.json create mode 100644 corpus/labels/80/sample-008.json create mode 100644 corpus/labels/80/sample-009.json create mode 100644 corpus/labels/83/sample-001.json create mode 100644 corpus/labels/83/sample-002.json create mode 100644 corpus/labels/83/sample-003.json create mode 100644 corpus/labels/83/sample-004.json create mode 100644 corpus/labels/83/sample-005.json create mode 100644 corpus/labels/83/sample-006.json create mode 100644 corpus/labels/8E/sample-001.json create mode 100644 corpus/labels/H1/ATIS/sample-001.json create mode 100644 corpus/labels/H1/ATIS/sample-002.json create mode 100644 corpus/labels/H1/ATIS/sample-003.json create mode 100644 corpus/labels/H1/ATIS/sample-004.json create mode 100644 corpus/labels/H1/ATIS/sample-005.json create mode 100644 corpus/labels/H1/ATIS/sample-006.json create mode 100644 corpus/labels/H1/EZF/sample-001.json create mode 100644 corpus/labels/H1/EZF/sample-002.json create mode 100644 corpus/labels/H1/EZF/sample-003.json create mode 100644 corpus/labels/H1/EZF/sample-004.json create mode 100644 corpus/labels/H1/EZF/sample-005.json create mode 100644 corpus/labels/H1/FLR/sample-001.json create mode 100644 corpus/labels/H1/FLR/sample-002.json create mode 100644 corpus/labels/H1/FLR/sample-003.json create mode 100644 corpus/labels/H1/FLR/sample-004.json create mode 100644 corpus/labels/H1/FLR/sample-005.json create mode 100644 corpus/labels/H1/FLR/sample-006.json create mode 100644 corpus/labels/H1/M_POS/sample-001.json create mode 100644 corpus/labels/H1/M_POS/sample-002.json create mode 100644 corpus/labels/H1/M_POS/sample-003.json create mode 100644 corpus/labels/H1/M_POS/sample-004.json create mode 100644 corpus/labels/H1/M_POS/sample-005.json create mode 100644 corpus/labels/H1/M_POS/sample-006.json create mode 100644 corpus/labels/H1/M_POS/sample-007.json create mode 100644 corpus/labels/H1/OFP/sample-001.json create mode 100644 corpus/labels/H1/OFP/sample-002.json create mode 100644 corpus/labels/H1/OFP/sample-003.json create mode 100644 corpus/labels/H1/OFP/sample-004.json create mode 100644 corpus/labels/H1/OFP/sample-005.json create mode 100644 corpus/labels/H1/OFP/sample-006.json create mode 100644 corpus/labels/H1/OHMA/sample-001.json create mode 100644 corpus/labels/H1/OHMA/sample-002.json create mode 100644 corpus/labels/H1/OHMA/sample-003.json create mode 100644 corpus/labels/H1/OHMA/sample-004.json create mode 100644 corpus/labels/H1/Paren/sample-001.json create mode 100644 corpus/labels/H1/Paren/sample-002.json create mode 100644 corpus/labels/H1/Paren/sample-003.json create mode 100644 corpus/labels/H1/WRN/sample-001.json create mode 100644 corpus/labels/H1/WRN/sample-002.json create mode 100644 corpus/labels/H1/WRN/sample-003.json create mode 100644 corpus/labels/H1/WRN/sample-004.json create mode 100644 corpus/labels/H1/WRN/sample-005.json create mode 100644 corpus/labels/H1/WRN/sample-006.json create mode 100644 corpus/labels/H2/02E/sample-001.json create mode 100644 corpus/labels/H2/02E/sample-002.json create mode 100644 corpus/labels/H2/02E/sample-003.json create mode 100644 corpus/labels/H2/02E/sample-004.json create mode 100644 corpus/labels/HX/sample-001.json create mode 100644 corpus/labels/HX/sample-002.json create mode 100644 corpus/labels/HX/sample-003.json create mode 100644 corpus/labels/MA/sample-001.json create mode 100644 corpus/labels/MA/sample-002.json create mode 100644 corpus/labels/MA/sample-003.json create mode 100644 corpus/labels/MA/sample-004.json create mode 100644 corpus/labels/QQ/sample-001.json create mode 100644 corpus/labels/QQ/sample-002.json create mode 100644 corpus/labels/QQ/sample-003.json create mode 100644 corpus/labels/SQ/sample-001.json create mode 100644 corpus/labels/SQ/sample-002.json create mode 100644 corpus/labels/SQ/sample-003.json create mode 100644 corpus/wildcards/arinc_702/sample-001.json create mode 100644 corpus/wildcards/arinc_702/sample-002.json create mode 100644 corpus/wildcards/arinc_702/sample-003.json create mode 100644 corpus/wildcards/arinc_702/sample-004.json create mode 100644 corpus/wildcards/arinc_702/sample-005.json create mode 100644 corpus/wildcards/arinc_702/sample-006.json create mode 100644 corpus/wildcards/arinc_702/sample-007.json create mode 100644 corpus/wildcards/arinc_702/sample-008.json create mode 100644 corpus/wildcards/arinc_702/sample-009.json create mode 100644 corpus/wildcards/arinc_702/sample-010.json create mode 100644 corpus/wildcards/arinc_702/sample-011.json create mode 100644 corpus/wildcards/arinc_702/sample-012.json create mode 100644 corpus/wildcards/arinc_702/sample-013.json create mode 100644 corpus/wildcards/arinc_702/sample-014.json create mode 100644 corpus/wildcards/arinc_702/sample-015.json create mode 100644 corpus/wildcards/arinc_702/sample-016.json create mode 100644 corpus/wildcards/arinc_702/sample-017.json create mode 100644 corpus/wildcards/arinc_702/sample-018.json create mode 100644 corpus/wildcards/arinc_702/sample-019.json create mode 100644 corpus/wildcards/arinc_702/sample-020.json create mode 100644 corpus/wildcards/arinc_702/sample-021.json create mode 100644 corpus/wildcards/arinc_702/sample-022.json create mode 100644 corpus/wildcards/arinc_702/sample-023.json create mode 100644 corpus/wildcards/arinc_702/sample-024.json create mode 100644 corpus/wildcards/arinc_702/sample-025.json create mode 100644 corpus/wildcards/arinc_702/sample-026.json create mode 100644 corpus/wildcards/arinc_702/sample-027.json create mode 100644 corpus/wildcards/arinc_702/sample-028.json create mode 100644 corpus/wildcards/arinc_702/sample-029.json create mode 100644 corpus/wildcards/arinc_702/sample-030.json create mode 100644 corpus/wildcards/arinc_702/sample-031.json create mode 100644 corpus/wildcards/arinc_702/sample-032.json create mode 100644 corpus/wildcards/arinc_702/sample-033.json create mode 100644 corpus/wildcards/arinc_702/sample-034.json create mode 100644 corpus/wildcards/arinc_702/sample-035.json create mode 100644 corpus/wildcards/arinc_702/sample-036.json create mode 100644 corpus/wildcards/arinc_702/sample-037.json create mode 100644 corpus/wildcards/arinc_702/sample-038.json create mode 100644 corpus/wildcards/arinc_702/sample-039.json create mode 100644 corpus/wildcards/arinc_702/sample-040.json create mode 100644 corpus/wildcards/arinc_702/sample-041.json create mode 100644 corpus/wildcards/arinc_702/sample-042.json create mode 100644 corpus/wildcards/arinc_702/sample-043.json create mode 100644 corpus/wildcards/arinc_702/sample-044.json create mode 100644 corpus/wildcards/arinc_702/sample-045.json create mode 100644 corpus/wildcards/arinc_702/sample-046.json create mode 100644 corpus/wildcards/arinc_702/sample-047.json create mode 100644 corpus/wildcards/arinc_702/sample-048.json create mode 100644 corpus/wildcards/arinc_702/sample-049.json create mode 100644 corpus/wildcards/arinc_702/sample-050.json create mode 100644 corpus/wildcards/arinc_702/sample-051.json create mode 100644 corpus/wildcards/arinc_702/sample-052.json create mode 100644 corpus/wildcards/arinc_702/sample-053.json create mode 100644 corpus/wildcards/arinc_702/sample-054.json create mode 100644 corpus/wildcards/arinc_702/sample-055.json create mode 100644 corpus/wildcards/arinc_702/sample-056.json create mode 100644 corpus/wildcards/arinc_702/sample-057.json create mode 100644 corpus/wildcards/arinc_702/sample-058.json create mode 100644 corpus/wildcards/arinc_702/sample-059.json create mode 100644 corpus/wildcards/arinc_702/sample-060.json create mode 100644 corpus/wildcards/arinc_702/sample-061.json create mode 100644 corpus/wildcards/arinc_702/sample-062.json create mode 100644 corpus/wildcards/arinc_702/sample-063.json create mode 100644 corpus/wildcards/arinc_702/sample-064.json create mode 100644 corpus/wildcards/arinc_702/sample-065.json create mode 100644 corpus/wildcards/arinc_702/sample-066.json create mode 100644 corpus/wildcards/arinc_702/sample-067.json create mode 100644 corpus/wildcards/arinc_702/sample-068.json create mode 100644 corpus/wildcards/arinc_702/sample-069.json create mode 100644 corpus/wildcards/arinc_702/sample-070.json create mode 100644 corpus/wildcards/arinc_702/sample-071.json create mode 100644 corpus/wildcards/arinc_702/sample-072.json create mode 100644 corpus/wildcards/arinc_702/sample-073.json create mode 100644 corpus/wildcards/arinc_702/sample-074.json create mode 100644 corpus/wildcards/arinc_702/sample-075.json create mode 100644 corpus/wildcards/arinc_702/sample-076.json create mode 100644 corpus/wildcards/arinc_702/sample-077.json create mode 100644 corpus/wildcards/arinc_702/sample-078.json create mode 100644 corpus/wildcards/arinc_702/sample-079.json create mode 100644 corpus/wildcards/arinc_702/sample-080.json create mode 100644 corpus/wildcards/arinc_702/sample-081.json create mode 100644 corpus/wildcards/arinc_702/sample-082.json create mode 100644 corpus/wildcards/arinc_702/sample-083.json create mode 100644 corpus/wildcards/arinc_702/sample-084.json create mode 100644 corpus/wildcards/arinc_702/sample-085.json create mode 100644 corpus/wildcards/arinc_702/sample-086.json create mode 100644 corpus/wildcards/arinc_702/sample-087.json create mode 100644 corpus/wildcards/arinc_702/sample-088.json create mode 100644 corpus/wildcards/arinc_702/sample-089.json create mode 100644 corpus/wildcards/arinc_702/sample-090.json create mode 100644 corpus/wildcards/arinc_702/sample-091.json create mode 100644 corpus/wildcards/arinc_702/sample-092.json create mode 100644 corpus/wildcards/arinc_702/sample-093.json create mode 100644 corpus/wildcards/arinc_702/sample-094.json create mode 100644 corpus/wildcards/arinc_702/sample-095.json diff --git a/corpus/labels/10/LDR/sample-001.json b/corpus/labels/10/LDR/sample-001.json new file mode 100644 index 0000000..2c50344 --- /dev/null +++ b/corpus/labels/10/LDR/sample-001.json @@ -0,0 +1,72 @@ +{ + "spec": "labels/10/LDR", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "10", + "text": "LDR01,189,C,SWA-2600-016,0,N 38.151,W 76.623,37003, 10.2,KATL,KLGA,KLGA,22/,/,/,0,0,,,,,,,0,0,0,00,,135.1,08.6,143.7,,," + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-10-ldr", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "38.151 N, 76.623 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "37003 feet" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KATL" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KLGA" + }, + { + "type": "icao", + "code": "ALT_DST", + "label": "Alternate Destination", + "value": "KLGA" + }, + { + "type": "runway", + "code": "ARWY", + "label": "Arrival Runway", + "value": "22" + } + ] + }, + "raw": { + "position": { + "latitude": 38.151, + "longitude": -76.623 + }, + "altitude": 37003, + "departure_icao": "KATL", + "arrival_icao": "KLGA", + "alternate_icao": "KLGA", + "arrival_runway": "22" + }, + "remaining": { + "text": "LDR01,189,C,SWA-2600-016,0,0,0,,,,,,,0,0,0,00,,135.1,08.6,143.7,,," + } + } +} diff --git a/corpus/labels/10/LDR/sample-002.json b/corpus/labels/10/LDR/sample-002.json new file mode 100644 index 0000000..4fe6680 --- /dev/null +++ b/corpus/labels/10/LDR/sample-002.json @@ -0,0 +1,79 @@ +{ + "spec": "labels/10/LDR", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "10", + "text": "LDR01,189,C,SWA-2600-016,0,N 37.873,W 79.541,30998, 16.6,KBNA,KBOS,KBOS,27/,33L/,22L/,0,1,,,,,,,0,0,0,00,,131.2,11.4,142.6,,," + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-10-ldr", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "37.873 N, 79.541 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "30998 feet" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KBNA" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KBOS" + }, + { + "type": "icao", + "code": "ALT_DST", + "label": "Alternate Destination", + "value": "KBOS" + }, + { + "type": "runway", + "code": "ARWY", + "label": "Arrival Runway", + "value": "27" + }, + { + "type": "runway", + "code": "ALT_ARWY", + "label": "Alternate Runway", + "value": "33L,22L" + } + ] + }, + "raw": { + "position": { + "latitude": 37.873, + "longitude": -79.541 + }, + "altitude": 30998, + "departure_icao": "KBNA", + "arrival_icao": "KBOS", + "alternate_icao": "KBOS", + "arrival_runway": "27", + "alternate_runway": "33L,22L" + }, + "remaining": { + "text": "LDR01,189,C,SWA-2600-016,0,0,1,,,,,,,0,0,0,00,,131.2,11.4,142.6,,," + } + } +} diff --git a/corpus/labels/10/LDR/sample-003.json b/corpus/labels/10/LDR/sample-003.json new file mode 100644 index 0000000..da94cfd --- /dev/null +++ b/corpus/labels/10/LDR/sample-003.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/10/LDR", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "10", + "text": "LDR Bogus Message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-10-ldr", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Position Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "LDR Bogus Message" + } + } +} diff --git a/corpus/labels/10/POS/sample-001.json b/corpus/labels/10/POS/sample-001.json index c5108be..a421203 100644 --- a/corpus/labels/10/POS/sample-001.json +++ b/corpus/labels/10/POS/sample-001.json @@ -1,7 +1,7 @@ { "spec": "labels/10/POS", - "source": "acars-decoder-typescript/lib/plugins/Label_10_POS.test.ts", - "description": "Label 10 Preamble POS variant 1 — successful decode with partial level", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", "input": { "label": "10", "text": "POS082150, N 3885,W 7841,---,308,26922, 51,22290, 529, 19,-225,6" @@ -18,7 +18,7 @@ "items": [ { "type": "aircraft_position", - "code": "ARP", + "code": "POS", "label": "Aircraft Position", "value": "38.850 N, 78.410 W" }, @@ -30,6 +30,13 @@ } ] }, + "raw": { + "position": { + "latitude": 38.85, + "longitude": -78.41 + }, + "altitude": 22290 + }, "remaining": { "text": "POS082150,---,308,26922, 51, 529, 19,-225,6" } diff --git a/corpus/labels/10/POS/sample-002-invalid.json b/corpus/labels/10/POS/sample-002.json similarity index 50% rename from corpus/labels/10/POS/sample-002-invalid.json rename to corpus/labels/10/POS/sample-002.json index a986602..b35a533 100644 --- a/corpus/labels/10/POS/sample-002-invalid.json +++ b/corpus/labels/10/POS/sample-002.json @@ -1,7 +1,7 @@ { "spec": "labels/10/POS", - "source": "acars-decoder-typescript/lib/plugins/Label_10_POS.test.ts", - "description": "Label 10 invalid input — fails decode with 'none' level", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", "input": { "label": "10", "text": "POS Bogus Message" @@ -14,7 +14,12 @@ "decodeLevel": "none" }, "formatted": { - "description": "Position Report" + "description": "Position Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "POS Bogus Message" } } } diff --git a/corpus/labels/10/Slash/sample-001.json b/corpus/labels/10/Slash/sample-001.json new file mode 100644 index 0000000..3b4a855 --- /dev/null +++ b/corpus/labels/10/Slash/sample-001.json @@ -0,0 +1,86 @@ +{ + "spec": "labels/10/Slash", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "10", + "text": "/N39.182/W077.217/10/0.42/180/055/KIAD/0004/0028/00015/MOWAT/HUSEL/2349/YACKK/2352/" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-10-slash", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "39.182 N, 77.217 W" + }, + { + "type": "heading", + "code": "HDG", + "label": "Heading", + "value": "180" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "5500 feet" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KIAD" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "00:04:00" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "MOWAT > HUSEL@23:49:00 > YACKK@23:52:00" + } + ] + }, + "raw": { + "position": { + "latitude": 39.182, + "longitude": -77.217 + }, + "heading": 180, + "altitude": 5500, + "arrival_icao": "KIAD", + "eta_time": 240, + "route": { + "waypoints": [ + { + "name": "MOWAT" + }, + { + "name": "HUSEL", + "time": 85740 + }, + { + "name": "YACKK", + "time": 85920 + } + ] + } + }, + "remaining": { + "text": "10/0.42/0028/00015" + } + } +} diff --git a/corpus/labels/10/Slash/sample-002.json b/corpus/labels/10/Slash/sample-002.json new file mode 100644 index 0000000..21db6b7 --- /dev/null +++ b/corpus/labels/10/Slash/sample-002.json @@ -0,0 +1,93 @@ +{ + "spec": "labels/10/Slash", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "10", + "text": "/N39.019/W078.468/10/0.83/246/400/KCVG/0155/0073/00018/COLNS/STEVY/0120/FAIIR/0126/KTEB/" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-10-slash", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "39.019 N, 78.468 W" + }, + { + "type": "heading", + "code": "HDG", + "label": "Heading", + "value": "246" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "40000 feet" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KCVG" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "01:55:00" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "COLNS > STEVY@01:20:00 > FAIIR@01:26:00" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KTEB" + } + ] + }, + "raw": { + "position": { + "latitude": 39.019, + "longitude": -78.468 + }, + "heading": 246, + "altitude": 40000, + "arrival_icao": "KCVG", + "eta_time": 6900, + "route": { + "waypoints": [ + { + "name": "COLNS" + }, + { + "name": "STEVY", + "time": 4800 + }, + { + "name": "FAIIR", + "time": 5160 + } + ] + }, + "departure_icao": "KTEB" + }, + "remaining": { + "text": "10/0.83/0073/00018/" + } + } +} diff --git a/corpus/labels/10/Slash/sample-003.json b/corpus/labels/10/Slash/sample-003.json new file mode 100644 index 0000000..eb34023 --- /dev/null +++ b/corpus/labels/10/Slash/sample-003.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/10/Slash", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "10", + "text": "/Bogus Message/" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-10-slash", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Position Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "/Bogus Message/" + } + } +} diff --git a/corpus/labels/12/N_Space/sample-001.json b/corpus/labels/12/N_Space/sample-001.json new file mode 100644 index 0000000..eaed6cc --- /dev/null +++ b/corpus/labels/12/N_Space/sample-001.json @@ -0,0 +1,44 @@ +{ + "spec": "labels/12/N_Space", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "12", + "text": "N 42.150,W121.187,39000,161859, 109,.C-GWSO,1742" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-12-n-space", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "42.150 N, 121.187 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "39000 feet" + } + ] + }, + "raw": { + "position": { + "latitude": 42.15, + "longitude": -121.187 + }, + "altitude": 39000 + }, + "remaining": { + "text": "161859,109,1742" + } + } +} diff --git a/corpus/labels/12/N_Space/sample-002.json b/corpus/labels/12/N_Space/sample-002.json new file mode 100644 index 0000000..a5a7db2 --- /dev/null +++ b/corpus/labels/12/N_Space/sample-002.json @@ -0,0 +1,44 @@ +{ + "spec": "labels/12/N_Space", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "12", + "text": "N 28.371,W 80.458,38000,170546, 100,.C-GVWJ,1736" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-12-n-space", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "28.371 N, 80.458 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "38000 feet" + } + ] + }, + "raw": { + "position": { + "latitude": 28.371, + "longitude": -80.458 + }, + "altitude": 38000 + }, + "remaining": { + "text": "170546,100,1736" + } + } +} diff --git a/corpus/labels/12/N_Space/sample-003.json b/corpus/labels/12/N_Space/sample-003.json new file mode 100644 index 0000000..e4b5557 --- /dev/null +++ b/corpus/labels/12/N_Space/sample-003.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/12/N_Space", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "12", + "text": "N Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-12-n-space", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Position Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "N Bogus message" + } + } +} diff --git a/corpus/labels/12/POS/sample-001.json b/corpus/labels/12/POS/sample-001.json new file mode 100644 index 0000000..716f83d --- /dev/null +++ b/corpus/labels/12/POS/sample-001.json @@ -0,0 +1,79 @@ +{ + "spec": "labels/12/POS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "12", + "text": "POSN 390104W 754601,-------,1244,1446,,- 4,23249 12,FOB 73,ETA 1303,KATL,KPHL," + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-12-pos", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "39.018 N, 75.767 W" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "12:44:00" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "14460 feet" + }, + { + "type": "fuel_on_board", + "code": "FOB", + "label": "Fuel On Board", + "value": "73" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "13:03:00" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KATL" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KPHL" + } + ] + }, + "raw": { + "position": { + "latitude": 39.01777777777777, + "longitude": -75.76694444444445 + }, + "message_timestamp": 45840, + "altitude": 14460, + "fuel_on_board": 73, + "eta_time": 46980, + "departure_icao": "KATL", + "arrival_icao": "KPHL" + }, + "remaining": { + "text": "-------,,- 4,23249 12," + } + } +} diff --git a/corpus/labels/12/POS/sample-002.json b/corpus/labels/12/POS/sample-002.json new file mode 100644 index 0000000..1cb5fb5 --- /dev/null +++ b/corpus/labels/12/POS/sample-002.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/12/POS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "12", + "text": "POS Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-12-pos", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Position Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "POS Bogus message" + } + } +} diff --git a/corpus/labels/15/FST/sample-001.json b/corpus/labels/15/FST/sample-001.json new file mode 100644 index 0000000..b6cc32f --- /dev/null +++ b/corpus/labels/15/FST/sample-001.json @@ -0,0 +1,58 @@ +{ + "spec": "labels/15/FST", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "15", + "text": "FST01EGKKKMCON373488W0756927380 156 495 M53C 4427422721045313002518521710" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-15-fst", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "37.349 N, 75.693 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "38000 feet" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "EGKK" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KMCO" + } + ] + }, + "raw": { + "position": { + "latitude": 37.3488, + "longitude": -75.6927 + }, + "altitude": 38000, + "departure_icao": "EGKK", + "arrival_icao": "KMCO" + }, + "remaining": { + "text": "156 495 M53C 4427422721045313002518521710" + } + } +} diff --git a/corpus/labels/15/FST/sample-002.json b/corpus/labels/15/FST/sample-002.json new file mode 100644 index 0000000..ed20199 --- /dev/null +++ b/corpus/labels/15/FST/sample-002.json @@ -0,0 +1,27 @@ +{ + "spec": "labels/15/FST", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "15", + "text": "INI Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-15-fst", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Position Report", + "items": [] + }, + "raw": { + "position": {} + }, + "remaining": { + "text": "INI Bogus message" + } + } +} diff --git a/corpus/labels/15/Paren2/sample-001.json b/corpus/labels/15/Paren2/sample-001.json new file mode 100644 index 0000000..34f9179 --- /dev/null +++ b/corpus/labels/15/Paren2/sample-001.json @@ -0,0 +1,51 @@ +{ + "spec": "labels/15/Paren2", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "15", + "text": "(2N38448W 77216--- 28 20 7(Z" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-15", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "38.747 N, 77.360 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "2000 feet" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "7 degrees" + } + ] + }, + "raw": { + "position": { + "latitude": 38.74666666666667, + "longitude": -77.36 + }, + "altitude": 2000, + "outside_air_temperature": 7 + }, + "remaining": { + "text": "--- 28" + } + } +} diff --git a/corpus/labels/15/Paren2/sample-002.json b/corpus/labels/15/Paren2/sample-002.json new file mode 100644 index 0000000..f6d7eb9 --- /dev/null +++ b/corpus/labels/15/Paren2/sample-002.json @@ -0,0 +1,51 @@ +{ + "spec": "labels/15/Paren2", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "15", + "text": "(2N40492W 77179248 99380-53(Z" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-15", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "40.820 N, 77.298 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "38000 feet" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-53 degrees" + } + ] + }, + "raw": { + "position": { + "latitude": 40.82, + "longitude": -77.29833333333333 + }, + "altitude": 38000, + "outside_air_temperature": -53 + }, + "remaining": { + "text": "248 99" + } + } +} diff --git a/corpus/labels/15/Paren2/sample-003.json b/corpus/labels/15/Paren2/sample-003.json new file mode 100644 index 0000000..6e91cf6 --- /dev/null +++ b/corpus/labels/15/Paren2/sample-003.json @@ -0,0 +1,44 @@ +{ + "spec": "labels/15/Paren2", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "15", + "text": "(2N39269W 77374--- 42---- 5(Z" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-15", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "39.448 N, 77.623 W" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-5 degrees" + } + ] + }, + "raw": { + "position": { + "latitude": 39.44833333333333, + "longitude": -77.62333333333333 + }, + "outside_air_temperature": -5 + }, + "remaining": { + "text": "--- 42" + } + } +} diff --git a/corpus/labels/15/Paren2/sample-004.json b/corpus/labels/15/Paren2/sample-004.json new file mode 100644 index 0000000..61ece50 --- /dev/null +++ b/corpus/labels/15/Paren2/sample-004.json @@ -0,0 +1,44 @@ +{ + "spec": "labels/15/Paren2", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 4", + "input": { + "label": "15", + "text": "(2N39018W 77284OFF11112418101313--------(Z" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-15", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "39.030 N, 77.473 W" + }, + { + "type": "time", + "code": "OFF", + "label": "Takeoff Time", + "value": "2024-11-11T18:10:00Z" + } + ] + }, + "raw": { + "position": { + "latitude": 39.03, + "longitude": -77.47333333333333 + }, + "off_time": 1731348600 + }, + "remaining": { + "text": "1313--------" + } + } +} diff --git a/corpus/labels/15/Paren2/sample-005.json b/corpus/labels/15/Paren2/sample-005.json new file mode 100644 index 0000000..dbb441d --- /dev/null +++ b/corpus/labels/15/Paren2/sample-005.json @@ -0,0 +1,44 @@ +{ + "spec": "labels/15/Paren2", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 5", + "input": { + "label": "15", + "text": "(2N42589W 83520OFF------13280606--------(Z" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-15", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "42.982 N, 83.867 W" + }, + { + "type": "time", + "code": "OFF", + "label": "Takeoff Time", + "value": "13:28:00" + } + ] + }, + "raw": { + "position": { + "latitude": 42.98166666666667, + "longitude": -83.86666666666666 + }, + "off_time": 48480 + }, + "remaining": { + "text": "0606--------" + } + } +} diff --git a/corpus/labels/15/Paren2/sample-006.json b/corpus/labels/15/Paren2/sample-006.json new file mode 100644 index 0000000..e815959 --- /dev/null +++ b/corpus/labels/15/Paren2/sample-006.json @@ -0,0 +1,44 @@ +{ + "spec": "labels/15/Paren2", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 6", + "input": { + "label": "15", + "text": "(2N39042W 77308OFF1311240327B1818 015(Z" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-15", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "39.070 N, 77.513 W" + }, + { + "type": "time", + "code": "OFF", + "label": "Takeoff Time", + "value": "2024-11-13T03:27:00Z" + } + ] + }, + "raw": { + "position": { + "latitude": 39.07, + "longitude": -77.51333333333334 + }, + "off_time": 1731468420 + }, + "remaining": { + "text": "B1818 015" + } + } +} diff --git a/corpus/labels/15/Paren2/sample-007.json b/corpus/labels/15/Paren2/sample-007.json new file mode 100644 index 0000000..ee99eb7 --- /dev/null +++ b/corpus/labels/15/Paren2/sample-007.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/15/Paren2", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 7", + "input": { + "label": "15", + "text": "(2 Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-15", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Position Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "(2 Bogus message" + } + } +} diff --git a/corpus/labels/16/AUTPOS/sample-001.json b/corpus/labels/16/AUTPOS/sample-001.json new file mode 100644 index 0000000..4b6085b --- /dev/null +++ b/corpus/labels/16/AUTPOS/sample-001.json @@ -0,0 +1,70 @@ +{ + "spec": "labels/16/AUTPOS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "16", + "text": "283806/AUTPOS/LLD N400547 W0774954\r\n/ALT 12932/SAT ****\r\n/WND ******/TAT ****/TAS ****/CRZ ***\r\n/FOB 065120\r\n/DAT 260228/TIM 150742" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-16-autpos", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "28" + }, + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "3806" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "40.096 N, 77.832 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "12932 feet" + }, + { + "type": "fuel_on_board", + "code": "FOB", + "label": "Fuel On Board", + "value": "65120" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "2026-02-28T15:07:42Z" + } + ] + }, + "raw": { + "day": 28, + "flight_number": "3806", + "position": { + "latitude": 40.09638888888889, + "longitude": -77.83166666666666 + }, + "altitude": 12932, + "fuel_on_board": 65120, + "message_timestamp": 1772291262 + }, + "remaining": {} + } +} diff --git a/corpus/labels/16/AUTPOS/sample-002.json b/corpus/labels/16/AUTPOS/sample-002.json new file mode 100644 index 0000000..91cc930 --- /dev/null +++ b/corpus/labels/16/AUTPOS/sample-002.json @@ -0,0 +1,114 @@ +{ + "spec": "labels/16/AUTPOS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "16", + "text": "289142/AUTPOS/LLD N395538 W0753341 \r\n/ALT 35000/SAT -057\r\n/WND 239065/TAT -027/TAS 476/CRZ 836\r\n/FOB 107600\r\n/DAT 260228/TIM 132714" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-16-autpos", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "28" + }, + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "9142" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "39.927 N, 75.561 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "35000 feet" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "POSITION at FL350: 239° at 65kt" + }, + { + "type": "temperature", + "code": "TATEMP", + "label": "Total Air Temperature (C)", + "value": "-27 degrees" + }, + { + "type": "airspeed", + "code": "ASPD", + "label": "True Airspeed", + "value": "476 knots" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-57 degrees" + }, + { + "type": "mach", + "code": "MACH", + "label": "Mach Number", + "value": "0.836 mach" + }, + { + "type": "fuel_on_board", + "code": "FOB", + "label": "Fuel On Board", + "value": "107600" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "2026-02-28T13:27:14Z" + } + ] + }, + "raw": { + "day": 28, + "flight_number": "9142", + "position": { + "latitude": 39.92722222222222, + "longitude": -75.56138888888889 + }, + "altitude": 35000, + "wind_data": [ + { + "waypoint": { + "name": "POSITION" + }, + "flightLevel": 350, + "windDirection": 239, + "windSpeed": 65 + } + ], + "total_air_temperature": -27, + "airspeed": 476, + "outside_air_temperature": -57, + "mach": 0.836, + "fuel_on_board": 107600, + "message_timestamp": 1772285234 + }, + "remaining": {} + } +} diff --git a/corpus/labels/16/AUTPOS/sample-003.json b/corpus/labels/16/AUTPOS/sample-003.json new file mode 100644 index 0000000..5595849 --- /dev/null +++ b/corpus/labels/16/AUTPOS/sample-003.json @@ -0,0 +1,23 @@ +{ + "spec": "labels/16/AUTPOS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "16", + "text": "invalid AUTPOS message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-16-autpos", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Unknown", + "items": [] + }, + "raw": {}, + "remaining": {} + } +} diff --git a/corpus/labels/16/Honeywell/sample-001.json b/corpus/labels/16/Honeywell/sample-001.json new file mode 100644 index 0000000..68598de --- /dev/null +++ b/corpus/labels/16/Honeywell/sample-001.json @@ -0,0 +1,51 @@ +{ + "spec": "labels/16/Honeywell", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "16", + "text": "(2AAABN39211W 77144KTEBMMTO-/A(Z" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-16-honeywell", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "39.352 N, 77.240 W" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KTEB" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "MMTO" + } + ] + }, + "raw": { + "position": { + "latitude": 39.35166666666667, + "longitude": -77.24 + }, + "departure_icao": "KTEB", + "arrival_icao": "MMTO" + }, + "remaining": { + "text": "AAAB/A" + } + } +} diff --git a/corpus/labels/16/Honeywell/sample-002.json b/corpus/labels/16/Honeywell/sample-002.json new file mode 100644 index 0000000..9c24a80 --- /dev/null +++ b/corpus/labels/16/Honeywell/sample-002.json @@ -0,0 +1,53 @@ +{ + "spec": "labels/16/Honeywell", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "16", + "text": "(2AAAAN37265W 78334-SSI /O(Z" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-16-honeywell", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "37.442 N, 78.557 W" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "SSI > /O" + } + ] + }, + "raw": { + "position": { + "latitude": 37.44166666666667, + "longitude": -78.55666666666667 + }, + "route": { + "waypoints": [ + { + "name": "SSI" + }, + { + "name": "/O" + } + ] + } + }, + "remaining": { + "text": "AAAA/O" + } + } +} diff --git a/corpus/labels/16/Honeywell/sample-003.json b/corpus/labels/16/Honeywell/sample-003.json new file mode 100644 index 0000000..fe39900 --- /dev/null +++ b/corpus/labels/16/Honeywell/sample-003.json @@ -0,0 +1,53 @@ +{ + "spec": "labels/16/Honeywell", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "16", + "text": "(2AAABN37197W 78404-SLOJOGRONK/O(Z" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-16-honeywell", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "37.328 N, 78.673 W" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "SLOJO > GRONK" + } + ] + }, + "raw": { + "position": { + "latitude": 37.32833333333333, + "longitude": -78.67333333333333 + }, + "route": { + "waypoints": [ + { + "name": "SLOJO" + }, + { + "name": "GRONK" + } + ] + } + }, + "remaining": { + "text": "AAAB/O" + } + } +} diff --git a/corpus/labels/16/Honeywell/sample-004.json b/corpus/labels/16/Honeywell/sample-004.json new file mode 100644 index 0000000..0db1be6 --- /dev/null +++ b/corpus/labels/16/Honeywell/sample-004.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/16/Honeywell", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 4", + "input": { + "label": "16", + "text": "(2 Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-16-honeywell", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Position Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "(2 Bogus message" + } + } +} diff --git a/corpus/labels/16/N_Space/sample-001.json b/corpus/labels/16/N_Space/sample-001.json new file mode 100644 index 0000000..58654b6 --- /dev/null +++ b/corpus/labels/16/N_Space/sample-001.json @@ -0,0 +1,44 @@ +{ + "spec": "labels/16/N_Space", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "16", + "text": "N 44.203,W 86.546,31965,6, 290" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-16-n-space", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "44.203 N, 86.546 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "31965 feet" + } + ] + }, + "raw": { + "position": { + "latitude": 44.203, + "longitude": -86.546 + }, + "altitude": 31965 + }, + "remaining": { + "text": "6,290" + } + } +} diff --git a/corpus/labels/16/N_Space/sample-002.json b/corpus/labels/16/N_Space/sample-002.json new file mode 100644 index 0000000..ada421e --- /dev/null +++ b/corpus/labels/16/N_Space/sample-002.json @@ -0,0 +1,35 @@ +{ + "spec": "labels/16/N_Space", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "16", + "text": "N 28.177/W 96.055" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-16-n-space", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "28.177 N, 96.055 W" + } + ] + }, + "raw": { + "position": { + "latitude": 28.177, + "longitude": -96.055 + } + }, + "remaining": {} + } +} diff --git a/corpus/labels/16/N_Space/sample-003.json b/corpus/labels/16/N_Space/sample-003.json new file mode 100644 index 0000000..7d46e78 --- /dev/null +++ b/corpus/labels/16/N_Space/sample-003.json @@ -0,0 +1,44 @@ +{ + "spec": "labels/16/N_Space", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "16", + "text": "N 44.988,W121.644,35940,6, 170" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-16-n-space", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "44.988 N, 121.644 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "35940 feet" + } + ] + }, + "raw": { + "position": { + "latitude": 44.988, + "longitude": -121.644 + }, + "altitude": 35940 + }, + "remaining": { + "text": "6,170" + } + } +} diff --git a/corpus/labels/16/N_Space/sample-004.json b/corpus/labels/16/N_Space/sample-004.json new file mode 100644 index 0000000..de6358c --- /dev/null +++ b/corpus/labels/16/N_Space/sample-004.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/16/N_Space", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 4", + "input": { + "label": "16", + "text": "N Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-16-n-space", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Position Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "N Bogus message" + } + } +} diff --git a/corpus/labels/16/POSA1/sample-001.json b/corpus/labels/16/POSA1/sample-001.json new file mode 100644 index 0000000..987de40 --- /dev/null +++ b/corpus/labels/16/POSA1/sample-001.json @@ -0,0 +1,62 @@ +{ + "spec": "labels/16/POSA1", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "16", + "text": "POSA1N37358W 77279,GEARS ,221626,370,BBOBO ,222053,,-61,139,1174,829" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-16-posa1", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "37.358 N, 77.279 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "37000 feet" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "GEARS@22:16:26 > BBOBO@22:20:53" + } + ] + }, + "raw": { + "position": { + "latitude": 37.358, + "longitude": -77.279 + }, + "altitude": 37000, + "route": { + "waypoints": [ + { + "name": "GEARS", + "time": 80186 + }, + { + "name": "BBOBO", + "time": 80453 + } + ] + } + }, + "remaining": { + "text": ",-61,139,1174,829" + } + } +} diff --git a/corpus/labels/16/POSA1/sample-002.json b/corpus/labels/16/POSA1/sample-002.json new file mode 100644 index 0000000..2a3a587 --- /dev/null +++ b/corpus/labels/16/POSA1/sample-002.json @@ -0,0 +1,62 @@ +{ + "spec": "labels/16/POSA1", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "16", + "text": "POSA1N38843W 78790,RONZZ ,005159,390,RAMAY ,010055,,*****,*****, 744, 0" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-16-posa1", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "38.843 N, 78.790 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "39000 feet" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "RONZZ@00:51:59 > RAMAY@01:00:55" + } + ] + }, + "raw": { + "position": { + "latitude": 38.843, + "longitude": -78.79 + }, + "altitude": 39000, + "route": { + "waypoints": [ + { + "name": "RONZZ", + "time": 3119 + }, + { + "name": "RAMAY", + "time": 3655 + } + ] + } + }, + "remaining": { + "text": ",*****,*****, 744, 0" + } + } +} diff --git a/corpus/labels/16/POSA1/sample-003.json b/corpus/labels/16/POSA1/sample-003.json new file mode 100644 index 0000000..14cc844 --- /dev/null +++ b/corpus/labels/16/POSA1/sample-003.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/16/POSA1", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "16", + "text": "N Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-16-posa1", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Position Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "N Bogus message" + } + } +} diff --git a/corpus/labels/16/TOD/sample-001.json b/corpus/labels/16/TOD/sample-001.json new file mode 100644 index 0000000..62a7201 --- /dev/null +++ b/corpus/labels/16/TOD/sample-001.json @@ -0,0 +1,58 @@ +{ + "spec": "labels/16/TOD", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "16", + "text": "005236,36787,0135, 97,N 38.364 W 75.226" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-16-tod", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "00:52:36" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "36787 feet" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "01:35:00" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "38.364 N, 75.226 W" + } + ] + }, + "raw": { + "message_timestamp": 3156, + "altitude": 36787, + "eta_time": 5700, + "position": { + "latitude": 38.364, + "longitude": -75.226 + } + }, + "remaining": { + "text": " 97" + } + } +} diff --git a/corpus/labels/16/TOD/sample-002.json b/corpus/labels/16/TOD/sample-002.json new file mode 100644 index 0000000..7956b60 --- /dev/null +++ b/corpus/labels/16/TOD/sample-002.json @@ -0,0 +1,65 @@ +{ + "spec": "labels/16/TOD", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "16", + "text": "110112,36000,1206, 51,N 45.140 E 16.341/SXS7SL" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-16-tod", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "11:01:12" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "36000 feet" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "12:06:00" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "45.140 N, 16.341 E" + }, + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "SXS7SL" + } + ] + }, + "raw": { + "message_timestamp": 39672, + "altitude": 36000, + "eta_time": 43560, + "position": { + "latitude": 45.14, + "longitude": 16.341 + }, + "flight_number": "SXS7SL" + }, + "remaining": { + "text": " 51" + } + } +} diff --git a/corpus/labels/16/TOD/sample-003.json b/corpus/labels/16/TOD/sample-003.json new file mode 100644 index 0000000..6eb0124 --- /dev/null +++ b/corpus/labels/16/TOD/sample-003.json @@ -0,0 +1,41 @@ +{ + "spec": "labels/16/TOD", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "16", + "text": "110122,,1206, 92,N . MMMM.MMM" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-16-tod", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "11:01:22" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "12:06:00" + } + ] + }, + "raw": { + "message_timestamp": 39682, + "eta_time": 43560 + }, + "remaining": { + "text": " 92" + } + } +} diff --git a/corpus/labels/16/TOD/sample-004.json b/corpus/labels/16/TOD/sample-004.json new file mode 100644 index 0000000..b6dbf07 --- /dev/null +++ b/corpus/labels/16/TOD/sample-004.json @@ -0,0 +1,58 @@ +{ + "spec": "labels/16/TOD", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 4", + "input": { + "label": "16", + "text": "001415,20274,0047, 3740,N3835.95 W07858.88" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-16-tod", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "00:14:15" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "20274 feet" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "00:47:00" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "38.359 N, 78.589 W" + } + ] + }, + "raw": { + "message_timestamp": 855, + "altitude": 20274, + "eta_time": 2820, + "position": { + "latitude": 38.3595, + "longitude": -78.5888 + } + }, + "remaining": { + "text": " 3740" + } + } +} diff --git a/corpus/labels/16/TOD/sample-005.json b/corpus/labels/16/TOD/sample-005.json new file mode 100644 index 0000000..a434494 --- /dev/null +++ b/corpus/labels/16/TOD/sample-005.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/16/TOD", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 5", + "input": { + "label": "16", + "text": "N Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-16-tod", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Position Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "N Bogus message" + } + } +} diff --git a/corpus/labels/1L/070/sample-001.json b/corpus/labels/1L/070/sample-001.json new file mode 100644 index 0000000..25599ac --- /dev/null +++ b/corpus/labels/1L/070/sample-001.json @@ -0,0 +1,65 @@ +{ + "spec": "labels/1L/070", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "1L", + "text": "000000070LOWW,KEWR,0932,1744,N 49.223,E 12.038,0659" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-1l-070", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "LOWW" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KEWR" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "09:32:00" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "17:44:00" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "49.223 N, 12.038 E" + } + ] + }, + "raw": { + "departure_icao": "LOWW", + "arrival_icao": "KEWR", + "message_timestamp": 34320, + "eta_time": 63840, + "position": { + "latitude": 49.223, + "longitude": 12.038 + } + }, + "remaining": { + "text": "0659" + } + } +} diff --git a/corpus/labels/1L/070/sample-002.json b/corpus/labels/1L/070/sample-002.json new file mode 100644 index 0000000..34b57ee --- /dev/null +++ b/corpus/labels/1L/070/sample-002.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/1L/070", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "1L", + "text": "POS Bogus Message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-1l-070", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Position Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "POS Bogus Message" + } + } +} diff --git a/corpus/labels/1L/660/sample-001.json b/corpus/labels/1L/660/sample-001.json new file mode 100644 index 0000000..dc91fe2 --- /dev/null +++ b/corpus/labels/1L/660/sample-001.json @@ -0,0 +1,64 @@ +{ + "spec": "labels/1L/660", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "1L", + "text": "000000660N50442E005566,100444359SOG-06 ,,--- 21-,83617441" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-1l-660", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "50.737 N, 5.943 E" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "10:04:44" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "35900 feet" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "SOG-06" + } + ] + }, + "raw": { + "position": { + "latitude": 50.736666666666665, + "longitude": 5.943333333333333 + }, + "message_timestamp": 36284, + "altitude": 35900, + "route": { + "waypoints": [ + { + "name": "SOG-06" + } + ] + } + }, + "remaining": { + "text": ",--- 21-,83617441" + } + } +} diff --git a/corpus/labels/1L/660/sample-002.json b/corpus/labels/1L/660/sample-002.json new file mode 100644 index 0000000..066595e --- /dev/null +++ b/corpus/labels/1L/660/sample-002.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/1L/660", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "1L", + "text": "POS Bogus Message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-1l-660", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Position Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "POS Bogus Message" + } + } +} diff --git a/corpus/labels/1M/Slash/sample-001.json b/corpus/labels/1M/Slash/sample-001.json new file mode 100644 index 0000000..a1b9b97 --- /dev/null +++ b/corpus/labels/1M/Slash/sample-001.json @@ -0,0 +1,61 @@ +{ + "spec": "labels/1M/Slash", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "1M", + "text": "/BA0843/ETA01/230822/LDSP/EGLL/EGSS/2JK0\n1940/EGLL27L/10" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-1m-slash", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "ETA Report", + "items": [ + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "LDSP" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "EGLL" + }, + { + "type": "icao", + "code": "ALT_DST", + "label": "Alternate Destination", + "value": "EGSS" + }, + { + "type": "runway", + "code": "ARWY", + "label": "Arrival Runway", + "value": "27L" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "2023-08-22T19:40:00Z" + } + ] + }, + "raw": { + "flight_number": "BA0843", + "departure_icao": "LDSP", + "arrival_icao": "EGLL", + "alternate_icao": "EGSS", + "arrival_runway": "27L", + "eta_time": 1692733200 + }, + "remaining": {} + } +} diff --git a/corpus/labels/20/POS/sample-001.json b/corpus/labels/20/POS/sample-001.json new file mode 100644 index 0000000..0e2a955 --- /dev/null +++ b/corpus/labels/20/POS/sample-001.json @@ -0,0 +1,36 @@ +{ + "spec": "labels/20/POS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "20", + "text": "POSN38160W077075,,211733,360,OTT,212041,,N42,19689,40,544" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-20-pos", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "38.160 N, 77.075 W" + } + ] + }, + "raw": { + "preamble": "POS", + "position": { + "latitude": 38.16, + "longitude": -77.075 + } + }, + "remaining": {} + } +} diff --git a/corpus/labels/20/POS/sample-002.json b/corpus/labels/20/POS/sample-002.json new file mode 100644 index 0000000..20daed1 --- /dev/null +++ b/corpus/labels/20/POS/sample-002.json @@ -0,0 +1,36 @@ +{ + "spec": "labels/20/POS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "20", + "text": "POSN38160W077075,,211733,360,OTT" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-20-pos", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "38.160 N, 77.075 W" + } + ] + }, + "raw": { + "preamble": "POS", + "position": { + "latitude": 38.16, + "longitude": -77.075 + } + }, + "remaining": {} + } +} diff --git a/corpus/labels/20/POS/sample-003.json b/corpus/labels/20/POS/sample-003.json new file mode 100644 index 0000000..9148073 --- /dev/null +++ b/corpus/labels/20/POS/sample-003.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/20/POS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "20", + "text": "POSUNKNOWN" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-20-pos", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Position Report", + "items": [] + }, + "raw": { + "preamble": "POS" + }, + "remaining": {} + } +} diff --git a/corpus/labels/21/POS/sample-001.json b/corpus/labels/21/POS/sample-001.json new file mode 100644 index 0000000..fbfb323 --- /dev/null +++ b/corpus/labels/21/POS/sample-001.json @@ -0,0 +1,73 @@ +{ + "spec": "labels/21/POS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "21", + "text": "POSN 39.841W 75.790, 220,184218,17222,22051, 34,- 4,204748,KTPA" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-21-pos", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "39.840 N, 75.790 W" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "18:42:18" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "17222 feet" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-4 degrees" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "20:47:48" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KTPA" + } + ] + }, + "raw": { + "preamble": "POS", + "position": { + "latitude": 39.84, + "longitude": -75.79 + }, + "message_timestamp": 67338, + "altitude": 17222, + "outside_air_temperature": -4, + "eta_time": 74868, + "arrival_icao": "KTPA" + }, + "remaining": { + "text": " 220,22051, 34" + } + } +} diff --git a/corpus/labels/21/POS/sample-002.json b/corpus/labels/21/POS/sample-002.json new file mode 100644 index 0000000..95a12a7 --- /dev/null +++ b/corpus/labels/21/POS/sample-002.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/21/POS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "21", + "text": "POS Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-21-pos", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Position Report", + "items": [] + }, + "raw": { + "preamble": "POS" + }, + "remaining": {} + } +} diff --git a/corpus/labels/22/OFF/sample-001.json b/corpus/labels/22/OFF/sample-001.json new file mode 100644 index 0000000..1a1a915 --- /dev/null +++ b/corpus/labels/22/OFF/sample-001.json @@ -0,0 +1,76 @@ +{ + "spec": "labels/22/OFF", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "22", + "text": "OFF01YX3661/25251712KIADKPWM171207 92" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-22-off", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Takeoff Report", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "YX3661" + }, + { + "type": "day", + "code": "DEP_DAY", + "label": "Departure Day", + "value": "25" + }, + { + "type": "day", + "code": "ARR_DAY", + "label": "Arrival Day", + "value": "25" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "17:12:00" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KIAD" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KPWM" + }, + { + "type": "time", + "code": "OFF", + "label": "Takeoff Time", + "value": "17:12:07" + } + ] + }, + "raw": { + "flight_number": "YX3661", + "departure_day": 25, + "arrival_day": 25, + "message_timestamp": 61920, + "departure_icao": "KIAD", + "arrival_icao": "KPWM", + "off_time": 61927 + }, + "remaining": { + "text": " 92" + } + } +} diff --git a/corpus/labels/22/OFF/sample-002.json b/corpus/labels/22/OFF/sample-002.json new file mode 100644 index 0000000..1a0ef5c --- /dev/null +++ b/corpus/labels/22/OFF/sample-002.json @@ -0,0 +1,86 @@ +{ + "spec": "labels/22/OFF", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "22", + "text": "OFF02XA0000/N38568 W077261251152KIADEPRZ1152****1958" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-22-off", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Takeoff Report", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "XA0000" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "38.947 N, 77.435 W" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "25" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "11:52:00" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KIAD" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "EPRZ" + }, + { + "type": "time", + "code": "OFF", + "label": "Takeoff Time", + "value": "11:52:00" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "19:58:00" + } + ] + }, + "raw": { + "flight_number": "XA0000", + "position": { + "latitude": 38.946666666666665, + "longitude": -77.435 + }, + "day": 25, + "message_timestamp": 42720, + "departure_icao": "KIAD", + "arrival_icao": "EPRZ", + "off_time": 42720, + "eta_time": 71880 + }, + "remaining": { + "text": "****" + } + } +} diff --git a/corpus/labels/22/OFF/sample-003.json b/corpus/labels/22/OFF/sample-003.json new file mode 100644 index 0000000..d9aa73b --- /dev/null +++ b/corpus/labels/22/OFF/sample-003.json @@ -0,0 +1,48 @@ +{ + "spec": "labels/22/OFF", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "22", + "text": "OFF02\r\nKBWI,KIND,1237,18.4" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-22-off", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Takeoff Report", + "items": [ + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KBWI" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KIND" + }, + { + "type": "time", + "code": "OFF", + "label": "Takeoff Time", + "value": "12:37:00" + } + ] + }, + "raw": { + "departure_icao": "KBWI", + "arrival_icao": "KIND", + "off_time": 45420 + }, + "remaining": { + "text": "18.4" + } + } +} diff --git a/corpus/labels/22/OFF/sample-004.json b/corpus/labels/22/OFF/sample-004.json new file mode 100644 index 0000000..6c84e1e --- /dev/null +++ b/corpus/labels/22/OFF/sample-004.json @@ -0,0 +1,23 @@ +{ + "spec": "labels/22/OFF", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 4", + "input": { + "label": "22", + "text": "POS Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-22-off", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Takeoff Report", + "items": [] + }, + "raw": {}, + "remaining": {} + } +} diff --git a/corpus/labels/22/POS/sample-001.json b/corpus/labels/22/POS/sample-001.json new file mode 100644 index 0000000..7454c56 --- /dev/null +++ b/corpus/labels/22/POS/sample-001.json @@ -0,0 +1,51 @@ +{ + "spec": "labels/22/POS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "22", + "text": "N 370824W 760010,-------,194936,30418, , , ,M 42,27335 42, 107," + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-22-pos", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "37.082 N, 76.001 W" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "19:49:36" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "30418 feet" + } + ] + }, + "raw": { + "position": { + "latitude": 37.0824, + "longitude": -76.001 + }, + "message_timestamp": 71376, + "altitude": 30418 + }, + "remaining": { + "text": "-------, , , ,M 42,27335 42, 107," + } + } +} diff --git a/corpus/labels/22/POS/sample-002.json b/corpus/labels/22/POS/sample-002.json new file mode 100644 index 0000000..5fe3986 --- /dev/null +++ b/corpus/labels/22/POS/sample-002.json @@ -0,0 +1,23 @@ +{ + "spec": "labels/22/POS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "22", + "text": "POS Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-22-pos", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Position Report", + "items": [] + }, + "raw": {}, + "remaining": {} + } +} diff --git a/corpus/labels/24/Slash/sample-001.json b/corpus/labels/24/Slash/sample-001.json new file mode 100644 index 0000000..350bb96 --- /dev/null +++ b/corpus/labels/24/Slash/sample-001.json @@ -0,0 +1,59 @@ +{ + "spec": "labels/24/Slash", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "24", + "text": "/241710/1021/04WM/34962/N53.13/E001.33/3374/1056/" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-24-slash", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "04WM" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "34962 feet" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "53.130 N, 1.330 E" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "10:56:00" + } + ] + }, + "raw": { + "message_timestamp": 1729160460, + "flight_number": "04WM", + "altitude": 34962, + "position": { + "latitude": 53.13, + "longitude": 1.33 + }, + "eta_time": 39360 + }, + "remaining": { + "text": "3374" + } + } +} diff --git a/corpus/labels/24/Slash/sample-002.json b/corpus/labels/24/Slash/sample-002.json new file mode 100644 index 0000000..550c04f --- /dev/null +++ b/corpus/labels/24/Slash/sample-002.json @@ -0,0 +1,23 @@ +{ + "spec": "labels/24/Slash", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "24", + "text": "/ Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-24-slash", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Position Report", + "items": [] + }, + "raw": {}, + "remaining": {} + } +} diff --git a/corpus/labels/2P/FM3/sample-001.json b/corpus/labels/2P/FM3/sample-001.json new file mode 100644 index 0000000..482ca7f --- /dev/null +++ b/corpus/labels/2P/FM3/sample-001.json @@ -0,0 +1,58 @@ +{ + "spec": "labels/2P/FM3", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "2P", + "text": "FM3 1217,1312,+ 43.77,- 70.18, 39981, 426, 25" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-2p-fm3", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Flight Report", + "items": [ + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "12:17:00" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "13:12:00" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "43.770 N, 70.180 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "39981 feet" + } + ] + }, + "raw": { + "message_timestamp": 44220, + "eta_time": 47520, + "position": { + "latitude": 43.77, + "longitude": -70.18 + }, + "altitude": 39981 + }, + "remaining": { + "text": " 426, 25" + } + } +} diff --git a/corpus/labels/2P/FM3/sample-002.json b/corpus/labels/2P/FM3/sample-002.json new file mode 100644 index 0000000..efe176c --- /dev/null +++ b/corpus/labels/2P/FM3/sample-002.json @@ -0,0 +1,65 @@ +{ + "spec": "labels/2P/FM3", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "2P", + "text": "M40AEY093CFM3 1216,1454,+057.31,-075.58, 38002, 469, 23" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-2p-fm3", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Flight Report", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "EY093C" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "12:16:00" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "14:54:00" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "57.310 N, 75.580 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "38002 feet" + } + ] + }, + "raw": { + "flight_number": "EY093C", + "message_timestamp": 44160, + "eta_time": 53640, + "position": { + "latitude": 57.31, + "longitude": -75.58 + }, + "altitude": 38002 + }, + "remaining": { + "text": "M40A, 469, 23" + } + } +} diff --git a/corpus/labels/2P/FM3/sample-003.json b/corpus/labels/2P/FM3/sample-003.json new file mode 100644 index 0000000..ac1e562 --- /dev/null +++ b/corpus/labels/2P/FM3/sample-003.json @@ -0,0 +1,58 @@ +{ + "spec": "labels/2P/FM3", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "2P", + "text": "FM3 133818,1607,N 45.206,E 17.726,34030, 440,98" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-2p-fm3", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Flight Report", + "items": [ + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "13:38:18" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "16:07:00" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "45.206 N, 17.726 E" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "34030 feet" + } + ] + }, + "raw": { + "message_timestamp": 49098, + "eta_time": 58020, + "position": { + "latitude": 45.206, + "longitude": 17.726 + }, + "altitude": 34030 + }, + "remaining": { + "text": " 440,98" + } + } +} diff --git a/corpus/labels/2P/FM3/sample-004.json b/corpus/labels/2P/FM3/sample-004.json new file mode 100644 index 0000000..fdff7b9 --- /dev/null +++ b/corpus/labels/2P/FM3/sample-004.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/2P/FM3", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 4", + "input": { + "label": "2P", + "text": "FM4 Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-2p-fm3", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Flight Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "FM4 Bogus message" + } + } +} diff --git a/corpus/labels/2P/FM4/sample-001.json b/corpus/labels/2P/FM4/sample-001.json new file mode 100644 index 0000000..6e588d0 --- /dev/null +++ b/corpus/labels/2P/FM4/sample-001.json @@ -0,0 +1,86 @@ +{ + "spec": "labels/2P/FM4", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "2P", + "text": "FM4KIAD,OMAA,140256,1448, 39.43,- 75.62,23228,328, 43.5, 72500" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-2p-fm4", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Flight Report", + "items": [ + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KIAD" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "OMAA" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "14" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "02:56:00" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "14:48:00" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "39.430 N, 75.620 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "23228 feet" + }, + { + "type": "heading", + "code": "HDG", + "label": "Heading", + "value": "328" + } + ] + }, + "raw": { + "departure_icao": "KIAD", + "arrival_icao": "OMAA", + "day": 14, + "message_timestamp": 10560, + "eta_time": 53280, + "position": { + "latitude": 39.43, + "longitude": -75.62 + }, + "altitude": 23228, + "heading": 328 + }, + "remaining": { + "text": " 43.5, 72500" + } + } +} diff --git a/corpus/labels/2P/FM4/sample-002.json b/corpus/labels/2P/FM4/sample-002.json new file mode 100644 index 0000000..42d3632 --- /dev/null +++ b/corpus/labels/2P/FM4/sample-002.json @@ -0,0 +1,93 @@ +{ + "spec": "labels/2P/FM4", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "2P", + "text": "M58AEY0801FM4RJAA,OMAA,141234,2105, 38.92, 115.44,34099,296,-105.5, 52800" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-2p-fm4", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Flight Report", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "EY0801" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "RJAA" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "OMAA" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "14" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "12:34:00" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "21:05:00" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "38.920 N, 115.440 E" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "34099 feet" + }, + { + "type": "heading", + "code": "HDG", + "label": "Heading", + "value": "296" + } + ] + }, + "raw": { + "flight_number": "EY0801", + "departure_icao": "RJAA", + "arrival_icao": "OMAA", + "day": 14, + "message_timestamp": 45240, + "eta_time": 75900, + "position": { + "latitude": 38.92, + "longitude": 115.44 + }, + "altitude": 34099, + "heading": 296 + }, + "remaining": { + "text": "M58A,-105.5, 52800" + } + } +} diff --git a/corpus/labels/2P/FM4/sample-003.json b/corpus/labels/2P/FM4/sample-003.json new file mode 100644 index 0000000..e0076e4 --- /dev/null +++ b/corpus/labels/2P/FM4/sample-003.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/2P/FM4", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "2P", + "text": "FM4 Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-2p-fm4", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Flight Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "FM4 Bogus message" + } + } +} diff --git a/corpus/labels/2P/FM5/sample-001.json b/corpus/labels/2P/FM5/sample-001.json new file mode 100644 index 0000000..4d3f0e3 --- /dev/null +++ b/corpus/labels/2P/FM5/sample-001.json @@ -0,0 +1,79 @@ +{ + "spec": "labels/2P/FM5", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "2P", + "text": "FM5 EIDW,OMAA,113522,1540,+45.147, +23.384,35002,116.24,502 ,36900,ETD23N ," + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-2p-fm5", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Flight Report", + "items": [ + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "EIDW" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "OMAA" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "11:35:22" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "15:40:00" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "45.147 N, 23.384 E" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "35002 feet" + }, + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "ETD23N" + } + ] + }, + "raw": { + "departure_icao": "EIDW", + "arrival_icao": "OMAA", + "message_timestamp": 41722, + "eta_time": 56400, + "position": { + "latitude": 45.147, + "longitude": 23.384 + }, + "altitude": 35002, + "flight_number": "ETD23N" + }, + "remaining": { + "text": "116.24,502 ,36900," + } + } +} diff --git a/corpus/labels/2P/FM5/sample-002.json b/corpus/labels/2P/FM5/sample-002.json new file mode 100644 index 0000000..734b61d --- /dev/null +++ b/corpus/labels/2P/FM5/sample-002.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/2P/FM5", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "2P", + "text": "FM4 Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-2p-fm5", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Flight Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "FM4 Bogus message" + } + } +} diff --git a/corpus/labels/30/Slash_EA/sample-001.json b/corpus/labels/30/Slash_EA/sample-001.json new file mode 100644 index 0000000..b8b36d1 --- /dev/null +++ b/corpus/labels/30/Slash_EA/sample-001.json @@ -0,0 +1,41 @@ +{ + "spec": "labels/30/Slash_EA", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "30", + "text": "/EA1719/DSKSFO/SK23" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-30-slash-ea", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "ETA Report", + "items": [ + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "17:19:00" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KSFO" + } + ] + }, + "raw": { + "eta_time": 62340, + "arrival_icao": "KSFO" + }, + "remaining": { + "text": "/SK23" + } + } +} diff --git a/corpus/labels/44/ETA/sample-001.json b/corpus/labels/44/ETA/sample-001.json new file mode 100644 index 0000000..25623ad --- /dev/null +++ b/corpus/labels/44/ETA/sample-001.json @@ -0,0 +1,91 @@ +{ + "spec": "labels/44/ETA", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "44", + "text": "00ETA03,N38241W081357,330,KBNA,KBWI,1107,0123,0208,008.1" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-44-eta", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "ETA Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "38.402 N, 81.595 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "33000 feet" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KBNA" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KBWI" + }, + { + "type": "month", + "code": "MSG_MON", + "label": "Month of Year", + "value": "11" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "7" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "01:23:00" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "02:08:00" + }, + { + "type": "fuel_remaining", + "code": " FUEL_REM", + "label": "Fuel Remaining", + "value": "8.1" + } + ] + }, + "raw": { + "position": { + "latitude": 38.401666666666664, + "longitude": -81.595 + }, + "altitude": 33000, + "departure_icao": "KBNA", + "arrival_icao": "KBWI", + "month": 11, + "day": 7, + "message_timestamp": 4980, + "eta_time": 7680, + "fuel_remaining": 8.1 + }, + "remaining": {} + } +} diff --git a/corpus/labels/44/ETA/sample-002.json b/corpus/labels/44/ETA/sample-002.json new file mode 100644 index 0000000..d49a064 --- /dev/null +++ b/corpus/labels/44/ETA/sample-002.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/44/ETA", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "44", + "text": "00OFF01 Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-44-eta", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "ETA Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "00OFF01 Bogus message" + } + } +} diff --git a/corpus/labels/44/IN/sample-001.json b/corpus/labels/44/IN/sample-001.json new file mode 100644 index 0000000..6f393fc --- /dev/null +++ b/corpus/labels/44/IN/sample-001.json @@ -0,0 +1,70 @@ +{ + "spec": "labels/44/IN", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "44", + "text": "IN01,N33528W084181,KCLT,KPDK,1106,0045,---.-" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-44-in", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "In Gate Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "33.880 N, 84.302 W" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KCLT" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KPDK" + }, + { + "type": "month", + "code": "MSG_MON", + "label": "Month of Year", + "value": "11" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "6" + }, + { + "type": "time", + "code": "IN", + "label": "In Gate Time", + "value": "00:45:00" + } + ] + }, + "raw": { + "position": { + "latitude": 33.88, + "longitude": -84.30166666666666 + }, + "departure_icao": "KCLT", + "arrival_icao": "KPDK", + "month": 11, + "day": 6, + "in_time": 2700 + }, + "remaining": {} + } +} diff --git a/corpus/labels/44/IN/sample-002.json b/corpus/labels/44/IN/sample-002.json new file mode 100644 index 0000000..020f2e5 --- /dev/null +++ b/corpus/labels/44/IN/sample-002.json @@ -0,0 +1,77 @@ +{ + "spec": "labels/44/IN", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "44", + "text": "IN02,N38338W121179,KMHR,KPDX,0806,2355,005.1" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-44-in", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "In Gate Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "38.563 N, 121.298 W" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KMHR" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KPDX" + }, + { + "type": "month", + "code": "MSG_MON", + "label": "Month of Year", + "value": "8" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "6" + }, + { + "type": "time", + "code": "IN", + "label": "In Gate Time", + "value": "23:55:00" + }, + { + "type": "fuel_remaining", + "code": " FUEL_REM", + "label": "Fuel Remaining", + "value": "5.1" + } + ] + }, + "raw": { + "position": { + "latitude": 38.56333333333333, + "longitude": -121.29833333333333 + }, + "departure_icao": "KMHR", + "arrival_icao": "KPDX", + "month": 8, + "day": 6, + "in_time": 86100, + "fuel_remaining": 5.1 + }, + "remaining": {} + } +} diff --git a/corpus/labels/44/IN/sample-003.json b/corpus/labels/44/IN/sample-003.json new file mode 100644 index 0000000..2749d07 --- /dev/null +++ b/corpus/labels/44/IN/sample-003.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/44/IN", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "44", + "text": "00OFF01 Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-44-in", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "In Gate Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "00OFF01 Bogus message" + } + } +} diff --git a/corpus/labels/44/OFF/sample-001.json b/corpus/labels/44/OFF/sample-001.json new file mode 100644 index 0000000..49c9295 --- /dev/null +++ b/corpus/labels/44/OFF/sample-001.json @@ -0,0 +1,84 @@ +{ + "spec": "labels/44/OFF", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "44", + "text": "OFF02,N39247W077226,KFDK,KSNA,1106,2124,0248,011.1" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-44-off", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Off Runway Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "39.412 N, 77.377 W" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KFDK" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KSNA" + }, + { + "type": "month", + "code": "MSG_MON", + "label": "Month of Year", + "value": "11" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "6" + }, + { + "type": "time", + "code": "OFF", + "label": "Takeoff Time", + "value": "21:24:00" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "02:48:00" + }, + { + "type": "fuel_remaining", + "code": " FUEL_REM", + "label": "Fuel Remaining", + "value": "11.1" + } + ] + }, + "raw": { + "position": { + "latitude": 39.41166666666667, + "longitude": -77.37666666666667 + }, + "departure_icao": "KFDK", + "arrival_icao": "KSNA", + "month": 11, + "day": 6, + "off_time": 77040, + "eta_time": 10080, + "fuel_remaining": 11.1 + }, + "remaining": {} + } +} diff --git a/corpus/labels/44/OFF/sample-002.json b/corpus/labels/44/OFF/sample-002.json new file mode 100644 index 0000000..199a8a7 --- /dev/null +++ b/corpus/labels/44/OFF/sample-002.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/44/OFF", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "44", + "text": "00OFF01 Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-44-off", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Off Runway Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "00OFF01 Bogus message" + } + } +} diff --git a/corpus/labels/44/ON/sample-001.json b/corpus/labels/44/ON/sample-001.json new file mode 100644 index 0000000..d3e5fbd --- /dev/null +++ b/corpus/labels/44/ON/sample-001.json @@ -0,0 +1,72 @@ +{ + "spec": "labels/44/ON", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "44", + "text": "ON01,N33522W084181,KCLT,KPDK,1106,004023,---.-," + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-44-on", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "On Runway Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "33.870 N, 84.302 W" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KCLT" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KPDK" + }, + { + "type": "month", + "code": "MSG_MON", + "label": "Month of Year", + "value": "11" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "6" + }, + { + "type": "time", + "code": "ON", + "label": "Landing Time", + "value": "00:40:23" + } + ] + }, + "raw": { + "position": { + "latitude": 33.87, + "longitude": -84.30166666666666 + }, + "departure_icao": "KCLT", + "arrival_icao": "KPDK", + "month": 11, + "day": 6, + "on_time": 2423 + }, + "remaining": { + "text": "" + } + } +} diff --git a/corpus/labels/44/ON/sample-002.json b/corpus/labels/44/ON/sample-002.json new file mode 100644 index 0000000..811d661 --- /dev/null +++ b/corpus/labels/44/ON/sample-002.json @@ -0,0 +1,77 @@ +{ + "spec": "labels/44/ON", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "44", + "text": "ON02,N38333W121178,KRNO,KMHR,0806,2350,005.2" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-44-on", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "On Runway Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "38.555 N, 121.297 W" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KRNO" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KMHR" + }, + { + "type": "month", + "code": "MSG_MON", + "label": "Month of Year", + "value": "8" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "6" + }, + { + "type": "time", + "code": "ON", + "label": "Landing Time", + "value": "23:50:00" + }, + { + "type": "fuel_remaining", + "code": " FUEL_REM", + "label": "Fuel Remaining", + "value": "5.2" + } + ] + }, + "raw": { + "position": { + "latitude": 38.555, + "longitude": -121.29666666666667 + }, + "departure_icao": "KRNO", + "arrival_icao": "KMHR", + "month": 8, + "day": 6, + "on_time": 85800, + "fuel_remaining": 5.2 + }, + "remaining": {} + } +} diff --git a/corpus/labels/44/ON/sample-003.json b/corpus/labels/44/ON/sample-003.json new file mode 100644 index 0000000..54af443 --- /dev/null +++ b/corpus/labels/44/ON/sample-003.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/44/ON", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "44", + "text": "00OFF01 Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-44-on", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "On Runway Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "00OFF01 Bogus message" + } + } +} diff --git a/corpus/labels/44/ON/sample-004.json b/corpus/labels/44/ON/sample-004.json new file mode 100644 index 0000000..0491c8d --- /dev/null +++ b/corpus/labels/44/ON/sample-004.json @@ -0,0 +1,72 @@ +{ + "spec": "labels/44/ON", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 4", + "input": { + "label": "44", + "text": "ON01,N33522W084181,KCLT,KPDK,1106,004023,---.-," + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-44-on", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "On Runway Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "33.870 N, 84.302 W" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KCLT" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KPDK" + }, + { + "type": "month", + "code": "MSG_MON", + "label": "Month of Year", + "value": "11" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "6" + }, + { + "type": "time", + "code": "ON", + "label": "Landing Time", + "value": "00:40:23" + } + ] + }, + "raw": { + "position": { + "latitude": 33.87, + "longitude": -84.30166666666666 + }, + "departure_icao": "KCLT", + "arrival_icao": "KPDK", + "month": 11, + "day": 6, + "on_time": 2423 + }, + "remaining": { + "text": "" + } + } +} diff --git a/corpus/labels/44/POS/sample-001.json b/corpus/labels/44/POS/sample-001.json new file mode 100644 index 0000000..ac4a867 --- /dev/null +++ b/corpus/labels/44/POS/sample-001.json @@ -0,0 +1,85 @@ +{ + "spec": "labels/44/POS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "44", + "text": "POS02,N38338W121179,GRD,KMHR,KPDX,0807,0003,0112,005.1" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-44-pos", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "38.563 N, 121.298 W" + }, + { + "type": "month", + "code": "MSG_MON", + "label": "Month of Year", + "value": "8" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "7" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "00:03:00" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "01:12:00" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KMHR" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KPDX" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "0 feet" + } + ] + }, + "raw": { + "position": { + "latitude": 38.56333333333333, + "longitude": -121.29833333333333 + }, + "month": 8, + "day": 7, + "message_timestamp": 180, + "eta_time": 4320, + "fuel_in_tons": 5.1, + "departure_icao": "KMHR", + "arrival_icao": "KPDX", + "altitude": 0 + }, + "remaining": {} + } +} diff --git a/corpus/labels/44/POS/sample-002.json b/corpus/labels/44/POS/sample-002.json new file mode 100644 index 0000000..daacc50 --- /dev/null +++ b/corpus/labels/44/POS/sample-002.json @@ -0,0 +1,85 @@ +{ + "spec": "labels/44/POS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "44", + "text": "POS02,N38171W077507,319,KJFK,KUZA,0926,0245,0327,004.6" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-44-pos", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "38.285 N, 77.845 W" + }, + { + "type": "month", + "code": "MSG_MON", + "label": "Month of Year", + "value": "9" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "26" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "02:45:00" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "03:27:00" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KJFK" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KUZA" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "31900 feet" + } + ] + }, + "raw": { + "position": { + "latitude": 38.285, + "longitude": -77.845 + }, + "month": 9, + "day": 26, + "message_timestamp": 9900, + "eta_time": 12420, + "fuel_in_tons": 4.6, + "departure_icao": "KJFK", + "arrival_icao": "KUZA", + "altitude": 31900 + }, + "remaining": {} + } +} diff --git a/corpus/labels/44/Slash/sample-001.json b/corpus/labels/44/Slash/sample-001.json new file mode 100644 index 0000000..2ae0699 --- /dev/null +++ b/corpus/labels/44/Slash/sample-001.json @@ -0,0 +1,58 @@ +{ + "spec": "labels/44/Slash", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "44", + "text": " /FB 0160/AD KORH/N 38.655,W 75.325,JBU2834,INA03,KORH,2043" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-44-slash", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Flight Briefing", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "38.655 N, 75.325 W" + }, + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "JBU2834" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KORH" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "20:43:00" + } + ] + }, + "raw": { + "position": { + "latitude": 38.655, + "longitude": -75.325 + }, + "flight_number": "JBU2834", + "arrival_icao": "KORH", + "eta_time": 74580 + }, + "remaining": { + "text": " /FB 0160,INA03" + } + } +} diff --git a/corpus/labels/44/Slash/sample-002.json b/corpus/labels/44/Slash/sample-002.json new file mode 100644 index 0000000..26a6600 --- /dev/null +++ b/corpus/labels/44/Slash/sample-002.json @@ -0,0 +1,86 @@ +{ + "spec": "labels/44/Slash", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "44", + "text": " /FB ----/AD KTPA/N 27.971,W 82.558,JBU91,FLS03,KTPA,53988,53988,----,1943,1943,1,1L,VIS1L,0,2,0,," + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-44-slash", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Flight Briefing", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "27.971 N, 82.558 W" + }, + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "JBU91" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KTPA" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "YYYY-MM-03T06:38:08" + }, + { + "type": "fuel_remaining", + "code": " FUEL_REM", + "label": "Fuel Remaining", + "value": "1943" + }, + { + "type": "runway", + "code": "ARWY", + "label": "Arrival Runway", + "value": "1L" + }, + { + "type": "procedure", + "code": "proc", + "label": "Arrival Procedure", + "value": "VIS1L" + } + ] + }, + "raw": { + "position": { + "latitude": 27.971, + "longitude": -82.558 + }, + "flight_number": "JBU91", + "arrival_icao": "KTPA", + "eta_time": 196688, + "fuel_remaining": 1943, + "arrival_runway": "1L", + "procedures": [ + { + "type": "arrival", + "route": { + "name": "VIS1L" + } + } + ] + }, + "remaining": { + "text": " /FB ----,FLS03,53988,----,1943,1,0,2,0,," + } + } +} diff --git a/corpus/labels/44/Slash/sample-003.json b/corpus/labels/44/Slash/sample-003.json new file mode 100644 index 0000000..9364d62 --- /dev/null +++ b/corpus/labels/44/Slash/sample-003.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/44/Slash", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "44", + "text": "00OFF01 Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-44-slash", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Flight Briefing", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "00OFF01 Bogus message" + } + } +} diff --git a/corpus/labels/4A/01/sample-001.json b/corpus/labels/4A/01/sample-001.json new file mode 100644 index 0000000..604c00f --- /dev/null +++ b/corpus/labels/4A/01/sample-001.json @@ -0,0 +1,79 @@ +{ + "spec": "labels/4A/01", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "4A", + "text": "01DCAP VIR41R/190203EGLLKSFO\r\n+ 1418158.0+ 24.8" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-4a-01", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Latest New Format", + "items": [ + { + "type": "state_change", + "code": "STATE_CHANGE", + "label": "State Change", + "value": "Descent -> Approach" + }, + { + "type": "callsign", + "code": "CALLSIGN", + "label": "Callsign", + "value": "VIR41R" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "19:02:03" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "EGLL" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KSFO" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "1418 feet" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "24.8 degrees" + } + ] + }, + "raw": { + "state_change": { + "from": "DC", + "to": "AP" + }, + "callsign": "VIR41R", + "message_timestamp": 68523, + "departure_icao": "EGLL", + "arrival_icao": "KSFO", + "altitude": 1418, + "outside_air_temperature": 24.8 + }, + "remaining": { + "text": "158.0" + } + } +} diff --git a/corpus/labels/4A/DIS/sample-001.json b/corpus/labels/4A/DIS/sample-001.json new file mode 100644 index 0000000..4339698 --- /dev/null +++ b/corpus/labels/4A/DIS/sample-001.json @@ -0,0 +1,46 @@ +{ + "spec": "labels/4A/DIS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "4A", + "text": "DIS01,190009,WEN3140,@HOLD CNX" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-4a-dis", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Latest New Format", + "items": [ + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "00:09:00" + }, + { + "type": "callsign", + "code": "CALLSIGN", + "label": "Callsign", + "value": "WEN3140" + }, + { + "type": "text", + "code": "TEXT", + "label": "Text Message", + "value": "@HOLD CNX" + } + ] + }, + "raw": { + "message_timestamp": 540, + "callsign": "WEN3140", + "text": "@HOLD CNX" + }, + "remaining": {} + } +} diff --git a/corpus/labels/4A/DOOR/sample-001.json b/corpus/labels/4A/DOOR/sample-001.json new file mode 100644 index 0000000..7c56aab --- /dev/null +++ b/corpus/labels/4A/DOOR/sample-001.json @@ -0,0 +1,42 @@ +{ + "spec": "labels/4A/DOOR", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "4A", + "text": "DOOR/FWDENTRY CLSD 1440" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-4a-door", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Latest New Format", + "items": [ + { + "type": "door_event", + "code": "DOOR", + "label": "Door Event", + "value": "FWDENTRY CLSD" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "14:40:00" + } + ] + }, + "raw": { + "door_event": { + "door": "FWDENTRY", + "state": "CLSD" + }, + "message_timestamp": 52800 + }, + "remaining": {} + } +} diff --git a/corpus/labels/4A/Slash_01/sample-001.json b/corpus/labels/4A/Slash_01/sample-001.json new file mode 100644 index 0000000..f2ccb4d --- /dev/null +++ b/corpus/labels/4A/Slash_01/sample-001.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/4A/Slash_01", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "4A", + "text": "/01-C" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-4a-slash-01", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Latest New Format", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "C" + } + } +} diff --git a/corpus/labels/4A/sample-001.json b/corpus/labels/4A/sample-001.json new file mode 100644 index 0000000..e04d233 --- /dev/null +++ b/corpus/labels/4A/sample-001.json @@ -0,0 +1,69 @@ +{ + "spec": "labels/4A", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "4A", + "text": "N22456E077014OSE35 ,192027370VEX36 ,192316,M46,275043309,85220111" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-4a", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Latest New Format", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "22.456 N, 77.014 E" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "37000 feet" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "OSE35@19:20:27 > VEX36@19:23:16" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-46 degrees" + } + ] + }, + "raw": { + "position": { + "latitude": 22.456, + "longitude": 77.014 + }, + "altitude": 37000, + "route": { + "waypoints": [ + { + "name": "OSE35", + "time": 69627 + }, + { + "name": "VEX36", + "time": 69796 + } + ] + }, + "outside_air_temperature": -46 + }, + "remaining": { + "text": "275043309,85220111" + } + } +} diff --git a/corpus/labels/4A/sample-002.json b/corpus/labels/4A/sample-002.json new file mode 100644 index 0000000..9880883 --- /dev/null +++ b/corpus/labels/4A/sample-002.json @@ -0,0 +1,62 @@ +{ + "spec": "labels/4A", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "4A", + "text": "063200,1910,.N343FR,FFT2028,KSLC,KORD,1,0632,RT0,LT0," + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-4a", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Latest New Format", + "items": [ + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "06:32:00" + }, + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "N343FR" + }, + { + "type": "callsign", + "code": "CALLSIGN", + "label": "Callsign", + "value": "FFT2028" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KSLC" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KORD" + } + ] + }, + "raw": { + "message_timestamp": 23520, + "tail": "N343FR", + "callsign": "FFT2028", + "departure_icao": "KSLC", + "arrival_icao": "KORD" + }, + "remaining": { + "text": "RT0,LT0," + } + } +} diff --git a/corpus/labels/4A/sample-003.json b/corpus/labels/4A/sample-003.json new file mode 100644 index 0000000..f0260d4 --- /dev/null +++ b/corpus/labels/4A/sample-003.json @@ -0,0 +1,55 @@ +{ + "spec": "labels/4A", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "4A", + "text": "101606,1910,.N317FR,,KMDW,----,1,1016,RT0,LT1," + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-4a", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Latest New Format", + "items": [ + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "10:16:06" + }, + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "N317FR" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KMDW" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "----" + } + ] + }, + "raw": { + "message_timestamp": 36966, + "tail": "N317FR", + "departure_icao": "KMDW", + "arrival_icao": "----" + }, + "remaining": { + "text": "RT0,LT1," + } + } +} diff --git a/corpus/labels/4A/sample-004.json b/corpus/labels/4A/sample-004.json new file mode 100644 index 0000000..2b365b8 --- /dev/null +++ b/corpus/labels/4A/sample-004.json @@ -0,0 +1,69 @@ +{ + "spec": "labels/4A", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 4", + "input": { + "label": "4A", + "text": "N45129W093113MSP/07 ,204436123VECTORS,,P04,268044858,46904221" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-4a", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Latest New Format", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "45.129 N, 93.113 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "12300 feet" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "MSP/07@20:44:36 > VECTORS" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "4 degrees" + } + ] + }, + "raw": { + "position": { + "latitude": 45.129, + "longitude": -93.113 + }, + "altitude": 12300, + "route": { + "waypoints": [ + { + "name": "MSP/07", + "time": 74676 + }, + { + "name": "VECTORS", + "time": 0 + } + ] + }, + "outside_air_temperature": 4 + }, + "remaining": { + "text": "268044858,46904221" + } + } +} diff --git a/corpus/labels/4A/sample-005.json b/corpus/labels/4A/sample-005.json new file mode 100644 index 0000000..69c3825 --- /dev/null +++ b/corpus/labels/4A/sample-005.json @@ -0,0 +1,58 @@ +{ + "spec": "labels/4A", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 5", + "input": { + "label": "4A", + "text": "124442,1320, 138,33467,N 41.093,W 72.677" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-4a", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Latest New Format", + "items": [ + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "12:44:42" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "13:20:00" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "33467 feet" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "41.093 N, 72.677 W" + } + ] + }, + "raw": { + "message_timestamp": 45882, + "eta_time": 48000, + "altitude": 33467, + "position": { + "latitude": 41.093, + "longitude": -72.677 + } + }, + "remaining": { + "text": " 138" + } + } +} diff --git a/corpus/labels/4A/sample-006.json b/corpus/labels/4A/sample-006.json new file mode 100644 index 0000000..a556d95 --- /dev/null +++ b/corpus/labels/4A/sample-006.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/4A", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 6", + "input": { + "label": "4A", + "text": "DIS01,182103,WEN3100,WRONG CREW HAHAHA" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-4a", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Latest New Format", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "DIS01,182103,WEN3100,WRONG CREW HAHAHA" + } + } +} diff --git a/corpus/labels/4N/sample-001.json b/corpus/labels/4N/sample-001.json new file mode 100644 index 0000000..5fa4e2f --- /dev/null +++ b/corpus/labels/4N/sample-001.json @@ -0,0 +1,70 @@ +{ + "spec": "labels/4N", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "4N", + "text": "285,C,,10/12,,,,,NRT,ANC,ANC,07R/,33/,0,0,,,,,,0,0,0,0,1,0,,0,0,709.8,048.7,758.5,75F3" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-4n", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Defined", + "items": [ + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "NRT" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "ANC" + }, + { + "type": "icao", + "code": "ALT_DST", + "label": "Alternate Destination", + "value": "ANC" + }, + { + "type": "runway", + "code": "ARWY", + "label": "Arrival Runway", + "value": "07R" + }, + { + "type": "runway", + "code": "ALT_ARWY", + "label": "Alternate Runway", + "value": "33" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x75f3" + } + ] + }, + "raw": { + "date": "10/12", + "departure_icao": "NRT", + "arrival_icao": "ANC", + "alternate_icao": "ANC", + "arrival_runway": "07R", + "alternate_runway": "33", + "checksum": 30195 + }, + "remaining": { + "text": "C,0,0,0,0,0,0,1,0,0,0,709.8,048.7,758.5" + } + } +} diff --git a/corpus/labels/4N/sample-002.json b/corpus/labels/4N/sample-002.json new file mode 100644 index 0000000..806eb59 --- /dev/null +++ b/corpus/labels/4N/sample-002.json @@ -0,0 +1,70 @@ +{ + "spec": "labels/4N", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "4N", + "text": "285,C,,10/12,,,,,NRT,ANC,ANC,07R/,33/,0,0,,,,,,0,0,0,0,1,0,,0,0,709.8,048.7,758.5,75F3" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-4n", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Defined", + "items": [ + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "NRT" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "ANC" + }, + { + "type": "icao", + "code": "ALT_DST", + "label": "Alternate Destination", + "value": "ANC" + }, + { + "type": "runway", + "code": "ARWY", + "label": "Arrival Runway", + "value": "07R" + }, + { + "type": "runway", + "code": "ALT_ARWY", + "label": "Alternate Runway", + "value": "33" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x75f3" + } + ] + }, + "raw": { + "date": "10/12", + "departure_icao": "NRT", + "arrival_icao": "ANC", + "alternate_icao": "ANC", + "arrival_runway": "07R", + "alternate_runway": "33", + "checksum": 30195 + }, + "remaining": { + "text": "C,0,0,0,0,0,0,1,0,0,0,709.8,048.7,758.5" + } + } +} diff --git a/corpus/labels/4N/sample-003.json b/corpus/labels/4N/sample-003.json new file mode 100644 index 0000000..93f23b4 --- /dev/null +++ b/corpus/labels/4N/sample-003.json @@ -0,0 +1,70 @@ +{ + "spec": "labels/4N", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "4N", + "text": "285,C,,10/12,,,,,NRT,ANC,ANC,07R/,33/,0,0,,,,,,0,0,0,0,1,0,,0,0,709.8,048.7,758.5,75F3" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-4n", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Defined", + "items": [ + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "NRT" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "ANC" + }, + { + "type": "icao", + "code": "ALT_DST", + "label": "Alternate Destination", + "value": "ANC" + }, + { + "type": "runway", + "code": "ARWY", + "label": "Arrival Runway", + "value": "07R" + }, + { + "type": "runway", + "code": "ALT_ARWY", + "label": "Alternate Runway", + "value": "33" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x75f3" + } + ] + }, + "raw": { + "date": "10/12", + "departure_icao": "NRT", + "arrival_icao": "ANC", + "alternate_icao": "ANC", + "arrival_runway": "07R", + "alternate_runway": "33", + "checksum": 30195 + }, + "remaining": { + "text": "C,0,0,0,0,0,0,1,0,0,0,709.8,048.7,758.5" + } + } +} diff --git a/corpus/labels/4N/sample-004.json b/corpus/labels/4N/sample-004.json new file mode 100644 index 0000000..b113d03 --- /dev/null +++ b/corpus/labels/4N/sample-004.json @@ -0,0 +1,59 @@ +{ + "spec": "labels/4N", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 4", + "input": { + "label": "4N", + "text": "22024N MCI JFK1\r\n0013 0072 N040586 W074421 230" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-4n", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Defined", + "items": [ + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "MCI" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "JFK" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "40.977 N, 74.702 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "23000 feet" + } + ] + }, + "raw": { + "day": "22", + "departure_icao": "MCI", + "arrival_icao": "JFK", + "position": { + "latitude": 40.97666666666667, + "longitude": -74.70166666666667 + }, + "altitude": 23000 + }, + "remaining": { + "text": "02 0013 0072" + } + } +} diff --git a/corpus/labels/4N/sample-005.json b/corpus/labels/4N/sample-005.json new file mode 100644 index 0000000..754c662 --- /dev/null +++ b/corpus/labels/4N/sample-005.json @@ -0,0 +1,87 @@ +{ + "spec": "labels/4N", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 5", + "input": { + "label": "4N", + "text": "285,B,69005074-507,10/12,+36.081,-094.810,35014,002.3,ELP,SDF,SDF,17R/,17L/,0,0,,,,,,0,0,0,0,1,,,,,247.0,014.2,261.2,421A" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-4n", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Defined", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "36.081 N, 94.810 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "35014 feet" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "ELP" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "SDF" + }, + { + "type": "icao", + "code": "ALT_DST", + "label": "Alternate Destination", + "value": "SDF" + }, + { + "type": "runway", + "code": "ARWY", + "label": "Arrival Runway", + "value": "17R" + }, + { + "type": "runway", + "code": "ALT_ARWY", + "label": "Alternate Runway", + "value": "17L" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x421a" + } + ] + }, + "raw": { + "date": "10/12", + "position": { + "latitude": 36.081, + "longitude": -94.81 + }, + "altitude": 35014, + "departure_icao": "ELP", + "arrival_icao": "SDF", + "alternate_icao": "SDF", + "arrival_runway": "17R", + "alternate_runway": "17L", + "checksum": 16922 + }, + "remaining": { + "text": "B,69005074-507,002.3,0,0,0,0,0,0,1,247.0,014.2,261.2" + } + } +} diff --git a/corpus/labels/4N/sample-006.json b/corpus/labels/4N/sample-006.json new file mode 100644 index 0000000..ae269b2 --- /dev/null +++ b/corpus/labels/4N/sample-006.json @@ -0,0 +1,63 @@ +{ + "spec": "labels/4N", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 6", + "input": { + "label": "4N", + "text": "285,C,,09/24,,,,,EWR,PHL,PHL,09R/,/,0,0,,,,,,1,0,0,0,1,0,,0,0,198.5,014.5,213.0,9BCD" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-4n", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Defined", + "items": [ + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "EWR" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "PHL" + }, + { + "type": "icao", + "code": "ALT_DST", + "label": "Alternate Destination", + "value": "PHL" + }, + { + "type": "runway", + "code": "ARWY", + "label": "Arrival Runway", + "value": "09R" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x9bcd" + } + ] + }, + "raw": { + "date": "09/24", + "departure_icao": "EWR", + "arrival_icao": "PHL", + "alternate_icao": "PHL", + "arrival_runway": "09R", + "checksum": 39885 + }, + "remaining": { + "text": "C,0,0,1,0,0,0,1,0,0,0,198.5,014.5,213.0" + } + } +} diff --git a/corpus/labels/4N/sample-007.json b/corpus/labels/4N/sample-007.json new file mode 100644 index 0000000..c50fcb3 --- /dev/null +++ b/corpus/labels/4N/sample-007.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/4N", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 7", + "input": { + "label": "4N", + "text": "4N Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-4n", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Airline Defined", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "4N Bogus message" + } + } +} diff --git a/corpus/labels/4T/AGFSR/sample-001.json b/corpus/labels/4T/AGFSR/sample-001.json new file mode 100644 index 0000000..84b99fc --- /dev/null +++ b/corpus/labels/4T/AGFSR/sample-001.json @@ -0,0 +1,86 @@ +{ + "spec": "labels/4T/AGFSR", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "4T", + "text": "AGFSR AC0620/07/08/YYZYHZ/0340Z/453/4435.1N07143.4W/350/ /0063/0035/ /281065/----/ /512/0240/0253/----/----" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-4t-agfsr", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "AC0620" + }, + { + "type": "day", + "code": "DEP_DAY", + "label": "Departure Day", + "value": "7" + }, + { + "type": "day", + "code": "ARR_DAY", + "label": "Arrival Day", + "value": "8" + }, + { + "type": "iata", + "code": "ORG", + "label": "Origin", + "value": "YYZ" + }, + { + "type": "iata", + "code": "DST", + "label": "Destination", + "value": "YHZ" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "03:40:00" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "44.585 N, 70.277 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "35000 feet" + } + ] + }, + "raw": { + "flight_number": "AC0620", + "departure_day": 7, + "arrival_day": 8, + "departure_iata": "YYZ", + "arrival_iata": "YHZ", + "message_timestamp": 13200, + "position": { + "latitude": 44.585, + "longitude": -70.27666666666667 + }, + "altitude": 35000 + }, + "remaining": { + "text": "453/ /0063/0035/ /281065/----/ /512/0240/0253/----/----" + } + } +} diff --git a/corpus/labels/4T/AGFSR/sample-002.json b/corpus/labels/4T/AGFSR/sample-002.json new file mode 100644 index 0000000..b77bbed --- /dev/null +++ b/corpus/labels/4T/AGFSR/sample-002.json @@ -0,0 +1,86 @@ +{ + "spec": "labels/4T/AGFSR", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "4T", + "text": "AGFSR AC0096/07/08/YULGRU/0354Z/833/4417.8N07232.6W/254/CLIMB /0565/0033/-33/275049/0289/144/453/0319/0339/****/****" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-4t-agfsr", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "AC0096" + }, + { + "type": "day", + "code": "DEP_DAY", + "label": "Departure Day", + "value": "7" + }, + { + "type": "day", + "code": "ARR_DAY", + "label": "Arrival Day", + "value": "8" + }, + { + "type": "iata", + "code": "ORG", + "label": "Origin", + "value": "YUL" + }, + { + "type": "iata", + "code": "DST", + "label": "Destination", + "value": "GRU" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "03:54:00" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "44.297 N, 71.457 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "25400 feet" + } + ] + }, + "raw": { + "flight_number": "AC0096", + "departure_day": 7, + "arrival_day": 8, + "departure_iata": "YUL", + "arrival_iata": "GRU", + "message_timestamp": 14040, + "position": { + "latitude": 44.29666666666667, + "longitude": -71.45666666666666 + }, + "altitude": 25400 + }, + "remaining": { + "text": "833/CLIMB /0565/0033/-33/275049/0289/144/453/0319/0339/****/****" + } + } +} diff --git a/corpus/labels/4T/AGFSR/sample-003.json b/corpus/labels/4T/AGFSR/sample-003.json new file mode 100644 index 0000000..91b40f5 --- /dev/null +++ b/corpus/labels/4T/AGFSR/sample-003.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/4T/AGFSR", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "4T", + "text": "POS/ Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-4t-agfsr", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Position Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "POS/ Bogus message" + } + } +} diff --git a/corpus/labels/4T/ETA/sample-001.json b/corpus/labels/4T/ETA/sample-001.json new file mode 100644 index 0000000..f9b4f45 --- /dev/null +++ b/corpus/labels/4T/ETA/sample-001.json @@ -0,0 +1,60 @@ +{ + "spec": "labels/4T/ETA", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "4T", + "text": "ETA AC7221/13/14 YYZ 0902Z" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-4t-eta", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "ETA Report", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "AC7221" + }, + { + "type": "day", + "code": "DEP_DAY", + "label": "Departure Day", + "value": "13" + }, + { + "type": "day", + "code": "ARR_DAY", + "label": "Arrival Day", + "value": "14" + }, + { + "type": "iata", + "code": "DST", + "label": "Destination", + "value": "YYZ" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "09:02:00" + } + ] + }, + "raw": { + "flight_number": "AC7221", + "departure_day": 13, + "arrival_day": 14, + "arrival_iata": "YYZ", + "eta_time": 32520 + }, + "remaining": {} + } +} diff --git a/corpus/labels/4T/ETA/sample-002.json b/corpus/labels/4T/ETA/sample-002.json new file mode 100644 index 0000000..4f51c22 --- /dev/null +++ b/corpus/labels/4T/ETA/sample-002.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/4T/ETA", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "4T", + "text": "ETA Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-4t-eta", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "ETA Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "ETA Bogus message" + } + } +} diff --git a/corpus/labels/58/sample-001.json b/corpus/labels/58/sample-001.json new file mode 100644 index 0000000..a3b0463 --- /dev/null +++ b/corpus/labels/58/sample-001.json @@ -0,0 +1,65 @@ +{ + "spec": "labels/58", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "58", + "text": "OG0704/06/230942/N39.214/W76.106/22683/N/" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-58", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "OG0704" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "6" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "23:09:42" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "39.214 N, 76.106 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "22683 feet" + } + ] + }, + "raw": { + "flight_number": "OG0704", + "day": 6, + "message_timestamp": 83382, + "position": { + "latitude": 39.214, + "longitude": -76.106 + }, + "altitude": 22683 + }, + "remaining": { + "text": "N/" + } + } +} diff --git a/corpus/labels/58/sample-002.json b/corpus/labels/58/sample-002.json new file mode 100644 index 0000000..86038c4 --- /dev/null +++ b/corpus/labels/58/sample-002.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/58", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "58", + "text": "Bogus/message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-58", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Position Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "Bogus/message" + } + } +} diff --git a/corpus/labels/5Z/Slash/sample-001.json b/corpus/labels/5Z/Slash/sample-001.json new file mode 100644 index 0000000..1c8c69d --- /dev/null +++ b/corpus/labels/5Z/Slash/sample-001.json @@ -0,0 +1,32 @@ +{ + "spec": "labels/5Z/Slash", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "5Z", + "text": "/TXT\r\nDID U GET THE TIMES" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-5z-slash", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Airline Designated Downlink", + "items": [ + { + "type": "text", + "code": "TEXT", + "label": "Text Message", + "value": "DID U GET THE TIMES" + } + ] + }, + "raw": { + "text": "DID U GET THE TIMES" + }, + "remaining": {} + } +} diff --git a/corpus/labels/5Z/Slash/sample-002.json b/corpus/labels/5Z/Slash/sample-002.json new file mode 100644 index 0000000..06c5a9a --- /dev/null +++ b/corpus/labels/5Z/Slash/sample-002.json @@ -0,0 +1,63 @@ +{ + "spec": "labels/5Z/Slash", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "5Z", + "text": "/B3 ATLIAD 14 R1C G1273" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-5z-slash", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Designated Downlink", + "items": [ + { + "type": "airline", + "code": "AIRLINE", + "label": "Airline", + "value": "United Airlines" + }, + { + "type": "message_type", + "code": "MSG_TYPE", + "label": "Message Type", + "value": "Request Departure Clearance (B3)" + }, + { + "type": "iata", + "code": "ORG", + "label": "Origin", + "value": "ATL" + }, + { + "type": "iata", + "code": "DST", + "label": "Destination", + "value": "IAD" + }, + { + "type": "runway", + "code": "ARWY", + "label": "Arrival Runway", + "value": "1C" + } + ] + }, + "raw": { + "airline": "United Airlines", + "message_type": "B3", + "departure_iata": "ATL", + "arrival_iata": "IAD", + "day": 14, + "arrival_runway": "1C" + }, + "remaining": { + "text": "G1273" + } + } +} diff --git a/corpus/labels/5Z/Slash/sample-003.json b/corpus/labels/5Z/Slash/sample-003.json new file mode 100644 index 0000000..03f6a72 --- /dev/null +++ b/corpus/labels/5Z/Slash/sample-003.json @@ -0,0 +1,61 @@ +{ + "spec": "labels/5Z/Slash", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "5Z", + "text": "/B3 DCAORD 14 R27C" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-5z-slash", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Airline Designated Downlink", + "items": [ + { + "type": "airline", + "code": "AIRLINE", + "label": "Airline", + "value": "United Airlines" + }, + { + "type": "message_type", + "code": "MSG_TYPE", + "label": "Message Type", + "value": "Request Departure Clearance (B3)" + }, + { + "type": "iata", + "code": "ORG", + "label": "Origin", + "value": "DCA" + }, + { + "type": "iata", + "code": "DST", + "label": "Destination", + "value": "ORD" + }, + { + "type": "runway", + "code": "ARWY", + "label": "Arrival Runway", + "value": "27C" + } + ] + }, + "raw": { + "airline": "United Airlines", + "message_type": "B3", + "departure_iata": "DCA", + "arrival_iata": "ORD", + "day": 14, + "arrival_runway": "27C" + }, + "remaining": {} + } +} diff --git a/corpus/labels/5Z/Slash/sample-004.json b/corpus/labels/5Z/Slash/sample-004.json new file mode 100644 index 0000000..2ca1bde --- /dev/null +++ b/corpus/labels/5Z/Slash/sample-004.json @@ -0,0 +1,70 @@ +{ + "spec": "labels/5Z/Slash", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 4", + "input": { + "label": "5Z", + "text": "/B3 TO DATA REQ / KIAH KBOS 14 152532 R4R /---- BOPT/OFF C0.000/1 LNO G1600" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-5z-slash", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Designated Downlink", + "items": [ + { + "type": "airline", + "code": "AIRLINE", + "label": "Airline", + "value": "United Airlines" + }, + { + "type": "message_type", + "code": "MSG_TYPE", + "label": "Message Type", + "value": "Request Departure Clearance (B3)" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KIAH" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KBOS" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "15:25:32" + }, + { + "type": "runway", + "code": "ARWY", + "label": "Arrival Runway", + "value": "4R" + } + ] + }, + "raw": { + "airline": "United Airlines", + "message_type": "B3", + "departure_icao": "KIAH", + "arrival_icao": "KBOS", + "day": 14, + "message_timestamp": 55532, + "arrival_runway": "4R" + }, + "remaining": { + "text": "---- BOPT/OFF C0.000/1 LNO G1600" + } + } +} diff --git a/corpus/labels/5Z/Slash/sample-005.json b/corpus/labels/5Z/Slash/sample-005.json new file mode 100644 index 0000000..787c467 --- /dev/null +++ b/corpus/labels/5Z/Slash/sample-005.json @@ -0,0 +1,70 @@ +{ + "spec": "labels/5Z/Slash", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 5", + "input": { + "label": "5Z", + "text": "/ET EXP TIME / KEWR KBNA 20 122559/EON 1336 AUTO" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-5z-slash", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Designated Downlink", + "items": [ + { + "type": "airline", + "code": "AIRLINE", + "label": "Airline", + "value": "United Airlines" + }, + { + "type": "message_type", + "code": "MSG_TYPE", + "label": "Message Type", + "value": "Expected Time (ET)" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KEWR" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KBNA" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "12:25:59" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "13:36:00" + } + ] + }, + "raw": { + "airline": "United Airlines", + "message_type": "ET", + "departure_icao": "KEWR", + "arrival_icao": "KBNA", + "day": 20, + "message_timestamp": 44759, + "eta_time": 48960 + }, + "remaining": { + "text": "AUTO" + } + } +} diff --git a/corpus/labels/5Z/Slash/sample-006.json b/corpus/labels/5Z/Slash/sample-006.json new file mode 100644 index 0000000..a4d3e0a --- /dev/null +++ b/corpus/labels/5Z/Slash/sample-006.json @@ -0,0 +1,53 @@ +{ + "spec": "labels/5Z/Slash", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 6", + "input": { + "label": "5Z", + "text": "/C3 IADDFW" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-5z-slash", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Airline Designated Downlink", + "items": [ + { + "type": "airline", + "code": "AIRLINE", + "label": "Airline", + "value": "United Airlines" + }, + { + "type": "message_type", + "code": "MSG_TYPE", + "label": "Message Type", + "value": "Off Message (C3)" + }, + { + "type": "iata", + "code": "ORG", + "label": "Origin", + "value": "IAD" + }, + { + "type": "iata", + "code": "DST", + "label": "Destination", + "value": "DFW" + } + ] + }, + "raw": { + "airline": "United Airlines", + "message_type": "C3", + "departure_iata": "IAD", + "arrival_iata": "DFW" + }, + "remaining": {} + } +} diff --git a/corpus/labels/5Z/Slash/sample-007.json b/corpus/labels/5Z/Slash/sample-007.json new file mode 100644 index 0000000..9429dc9 --- /dev/null +++ b/corpus/labels/5Z/Slash/sample-007.json @@ -0,0 +1,63 @@ +{ + "spec": "labels/5Z/Slash", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 7", + "input": { + "label": "5Z", + "text": "/C3 GATE REQ / KBNA KEWR 22 115400 0554 ---- ---- ---- ----" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-5z-slash", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Designated Downlink", + "items": [ + { + "type": "airline", + "code": "AIRLINE", + "label": "Airline", + "value": "United Airlines" + }, + { + "type": "message_type", + "code": "MSG_TYPE", + "label": "Message Type", + "value": "Off Message (C3)" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KBNA" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KEWR" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "11:54:00" + } + ] + }, + "raw": { + "airline": "United Airlines", + "message_type": "C3", + "departure_icao": "KBNA", + "arrival_icao": "KEWR", + "day": 22, + "message_timestamp": 42840 + }, + "remaining": { + "text": "0554 ---- ---- ---- ----" + } + } +} diff --git a/corpus/labels/5Z/Slash/sample-008.json b/corpus/labels/5Z/Slash/sample-008.json new file mode 100644 index 0000000..5981a29 --- /dev/null +++ b/corpus/labels/5Z/Slash/sample-008.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/5Z/Slash", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 8", + "input": { + "label": "5Z", + "text": "/ Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-5z-slash", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Airline Designated Downlink", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "/ Bogus message" + } + } +} diff --git a/corpus/labels/80/sample-001.json b/corpus/labels/80/sample-001.json new file mode 100644 index 0000000..1395c5f --- /dev/null +++ b/corpus/labels/80/sample-001.json @@ -0,0 +1,100 @@ +{ + "spec": "labels/80", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "80", + "text": "3N01 POSRPT 5891/04 KIAH/MMGL .XA-VOI\r\n/POS N29395W095133/ALT +15608/MCH 558/FOB 0100/ETA 0410" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-80", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Defined Position Report", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "5891" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "4" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KIAH" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "MMGL" + }, + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "XA-VOI" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "29.395 N, 95.133 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "15608 feet" + }, + { + "type": "mach", + "code": "MACH", + "label": "Mach Number", + "value": "0.558 mach" + }, + { + "type": "fuel_on_board", + "code": "FOB", + "label": "Fuel On Board", + "value": "100" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "04:10:00" + } + ] + }, + "raw": { + "flight_number": "5891", + "day": 4, + "departure_icao": "KIAH", + "arrival_icao": "MMGL", + "tail": "XA-VOI", + "position": { + "latitude": 29.395, + "longitude": -95.133 + }, + "altitude": 15608, + "mach": 0.558, + "fuel_on_board": 100, + "eta_time": 15000 + }, + "remaining": { + "text": "3N01 POSRPT/" + } + } +} diff --git a/corpus/labels/80/sample-002.json b/corpus/labels/80/sample-002.json new file mode 100644 index 0000000..7f4718d --- /dev/null +++ b/corpus/labels/80/sample-002.json @@ -0,0 +1,122 @@ +{ + "spec": "labels/80", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "80", + "text": "3N01 POSRPT 0581/27 KIAD/MSLP .N962AV/04H 11:02\r\n/NWYP CIGAR /HDG 233/MCH 782\r\n/POS N3539.2W07937.2/FL 360/TAS 445/SAT -060\r\n/SWND 110/DWND 306/FOB N009414/ETA 14:26.0 " + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-80", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Defined Position Report", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "0581" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "27" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KIAD" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "MSLP" + }, + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "N962AV" + }, + { + "type": "heading", + "code": "HDG", + "label": "Heading", + "value": "233" + }, + { + "type": "mach", + "code": "MACH", + "label": "Mach Number", + "value": "0.782 mach" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "35.392 N, 79.372 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "36000 feet" + }, + { + "type": "airspeed", + "code": "ASPD", + "label": "True Airspeed", + "value": "445 knots" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-60 degrees" + }, + { + "type": "fuel_on_board", + "code": "FOB", + "label": "Fuel On Board", + "value": "9414" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "14:26:00" + } + ] + }, + "raw": { + "flight_number": "0581", + "day": 27, + "departure_icao": "KIAD", + "arrival_icao": "MSLP", + "tail": "N962AV", + "next_waypoint": "CIGAR", + "heading": 233, + "mach": 0.782, + "position": { + "latitude": 35.391999999999996, + "longitude": -79.372 + }, + "altitude": 36000, + "airspeed": 445, + "outside_air_temperature": -60, + "fuel_on_board": 9414, + "eta_time": 51960 + }, + "remaining": { + "text": "3N01 POSRPT/04H 11:02////SWND 110/DWND 306" + } + } +} diff --git a/corpus/labels/80/sample-003.json b/corpus/labels/80/sample-003.json new file mode 100644 index 0000000..5b7e88e --- /dev/null +++ b/corpus/labels/80/sample-003.json @@ -0,0 +1,107 @@ +{ + "spec": "labels/80", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "80", + "text": "/FB 0105/AD KCHS/N3950.1,W07548.3,3P01 POSRPT 0267/20 KBOS/KCHS .N3275J\n/UTC 143605/POS N3950.1 W07548.3/ALT 38007\n/SPD 334/FOB 0105/ETA 1622" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-80", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Defined Position Report", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "0267" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "20" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KBOS" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KCHS" + }, + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "N3275J" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "14:36:05" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "39.501 N, 75.483 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "38007 feet" + }, + { + "type": "aircraft_groundspeed", + "code": "GSPD", + "label": "Aircraft Groundspeed", + "value": "334 knots" + }, + { + "type": "fuel_on_board", + "code": "FOB", + "label": "Fuel On Board", + "value": "105" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "16:22:00" + } + ] + }, + "raw": { + "flight_number": "0267", + "day": 20, + "departure_icao": "KBOS", + "arrival_icao": "KCHS", + "tail": "N3275J", + "message_timestamp": 52565, + "position": { + "latitude": 39.501, + "longitude": -75.483 + }, + "altitude": 38007, + "groundspeed": 334, + "fuel_on_board": 105, + "eta_time": 58920 + }, + "remaining": { + "text": "N3950.1/W07548.3 3P01 POSRPT//" + } + } +} diff --git a/corpus/labels/80/sample-004.json b/corpus/labels/80/sample-004.json new file mode 100644 index 0000000..2cfc38b --- /dev/null +++ b/corpus/labels/80/sample-004.json @@ -0,0 +1,65 @@ +{ + "spec": "labels/80", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 4", + "input": { + "label": "80", + "text": "3C01 POS N39328W077307 ,,143700, , , ,P47,124,0069" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-80", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Defined Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "39.328 N, 77.307 W" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "14:37:00" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "47 degrees" + }, + { + "type": "airspeed", + "code": "ASPD", + "label": "True Airspeed", + "value": "124 knots" + }, + { + "type": "fuel_on_board", + "code": "FOB", + "label": "Fuel On Board", + "value": "69" + } + ] + }, + "raw": { + "position": { + "latitude": 39.328, + "longitude": -77.307 + }, + "message_timestamp": 52620, + "outside_air_temperature": 47, + "airspeed": 124, + "fuel_on_board": 69 + }, + "remaining": { + "text": "3C01 POS,, , " + } + } +} diff --git a/corpus/labels/80/sample-005.json b/corpus/labels/80/sample-005.json new file mode 100644 index 0000000..bcbbd8b --- /dev/null +++ b/corpus/labels/80/sample-005.json @@ -0,0 +1,62 @@ +{ + "spec": "labels/80", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 5", + "input": { + "label": "80", + "text": "3M01 OPNORM 0411/20 KEWR/MMMX .XA-MAT " + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-80", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Defined Position Report", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "0411" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "20" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KEWR" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "MMMX" + }, + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "XA-MAT" + } + ] + }, + "raw": { + "flight_number": "0411", + "day": 20, + "departure_icao": "KEWR", + "arrival_icao": "MMMX", + "tail": "XA-MAT" + }, + "remaining": { + "text": "3M01 OPNORM" + } + } +} diff --git a/corpus/labels/80/sample-006.json b/corpus/labels/80/sample-006.json new file mode 100644 index 0000000..6320ecb --- /dev/null +++ b/corpus/labels/80/sample-006.json @@ -0,0 +1,69 @@ +{ + "spec": "labels/80", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 6", + "input": { + "label": "80", + "text": "3701 INRANG 3451/20 KSBD/KBWI .N613AZ\n/ETA 1254/ERT " + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-80", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Defined Position Report", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "3451" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "20" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KSBD" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KBWI" + }, + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "N613AZ" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "12:54:00" + } + ] + }, + "raw": { + "flight_number": "3451", + "day": 20, + "departure_icao": "KSBD", + "arrival_icao": "KBWI", + "tail": "N613AZ", + "eta_time": 46440 + }, + "remaining": { + "text": "3701 INRANG//ERT" + } + } +} diff --git a/corpus/labels/80/sample-007.json b/corpus/labels/80/sample-007.json new file mode 100644 index 0000000..a889c8c --- /dev/null +++ b/corpus/labels/80/sample-007.json @@ -0,0 +1,23 @@ +{ + "spec": "labels/80", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 7", + "input": { + "label": "80", + "text": "INR/ID91511S,,/DC04032026,143534/MR19,/NR,,,,,,,,950,0/ET041505/FB983/VR32BF4C" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-80", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Airline Defined Position Report", + "items": [] + }, + "raw": {}, + "remaining": {} + } +} diff --git a/corpus/labels/80/sample-008.json b/corpus/labels/80/sample-008.json new file mode 100644 index 0000000..b30b0a0 --- /dev/null +++ b/corpus/labels/80/sample-008.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/80", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 8", + "input": { + "label": "80", + "text": "3N01 POSRPT Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-80", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Airline Defined Position Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "3N01 POSRPT Bogus message" + } + } +} diff --git a/corpus/labels/80/sample-009.json b/corpus/labels/80/sample-009.json new file mode 100644 index 0000000..4123a51 --- /dev/null +++ b/corpus/labels/80/sample-009.json @@ -0,0 +1,62 @@ +{ + "spec": "labels/80", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 9", + "input": { + "label": "80", + "text": "3D01 RMPSRV 2501/02 LEMD/MMUN .EC-NOI\n/LAV Y/CAB Y/MEDA N/SEC N/WAS N/WAT N/FUEL N/WCHRR 01/WCHRS --/WCHRC --/UMNR --/MAAS N\nBUENAS NOCHES, ETA 0055Z Y NOS HAN PEDIDO UNA WCHR QUE AL PARECER NO TENIAMOS CONSTANCIA DE ELLA, POR SI PODEIS PEDIRLO A OPS CUN. MUCHAS GRACIAS" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-80", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Defined Position Report", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "2501" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "2" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "LEMD" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "MMUN" + }, + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "EC-NOI" + } + ] + }, + "raw": { + "flight_number": "2501", + "day": 2, + "departure_icao": "LEMD", + "arrival_icao": "MMUN", + "tail": "EC-NOI" + }, + "remaining": { + "text": "3D01 RMPSRV//LAV Y/CAB Y/MEDA N/SEC N/WAS N/WAT N/FUEL N/WCHRR 01/WCHRS --/WCHRC --/UMNR --/MAAS N/BUENAS NOCHES, ETA 0055Z Y NOS HAN PEDIDO UNA WCHR QUE AL PARECER NO TENIAMOS CONSTANCIA DE ELLA, POR SI PODEIS PEDIRLO A OPS CUN. MUCHAS GRACIAS" + } + } +} diff --git a/corpus/labels/83/sample-001.json b/corpus/labels/83/sample-001.json new file mode 100644 index 0000000..5091684 --- /dev/null +++ b/corpus/labels/83/sample-001.json @@ -0,0 +1,74 @@ +{ + "spec": "labels/83", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "83", + "text": "KIAH,RJAA,110012, 39.12,-175.10,39001,265,-107.6, 64900" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-83", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Defined", + "items": [ + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KIAH" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "RJAA" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "39.120 N, 175.100 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "39001 feet" + }, + { + "type": "aircraft_groundspeed", + "code": "GSPD", + "label": "Aircraft Groundspeed", + "value": "265 knots" + }, + { + "type": "heading", + "code": "HDG", + "label": "Heading", + "value": "-107.6" + } + ] + }, + "raw": { + "departure_icao": "KIAH", + "arrival_icao": "RJAA", + "day": "11", + "time": "0012", + "position": { + "latitude": 39.12, + "longitude": -175.1 + }, + "altitude": 39001, + "groundspeed": 265, + "heading": -107.6 + }, + "remaining": { + "text": "64900" + } + } +} diff --git a/corpus/labels/83/sample-002.json b/corpus/labels/83/sample-002.json new file mode 100644 index 0000000..8a002c5 --- /dev/null +++ b/corpus/labels/83/sample-002.json @@ -0,0 +1,45 @@ +{ + "spec": "labels/83", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "83", + "text": "001PR11013423N0556.6E11603.0000000----" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-83", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Defined", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "5.943 N, 116.050 E" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "0 feet" + } + ] + }, + "raw": { + "day": "11", + "position": { + "latitude": 5.943333333333333, + "longitude": 116.05 + }, + "altitude": 0 + }, + "remaining": { + "text": "0----" + } + } +} diff --git a/corpus/labels/83/sample-003.json b/corpus/labels/83/sample-003.json new file mode 100644 index 0000000..1694b39 --- /dev/null +++ b/corpus/labels/83/sample-003.json @@ -0,0 +1,74 @@ +{ + "spec": "labels/83", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "83", + "text": "KLAX,KEWR,220103, 40.53,- 74.47, 3836,212, 140.0, 19700" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-83", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Defined", + "items": [ + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KLAX" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KEWR" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "40.530 N, 74.470 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "3836 feet" + }, + { + "type": "aircraft_groundspeed", + "code": "GSPD", + "label": "Aircraft Groundspeed", + "value": "212 knots" + }, + { + "type": "heading", + "code": "HDG", + "label": "Heading", + "value": "140" + } + ] + }, + "raw": { + "departure_icao": "KLAX", + "arrival_icao": "KEWR", + "day": "22", + "time": "0103", + "position": { + "latitude": 40.53, + "longitude": -74.47 + }, + "altitude": 3836, + "groundspeed": 212, + "heading": 140 + }, + "remaining": { + "text": "19700" + } + } +} diff --git a/corpus/labels/83/sample-004.json b/corpus/labels/83/sample-004.json new file mode 100644 index 0000000..831153e --- /dev/null +++ b/corpus/labels/83/sample-004.json @@ -0,0 +1,56 @@ +{ + "spec": "labels/83", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 4", + "input": { + "label": "83", + "text": "4DH3 ETAT2 0907/22 ENGM/KEWR .LN-RKO\r\n/ETA 1641" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-83", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Defined", + "items": [ + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "ENGM" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KEWR" + }, + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "LN-RKO" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "16:41:00" + } + ] + }, + "raw": { + "day": "22", + "departure_icao": "ENGM", + "arrival_icao": "KEWR", + "tail": "LN-RKO", + "eta_time": 60060 + }, + "remaining": { + "text": "0907" + } + } +} diff --git a/corpus/labels/83/sample-005.json b/corpus/labels/83/sample-005.json new file mode 100644 index 0000000..7599db8 --- /dev/null +++ b/corpus/labels/83/sample-005.json @@ -0,0 +1,45 @@ +{ + "spec": "labels/83", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 5", + "input": { + "label": "83", + "text": "001PR22035539N4038.6W07427.80292500008" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-83", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Airline Defined", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "40.643 N, 74.463 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "2925 feet" + } + ] + }, + "raw": { + "day": "22", + "position": { + "latitude": 40.64333333333333, + "longitude": -74.46333333333334 + }, + "altitude": 2925 + }, + "remaining": { + "text": "00008" + } + } +} diff --git a/corpus/labels/83/sample-006.json b/corpus/labels/83/sample-006.json new file mode 100644 index 0000000..51e0496 --- /dev/null +++ b/corpus/labels/83/sample-006.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/83", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 6", + "input": { + "label": "83", + "text": "83 Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-83", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Airline Defined", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "83 Bogus message" + } + } +} diff --git a/corpus/labels/8E/sample-001.json b/corpus/labels/8E/sample-001.json new file mode 100644 index 0000000..34e6ecf --- /dev/null +++ b/corpus/labels/8E/sample-001.json @@ -0,0 +1,39 @@ +{ + "spec": "labels/8E", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "8E", + "text": "EGSS,1618" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-8e", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "ETA Report", + "items": [ + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "16:18:00" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "EGSS" + } + ] + }, + "raw": { + "eta_time": 58680, + "arrival_icao": "EGSS" + }, + "remaining": {} + } +} diff --git a/corpus/labels/H1/ATIS/sample-001.json b/corpus/labels/H1/ATIS/sample-001.json new file mode 100644 index 0000000..ec638b6 --- /dev/null +++ b/corpus/labels/H1/ATIS/sample-001.json @@ -0,0 +1,53 @@ +{ + "spec": "labels/H1/ATIS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "H1", + "text": "L95AQF0073/KSFO.TI2/030KSFOAFF5C" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-atis", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "ATIS Subscription", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "QF0073" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KSFO" + }, + { + "type": "atis", + "code": "ATIS_CODE", + "label": "ATIS Code", + "value": "030" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0xff5c" + } + ] + }, + "raw": { + "flight_number": "QF0073", + "arrival_icao": "KSFO", + "atis_code": "030", + "checksum": 720732 + }, + "remaining": {} + } +} diff --git a/corpus/labels/H1/ATIS/sample-002.json b/corpus/labels/H1/ATIS/sample-002.json new file mode 100644 index 0000000..e923425 --- /dev/null +++ b/corpus/labels/H1/ATIS/sample-002.json @@ -0,0 +1,53 @@ +{ + "spec": "labels/H1/ATIS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "H1", + "text": "L01AMC5477/OERK.TI2/024OERKC8781" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-atis", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "ATIS Subscription", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "MC5477" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "OERK" + }, + { + "type": "atis", + "code": "ATIS_CODE", + "label": "ATIS Code", + "value": "024" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x8781" + } + ] + }, + "raw": { + "flight_number": "MC5477", + "arrival_icao": "OERK", + "atis_code": "024", + "checksum": 821121 + }, + "remaining": {} + } +} diff --git a/corpus/labels/H1/ATIS/sample-003.json b/corpus/labels/H1/ATIS/sample-003.json new file mode 100644 index 0000000..c672412 --- /dev/null +++ b/corpus/labels/H1/ATIS/sample-003.json @@ -0,0 +1,53 @@ +{ + "spec": "labels/H1/ATIS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "H1", + "text": "L11AMC5477/LICC.TI2/024LICCAFBD9" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-atis", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "ATIS Subscription", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "MC5477" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "LICC" + }, + { + "type": "atis", + "code": "ATIS_CODE", + "label": "ATIS Code", + "value": "024" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0xfbd9" + } + ] + }, + "raw": { + "flight_number": "MC5477", + "arrival_icao": "LICC", + "atis_code": "024", + "checksum": 719833 + }, + "remaining": {} + } +} diff --git a/corpus/labels/H1/ATIS/sample-004.json b/corpus/labels/H1/ATIS/sample-004.json new file mode 100644 index 0000000..5aabafd --- /dev/null +++ b/corpus/labels/H1/ATIS/sample-004.json @@ -0,0 +1,53 @@ +{ + "spec": "labels/H1/ATIS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 4", + "input": { + "label": "5Z", + "text": "L95AQF0073/KSFO.TI2/030KSFOAFF5C" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-atis", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "ATIS Subscription", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "QF0073" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KSFO" + }, + { + "type": "atis", + "code": "ATIS_CODE", + "label": "ATIS Code", + "value": "030" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0xff5c" + } + ] + }, + "raw": { + "flight_number": "QF0073", + "arrival_icao": "KSFO", + "atis_code": "030", + "checksum": 720732 + }, + "remaining": {} + } +} diff --git a/corpus/labels/H1/ATIS/sample-005.json b/corpus/labels/H1/ATIS/sample-005.json new file mode 100644 index 0000000..5a48a01 --- /dev/null +++ b/corpus/labels/H1/ATIS/sample-005.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/H1/ATIS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 5", + "input": { + "label": "H1", + "text": "LINVALID MESSAGE" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-h1-atis", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "ATIS Subscription", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "LINVALID MESSAGE" + } + } +} diff --git a/corpus/labels/H1/ATIS/sample-006.json b/corpus/labels/H1/ATIS/sample-006.json new file mode 100644 index 0000000..d2e0673 --- /dev/null +++ b/corpus/labels/H1/ATIS/sample-006.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/H1/ATIS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 6", + "input": { + "label": "H1", + "text": "FLR/FR24030411230034583106FWC2" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-h1-atis", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "ATIS Subscription", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "FLR/FR24030411230034583106FWC2" + } + } +} diff --git a/corpus/labels/H1/EZF/sample-001.json b/corpus/labels/H1/EZF/sample-001.json new file mode 100644 index 0000000..9b863f3 --- /dev/null +++ b/corpus/labels/H1/EZF/sample-001.json @@ -0,0 +1,120 @@ +{ + "spec": "labels/H1/EZF", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "H1", + "text": "EZF\nNO0246/10/EI-NEO\n/C28Y331/3/9\n-SCT/MXP-ZNZ\n-STD/2200\n-FLT STATUS/CLOSED\n-UOM/KG\n-ZFW/162075\n-PAX/305\n-PXT/122/160/19/02\n-PXW/22601\n-CGO/3573\n-BAG/4783\n-OTH/4336\n-TTL/35293\n-FWT/51063\n-TOW/213138\n-DOW/126782" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-ezf", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Load Sheet", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "NO0246" + }, + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "EI-NEO" + }, + { + "type": "iata", + "code": "ORG", + "label": "Origin", + "value": "MXP" + }, + { + "type": "iata", + "code": "DST", + "label": "Destination", + "value": "ZNZ" + }, + { + "type": "loadsheet", + "code": "STD", + "label": "Scheduled Time of Departure", + "value": "2200" + }, + { + "type": "loadsheet", + "code": "FLIGHT_STATUS", + "label": "Flight Status", + "value": "CLOSED" + }, + { + "type": "loadsheet", + "code": "UOM", + "label": "Unit of Measure", + "value": "KG" + }, + { + "type": "loadsheet", + "code": "ZFW", + "label": "Zero Fuel Weight", + "value": "162075" + }, + { + "type": "loadsheet", + "code": "PAX", + "label": "Passengers", + "value": "305" + }, + { + "type": "loadsheet", + "code": "TOW", + "label": "Takeoff Weight", + "value": "213138" + }, + { + "type": "loadsheet", + "code": "DOW", + "label": "Dry Operating Weight", + "value": "126782" + }, + { + "type": "loadsheet", + "code": "FUEL_WEIGHT", + "label": "Fuel Weight", + "value": "51063" + } + ] + }, + "raw": { + "flight_number": "NO0246", + "tail": "EI-NEO", + "departure_iata": "MXP", + "arrival_iata": "ZNZ", + "loadsheet": { + "SCT": "MXP-ZNZ", + "STD": "2200", + "FLT STATUS": "CLOSED", + "UOM": "KG", + "ZFW": "162075", + "PAX": "305", + "PXT": "122/160/19/02", + "PXW": "22601", + "CGO": "3573", + "BAG": "4783", + "OTH": "4336", + "TTL": "35293", + "FWT": "51063", + "TOW": "213138", + "DOW": "126782" + } + }, + "remaining": { + "text": "/C28Y331/3/9" + } + } +} diff --git a/corpus/labels/H1/EZF/sample-002.json b/corpus/labels/H1/EZF/sample-002.json new file mode 100644 index 0000000..ae0c9c1 --- /dev/null +++ b/corpus/labels/H1/EZF/sample-002.json @@ -0,0 +1,70 @@ +{ + "spec": "labels/H1/EZF", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "H1", + "text": "EZF\nAB1234/05/D-ABCD\n-SCT/JFK-LAX\n-PAX/180\n-ZFW/55000" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-ezf", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Load Sheet", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "AB1234" + }, + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "D-ABCD" + }, + { + "type": "iata", + "code": "ORG", + "label": "Origin", + "value": "JFK" + }, + { + "type": "iata", + "code": "DST", + "label": "Destination", + "value": "LAX" + }, + { + "type": "loadsheet", + "code": "ZFW", + "label": "Zero Fuel Weight", + "value": "55000" + }, + { + "type": "loadsheet", + "code": "PAX", + "label": "Passengers", + "value": "180" + } + ] + }, + "raw": { + "flight_number": "AB1234", + "tail": "D-ABCD", + "departure_iata": "JFK", + "arrival_iata": "LAX", + "loadsheet": { + "SCT": "JFK-LAX", + "PAX": "180", + "ZFW": "55000" + } + }, + "remaining": {} + } +} diff --git a/corpus/labels/H1/EZF/sample-003.json b/corpus/labels/H1/EZF/sample-003.json new file mode 100644 index 0000000..a6a339c --- /dev/null +++ b/corpus/labels/H1/EZF/sample-003.json @@ -0,0 +1,63 @@ +{ + "spec": "labels/H1/EZF", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "1M", + "text": "EZF\nXY5678/12/G-TEST\n-SCT/LHR-CDG\n-PAX/90" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-ezf", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Load Sheet", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "XY5678" + }, + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "G-TEST" + }, + { + "type": "iata", + "code": "ORG", + "label": "Origin", + "value": "LHR" + }, + { + "type": "iata", + "code": "DST", + "label": "Destination", + "value": "CDG" + }, + { + "type": "loadsheet", + "code": "PAX", + "label": "Passengers", + "value": "90" + } + ] + }, + "raw": { + "flight_number": "XY5678", + "tail": "G-TEST", + "departure_iata": "LHR", + "arrival_iata": "CDG", + "loadsheet": { + "SCT": "LHR-CDG", + "PAX": "90" + } + }, + "remaining": {} + } +} diff --git a/corpus/labels/H1/EZF/sample-004.json b/corpus/labels/H1/EZF/sample-004.json new file mode 100644 index 0000000..8b97ca2 --- /dev/null +++ b/corpus/labels/H1/EZF/sample-004.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/H1/EZF", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 4", + "input": { + "label": "H1", + "text": "NOT_EZF data here" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-h1-ezf", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Load Sheet", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "NOT_EZF data here" + } + } +} diff --git a/corpus/labels/H1/EZF/sample-005.json b/corpus/labels/H1/EZF/sample-005.json new file mode 100644 index 0000000..9a5db95 --- /dev/null +++ b/corpus/labels/H1/EZF/sample-005.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/H1/EZF", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 5", + "input": { + "label": "H1", + "text": "EZF" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-h1-ezf", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Load Sheet", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "EZF" + } + } +} diff --git a/corpus/labels/H1/FLR/sample-001.json b/corpus/labels/H1/FLR/sample-001.json new file mode 100644 index 0000000..fced4f0 --- /dev/null +++ b/corpus/labels/H1/FLR/sample-001.json @@ -0,0 +1,35 @@ +{ + "spec": "labels/H1/FLR", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "H1", + "text": "FLR/FR24030411230034583106FWC2 :NO DATA FROM GPS1 /IDECAM 2 ,ECAM 1 " + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-flr", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Fault Log Report", + "items": [ + { + "type": "fault", + "code": "FR", + "label": "Fault Report", + "value": "FWC2 :NO DATA FROM GPS1 /IDECAM 2 ,ECAM 1 " + } + ] + }, + "raw": { + "message_timestamp": 1709551380, + "fault_message": "FWC2 :NO DATA FROM GPS1 /IDECAM 2 ,ECAM 1 " + }, + "remaining": { + "text": "34583106" + } + } +} diff --git a/corpus/labels/H1/FLR/sample-002.json b/corpus/labels/H1/FLR/sample-002.json new file mode 100644 index 0000000..e97f279 --- /dev/null +++ b/corpus/labels/H1/FLR/sample-002.json @@ -0,0 +1,35 @@ +{ + "spec": "labels/H1/FLR", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "H1", + "text": "FLR/PNRC12860AA07/FR24030411040023723406CDSU(9RA)/DU SD(4WT2) /IDEIS 1 " + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-flr", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Fault Log Report", + "items": [ + { + "type": "fault", + "code": "FR", + "label": "Fault Report", + "value": "CDSU(9RA)/DU SD(4WT2) /IDEIS 1 " + } + ] + }, + "raw": { + "message_timestamp": 1709550240, + "fault_message": "CDSU(9RA)/DU SD(4WT2) /IDEIS 1 " + }, + "remaining": { + "text": "PNRC12860AA07/23723406" + } + } +} diff --git a/corpus/labels/H1/FLR/sample-003.json b/corpus/labels/H1/FLR/sample-003.json new file mode 100644 index 0000000..60735dd --- /dev/null +++ b/corpus/labels/H1/FLR/sample-003.json @@ -0,0 +1,35 @@ +{ + "spec": "labels/H1/FLR", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "H1", + "text": "FLR/FR24030409560034423306RA1/IDEFCS 1 ,EFCS 2 " + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-flr", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Fault Log Report", + "items": [ + { + "type": "fault", + "code": "FR", + "label": "Fault Report", + "value": "RA1/IDEFCS 1 ,EFCS 2 " + } + ] + }, + "raw": { + "message_timestamp": 1709546160, + "fault_message": "RA1/IDEFCS 1 ,EFCS 2 " + }, + "remaining": { + "text": "34423306" + } + } +} diff --git a/corpus/labels/H1/FLR/sample-004.json b/corpus/labels/H1/FLR/sample-004.json new file mode 100644 index 0000000..3b89906 --- /dev/null +++ b/corpus/labels/H1/FLR/sample-004.json @@ -0,0 +1,35 @@ +{ + "spec": "labels/H1/FLR", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 4", + "input": { + "label": "H1", + "text": "FLR/FR24030410030038316206LIQD LVL XMTR (40MG)/IDTOILET " + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-flr", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Fault Log Report", + "items": [ + { + "type": "fault", + "code": "FR", + "label": "Fault Report", + "value": "LIQD LVL XMTR (40MG)/IDTOILET " + } + ] + }, + "raw": { + "message_timestamp": 1709546580, + "fault_message": "LIQD LVL XMTR (40MG)/IDTOILET " + }, + "remaining": { + "text": "38316206" + } + } +} diff --git a/corpus/labels/H1/FLR/sample-005.json b/corpus/labels/H1/FLR/sample-005.json new file mode 100644 index 0000000..71061ab --- /dev/null +++ b/corpus/labels/H1/FLR/sample-005.json @@ -0,0 +1,35 @@ +{ + "spec": "labels/H1/FLR", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 5", + "input": { + "label": "H1", + "text": "#CFBFLR/FR24030412400034723406ATC1(1SH1)/TCAS(1000SG) /IDTCAS " + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-flr", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Fault Log Report", + "items": [ + { + "type": "fault", + "code": "FR", + "label": "Fault Report", + "value": "ATC1(1SH1)/TCAS(1000SG) /IDTCAS " + } + ] + }, + "raw": { + "message_timestamp": 1709556000, + "fault_message": "ATC1(1SH1)/TCAS(1000SG) /IDTCAS " + }, + "remaining": { + "text": "34723406" + } + } +} diff --git a/corpus/labels/H1/FLR/sample-006.json b/corpus/labels/H1/FLR/sample-006.json new file mode 100644 index 0000000..453d816 --- /dev/null +++ b/corpus/labels/H1/FLR/sample-006.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/H1/FLR", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 6", + "input": { + "label": "H1", + "text": "FLR " + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-h1-flr", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Fault Log Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "FLR " + } + } +} diff --git a/corpus/labels/H1/M_POS/sample-001.json b/corpus/labels/H1/M_POS/sample-001.json new file mode 100644 index 0000000..9e3c4bc --- /dev/null +++ b/corpus/labels/H1/M_POS/sample-001.json @@ -0,0 +1,86 @@ +{ + "spec": "labels/H1/M_POS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "H1", + "text": "M85AQF0073YSSY,KSFO,101621,- 4.9985,-169.9820,35003,290, 35.1, 44100,S05W169,S02W167,1645" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-m-pos", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "M-Series Periodic Position Report", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "QF0073" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "YSSY" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KSFO" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "10" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "16:21:00" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "4.998 S, 169.982 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "35003 feet" + }, + { + "type": "heading", + "code": "HDG", + "label": "Heading", + "value": "290" + } + ] + }, + "raw": { + "flight_number": "QF0073", + "departure_icao": "YSSY", + "arrival_icao": "KSFO", + "day": 10, + "message_timestamp": 58860, + "position": { + "latitude": -4.9985, + "longitude": -169.982 + }, + "altitude": 35003, + "heading": 290 + }, + "remaining": { + "text": " 35.1, 44100,S05W169,S02W167,1645" + } + } +} diff --git a/corpus/labels/H1/M_POS/sample-002.json b/corpus/labels/H1/M_POS/sample-002.json new file mode 100644 index 0000000..464c0de --- /dev/null +++ b/corpus/labels/H1/M_POS/sample-002.json @@ -0,0 +1,86 @@ +{ + "spec": "labels/H1/M_POS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "H1", + "text": "M87AQF0073YSSY,KSFO,101702, 0.7144,-166.0643,36000,282, 34.1, 40200,S00W166,N03W162,1739" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-m-pos", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "M-Series Periodic Position Report", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "QF0073" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "YSSY" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KSFO" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "10" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "17:02:00" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "0.714 N, 166.064 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "36000 feet" + }, + { + "type": "heading", + "code": "HDG", + "label": "Heading", + "value": "282" + } + ] + }, + "raw": { + "flight_number": "QF0073", + "departure_icao": "YSSY", + "arrival_icao": "KSFO", + "day": 10, + "message_timestamp": 61320, + "position": { + "latitude": 0.7144, + "longitude": -166.0643 + }, + "altitude": 36000, + "heading": 282 + }, + "remaining": { + "text": " 34.1, 40200,S00W166,N03W162,1739" + } + } +} diff --git a/corpus/labels/H1/M_POS/sample-003.json b/corpus/labels/H1/M_POS/sample-003.json new file mode 100644 index 0000000..ec38393 --- /dev/null +++ b/corpus/labels/H1/M_POS/sample-003.json @@ -0,0 +1,86 @@ +{ + "spec": "labels/H1/M_POS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "H1", + "text": "M89AQF0073YSSY,KSFO,101819, 7.2016,-158.1146,37001,275, 33.5, 32700,N07W158,N11W153,1900" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-m-pos", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "M-Series Periodic Position Report", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "QF0073" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "YSSY" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KSFO" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "10" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "18:19:00" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "7.202 N, 158.115 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "37001 feet" + }, + { + "type": "heading", + "code": "HDG", + "label": "Heading", + "value": "275" + } + ] + }, + "raw": { + "flight_number": "QF0073", + "departure_icao": "YSSY", + "arrival_icao": "KSFO", + "day": 10, + "message_timestamp": 65940, + "position": { + "latitude": 7.2016, + "longitude": -158.1146 + }, + "altitude": 37001, + "heading": 275 + }, + "remaining": { + "text": " 33.5, 32700,N07W158,N11W153,1900" + } + } +} diff --git a/corpus/labels/H1/M_POS/sample-004.json b/corpus/labels/H1/M_POS/sample-004.json new file mode 100644 index 0000000..e34e974 --- /dev/null +++ b/corpus/labels/H1/M_POS/sample-004.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/H1/M_POS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 4", + "input": { + "label": "H1", + "text": "FLR/FR24030411230034583106FWC2" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-h1-m-pos", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "M-Series Periodic Position Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "FLR/FR24030411230034583106FWC2" + } + } +} diff --git a/corpus/labels/H1/M_POS/sample-005.json b/corpus/labels/H1/M_POS/sample-005.json new file mode 100644 index 0000000..c75fb9d --- /dev/null +++ b/corpus/labels/H1/M_POS/sample-005.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/H1/M_POS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 5", + "input": { + "label": "H1", + "text": "M85AQF0073YSSY,KSFO" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-h1-m-pos", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "M-Series Periodic Position Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "M85AQF0073YSSY,KSFO" + } + } +} diff --git a/corpus/labels/H1/M_POS/sample-006.json b/corpus/labels/H1/M_POS/sample-006.json new file mode 100644 index 0000000..81a648c --- /dev/null +++ b/corpus/labels/H1/M_POS/sample-006.json @@ -0,0 +1,26 @@ +{ + "spec": "labels/H1/M_POS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 6", + "input": { + "label": "H1", + "sublabel": "DF", + "text": "A350,000354,1,1,TB000000/REP035,01,02;H01,035,01,02,4000,00066,.F-HREV,3,0,08,12,19,06,58,42,071/H02,KSFO LFPO,FBU711 ,S0285,S0385,S0142S0185/H03, /A10,06,58,40,+26535,+01198,045,+0493,321723,XXXXXX,--------,0423,+0573/A11,06,58,32,+26356,+01221,041,+0502,324326,XXXXXX,--------,0424,+0614/:" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-h1-m-pos", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "M-Series Periodic Position Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "A350,000354,1,1,TB000000/REP035,01,02;H01,035,01,02,4000,00066,.F-HREV,3,0,08,12,19,06,58,42,071/H02,KSFO LFPO,FBU711 ,S0285,S0385,S0142S0185/H03, /A10,06,58,40,+26535,+01198,045,+0493,321723,XXXXXX,--------,0423,+0573/A11,06,58,32,+26356,+01221,041,+0502,324326,XXXXXX,--------,0424,+0614/:" + } + } +} diff --git a/corpus/labels/H1/M_POS/sample-007.json b/corpus/labels/H1/M_POS/sample-007.json new file mode 100644 index 0000000..df23540 --- /dev/null +++ b/corpus/labels/H1/M_POS/sample-007.json @@ -0,0 +1,26 @@ +{ + "spec": "labels/H1/M_POS", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 7", + "input": { + "label": "H1", + "sublabel": "DF", + "text": "A350,000354,1,1,TB000000/REP035,01,02;H01,035,01,02,4000,00066,.F-HREV,3,0,08,12,19,06,58,42,071/H02,KSFO LFPO,FBU711 ,S0285,S0385,S0142S0185/H03, /A10,06,58,40,+26535,+01198,045,+0493,321723,XXXXXX,--------,0423,+0573/A11,06,58,32,+26356,+01221,041,+0502,324326,XXXXXX,--------,0424,+0614/:" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-h1-m-pos", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "M-Series Periodic Position Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "A350,000354,1,1,TB000000/REP035,01,02;H01,035,01,02,4000,00066,.F-HREV,3,0,08,12,19,06,58,42,071/H02,KSFO LFPO,FBU711 ,S0285,S0385,S0142S0185/H03, /A10,06,58,40,+26535,+01198,045,+0493,321723,XXXXXX,--------,0423,+0573/A11,06,58,32,+26356,+01221,041,+0502,324326,XXXXXX,--------,0424,+0614/:" + } + } +} diff --git a/corpus/labels/H1/OFP/sample-001.json b/corpus/labels/H1/OFP/sample-001.json new file mode 100644 index 0000000..58da469 --- /dev/null +++ b/corpus/labels/H1/OFP/sample-001.json @@ -0,0 +1,26 @@ +{ + "spec": "labels/H1/OFP", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "H1", + "sublabel": "DF", + "text": "A350,000354,1,1,TB000000/REP035,01,02;H01,035,01,02,4000,00066,.F-HREV,3,0,08,12,19,06,58,42,071/H02,KSFO LFPO,FBU711 ,S0285,S0385,S0142S0185/H03, /A10,06,58,40,+26535,+01198,045,+0493,321723,XXXXXX,--------,0423,+0573/A11,06,58,32,+26356,+01221,041,+0502,324326,XXXXXX,--------,0424,+0614/:" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-h1-ofp", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Operational Flight Plan", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "A350,000354,1,1,TB000000/REP035,01,02;H01,035,01,02,4000,00066,.F-HREV,3,0,08,12,19,06,58,42,071/H02,KSFO LFPO,FBU711 ,S0285,S0385,S0142S0185/H03, /A10,06,58,40,+26535,+01198,045,+0493,321723,XXXXXX,--------,0423,+0573/A11,06,58,32,+26356,+01221,041,+0502,324326,XXXXXX,--------,0424,+0614/:" + } + } +} diff --git a/corpus/labels/H1/OFP/sample-002.json b/corpus/labels/H1/OFP/sample-002.json new file mode 100644 index 0000000..45be5a9 --- /dev/null +++ b/corpus/labels/H1/OFP/sample-002.json @@ -0,0 +1,164 @@ +{ + "spec": "labels/H1/OFP", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "H1", + "text": "(FPL-CCA769-IS\n-B77W/H-SDE3FGHIJ1J2J3J4J5M1P2RWXYZ/LB1D1\n-VESPA2354\n-N0482F350 DCT ENI DCT OAK DCT BURGL IRNMN2\n-KLAX0117 KSFO\n-PBN/A1B1C1D1L1O2S2T1 NAV/ABAS RNP2 DAT/1FANSE SUR/RSP180\n DOF/260310 REG/B2036\n EET/KZLA0051\n SEL/BRCE CODE/780970\n RMK/ACAS II)" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-ofp", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Operational Flight Plan", + "items": [ + { + "type": "callsign", + "code": "CALLSIGN", + "label": "Callsign", + "value": "CCA769" + }, + { + "type": "aircraft_type", + "code": "ACFT", + "label": "Aircraft Type", + "value": "B77W" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "VESPA" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KLAX" + }, + { + "type": "icao", + "code": "ALT_DST", + "label": "Alternate Destination", + "value": "KSFO" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "DCT > ENI > DCT > OAK > DCT > BURGL > IRNMN2" + }, + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "B2036" + }, + { + "type": "flight_rules", + "code": "RULES", + "label": "Flight Rules", + "value": "IFR" + }, + { + "type": "cruise", + "code": "CRUISE", + "label": "Cruise Speed/Level", + "value": "N0482 F350" + } + ] + }, + "raw": { + "callsign": "CCA769", + "departure_icao": "VESPA", + "arrival_icao": "KLAX", + "alternate_icao": "KSFO", + "route": { + "waypoints": [ + { + "name": "DCT" + }, + { + "name": "ENI" + }, + { + "name": "DCT" + }, + { + "name": "OAK" + }, + { + "name": "DCT" + }, + { + "name": "BURGL" + }, + { + "name": "IRNMN2" + } + ] + }, + "tail": "B2036", + "icao_fpl": { + "callsign": "CCA769", + "flightRules": "I", + "flightType": "S", + "aircraftType": "B77W", + "wakeTurbulence": "H", + "equipment": "SDE3FGHIJ1J2J3J4J5M1P2RWXYZ", + "surveillance": "LB1D1", + "departure": "VESPA", + "departureTime": "2354", + "cruiseSpeed": "N0482", + "cruiseLevel": "F350", + "route": { + "waypoints": [ + { + "name": "DCT" + }, + { + "name": "ENI" + }, + { + "name": "DCT" + }, + { + "name": "OAK" + }, + { + "name": "DCT" + }, + { + "name": "BURGL" + }, + { + "name": "IRNMN2" + } + ] + }, + "destination": "KLAX", + "eet": "0117", + "alternates": [ + "KSFO" + ], + "otherInfo": { + "PBN": "A1B1C1D1L1O2S2T1", + "NAV": "ABAS RNP2", + "DAT": "1FANSE", + "SUR": "RSP180", + "DOF": "260310", + "REG": "B2036", + "EET": "KZLA0051", + "SEL": "BRCE", + "CODE": "780970", + "RMK": "ACAS II" + } + } + }, + "remaining": {} + } +} diff --git a/corpus/labels/H1/OFP/sample-003.json b/corpus/labels/H1/OFP/sample-003.json new file mode 100644 index 0000000..9aa0e59 --- /dev/null +++ b/corpus/labels/H1/OFP/sample-003.json @@ -0,0 +1,145 @@ +{ + "spec": "labels/H1/OFP", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "H1", + "text": "(FPL-UAL123-IS-B789/H-SDE2E3FGHIJ1J5M1RWXYZ/LB1D1-KSFO0100-N0487F370 DCT PORTE J584 ENI DCT-KLAX0055 KONT-PBN/A1B1C1D1L1 DOF/260311 REG/N22992)" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-ofp", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Operational Flight Plan", + "items": [ + { + "type": "callsign", + "code": "CALLSIGN", + "label": "Callsign", + "value": "UAL123" + }, + { + "type": "aircraft_type", + "code": "ACFT", + "label": "Aircraft Type", + "value": "B789" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KSFO" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KLAX" + }, + { + "type": "icao", + "code": "ALT_DST", + "label": "Alternate Destination", + "value": "KONT" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "DCT > PORTE > J584 > ENI > DCT" + }, + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "N22992" + }, + { + "type": "flight_rules", + "code": "RULES", + "label": "Flight Rules", + "value": "IFR" + }, + { + "type": "cruise", + "code": "CRUISE", + "label": "Cruise Speed/Level", + "value": "N0487 F370" + } + ] + }, + "raw": { + "callsign": "UAL123", + "departure_icao": "KSFO", + "arrival_icao": "KLAX", + "alternate_icao": "KONT", + "route": { + "waypoints": [ + { + "name": "DCT" + }, + { + "name": "PORTE" + }, + { + "name": "J584" + }, + { + "name": "ENI" + }, + { + "name": "DCT" + } + ] + }, + "tail": "N22992", + "icao_fpl": { + "callsign": "UAL123", + "flightRules": "I", + "flightType": "S", + "aircraftType": "B789", + "wakeTurbulence": "H", + "equipment": "SDE2E3FGHIJ1J5M1RWXYZ", + "surveillance": "LB1D1", + "departure": "KSFO", + "departureTime": "0100", + "cruiseSpeed": "N0487", + "cruiseLevel": "F370", + "route": { + "waypoints": [ + { + "name": "DCT" + }, + { + "name": "PORTE" + }, + { + "name": "J584" + }, + { + "name": "ENI" + }, + { + "name": "DCT" + } + ] + }, + "destination": "KLAX", + "eet": "0055", + "alternates": [ + "KONT" + ], + "otherInfo": { + "PBN": "A1B1C1D1L1", + "DOF": "260311", + "REG": "N22992" + } + } + }, + "remaining": {} + } +} diff --git a/corpus/labels/H1/OFP/sample-004.json b/corpus/labels/H1/OFP/sample-004.json new file mode 100644 index 0000000..7db4f2e --- /dev/null +++ b/corpus/labels/H1/OFP/sample-004.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/H1/OFP", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 4", + "input": { + "label": "H1", + "text": "FPN/RI:DA:KSFO:AA:KLAX:F:KSFO..KLAX" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-h1-ofp", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Operational Flight Plan", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "FPN/RI:DA:KSFO:AA:KLAX:F:KSFO..KLAX" + } + } +} diff --git a/corpus/labels/H1/OFP/sample-005.json b/corpus/labels/H1/OFP/sample-005.json new file mode 100644 index 0000000..ea173dd --- /dev/null +++ b/corpus/labels/H1/OFP/sample-005.json @@ -0,0 +1,145 @@ +{ + "spec": "labels/H1/OFP", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 5", + "input": { + "label": "1M", + "text": "(FPL-UAL123-IS-B789/H-SDE2E3FGHIJ1J5M1RWXYZ/LB1D1-KSFO0100-N0487F370 DCT PORTE J584 ENI DCT-KLAX0055 KONT-PBN/A1B1C1D1L1 DOF/260311 REG/N22992)" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-ofp", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Operational Flight Plan", + "items": [ + { + "type": "callsign", + "code": "CALLSIGN", + "label": "Callsign", + "value": "UAL123" + }, + { + "type": "aircraft_type", + "code": "ACFT", + "label": "Aircraft Type", + "value": "B789" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KSFO" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KLAX" + }, + { + "type": "icao", + "code": "ALT_DST", + "label": "Alternate Destination", + "value": "KONT" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "DCT > PORTE > J584 > ENI > DCT" + }, + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "N22992" + }, + { + "type": "flight_rules", + "code": "RULES", + "label": "Flight Rules", + "value": "IFR" + }, + { + "type": "cruise", + "code": "CRUISE", + "label": "Cruise Speed/Level", + "value": "N0487 F370" + } + ] + }, + "raw": { + "callsign": "UAL123", + "departure_icao": "KSFO", + "arrival_icao": "KLAX", + "alternate_icao": "KONT", + "route": { + "waypoints": [ + { + "name": "DCT" + }, + { + "name": "PORTE" + }, + { + "name": "J584" + }, + { + "name": "ENI" + }, + { + "name": "DCT" + } + ] + }, + "tail": "N22992", + "icao_fpl": { + "callsign": "UAL123", + "flightRules": "I", + "flightType": "S", + "aircraftType": "B789", + "wakeTurbulence": "H", + "equipment": "SDE2E3FGHIJ1J5M1RWXYZ", + "surveillance": "LB1D1", + "departure": "KSFO", + "departureTime": "0100", + "cruiseSpeed": "N0487", + "cruiseLevel": "F370", + "route": { + "waypoints": [ + { + "name": "DCT" + }, + { + "name": "PORTE" + }, + { + "name": "J584" + }, + { + "name": "ENI" + }, + { + "name": "DCT" + } + ] + }, + "destination": "KLAX", + "eet": "0055", + "alternates": [ + "KONT" + ], + "otherInfo": { + "PBN": "A1B1C1D1L1", + "DOF": "260311", + "REG": "N22992" + } + } + }, + "remaining": {} + } +} diff --git a/corpus/labels/H1/OFP/sample-006.json b/corpus/labels/H1/OFP/sample-006.json new file mode 100644 index 0000000..d32dd14 --- /dev/null +++ b/corpus/labels/H1/OFP/sample-006.json @@ -0,0 +1,26 @@ +{ + "spec": "labels/H1/OFP", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 6", + "input": { + "label": "H1", + "sublabel": "DF", + "text": "A350,000354,1,1,TB000000/REP035,01,02;H01,035,01,02,4000,00066,.F-HREV,3,0,08,12,19,06,58,42,071/H02,KSFO LFPO,FBU711 ,S0285,S0385,S0142S0185/H03, /A10,06,58,40,+26535,+01198,045,+0493,321723,XXXXXX,--------,0423,+0573/A11,06,58,32,+26356,+01221,041,+0502,324326,XXXXXX,--------,0424,+0614/:" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-h1-ofp", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Operational Flight Plan", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "A350,000354,1,1,TB000000/REP035,01,02;H01,035,01,02,4000,00066,.F-HREV,3,0,08,12,19,06,58,42,071/H02,KSFO LFPO,FBU711 ,S0285,S0385,S0142S0185/H03, /A10,06,58,40,+26535,+01198,045,+0493,321723,XXXXXX,--------,0423,+0573/A11,06,58,32,+26356,+01221,041,+0502,324326,XXXXXX,--------,0424,+0614/:" + } + } +} diff --git a/corpus/labels/H1/OHMA/sample-001.json b/corpus/labels/H1/OHMA/sample-001.json new file mode 100644 index 0000000..01c7ba0 --- /dev/null +++ b/corpus/labels/H1/OHMA/sample-001.json @@ -0,0 +1,32 @@ +{ + "spec": "labels/H1/OHMA", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "H1", + "text": "OHMAeJy1Ul2PojAU/SubPiuhlA/ljQHMkAElgDuTXTakqx2nCaAp1WQz8b/vFXTGEcw+LX26veece88p7+jARMO3NbKRpqhohCrWNHTDoH7P0arkrJbBOkd2jhaPkZOjUX6BeFSytqGpmj5WJ2NVyzC2ydTWiWJN9B8teE0lBRSIUS52Ja1ZA+VPqCXl5Xxf/WaiVZkTyzD0bsB2zcr2si1fS755kxfamu2okHvBHNDbCukCuMU+PSzSFk+F4Ada9vqLxLvSuxq9dELDtK56IdukEqZkvOpbVG0VXIJFYnYW2QFCuqzXFm5Jm+YcG2xLKyYFX32CB3V70XXQPzv2EX8R+Vn3BrxuJK1XH2HW9KxYyRgWv5gDJCSx72CnOH8dR1/RLhXl9+4f6MF1xRhgJPuSNUzeI1nE+hY5L7ZtO4/RmFjRQwj+4MNqCJf4pNhp/reosggXbhjEke8Fzrxwk2WQ+kUSZ8U/wgNi5kcxhpy9G2PENBVtqt+kcSZoQ4QJViyTDBO8YDYb4GBDwQTfUOIIx4mfpsvEH1xMhzHmfc7gbkTR1R7HcZ9m4eL53m5YmeAhTjA/jbrDMhULT08P3p0jOv4FG0dfvQ==" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-ohma", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "OHMA Message", + "items": [ + { + "type": "ohma", + "code": "OHMA", + "label": "OHMA Downlink", + "value": "{\n \"clientId\": \"OHMA\",\n \"messageDate\": \"2024-08-02T11:39:43.784Z\",\n \"data\": {\n \"airplanes\": [\n {\n \"tailNumber\": \"N37554\",\n \"model\": \"\",\n \"flights\": [\n {\n \"departureAirportCode\": \"KBOS\",\n \"arrivalAirportCode\": \"KORD\",\n \"flightNumber\": \"UAL567\",\n \"flightLegStartTime\": \"2024-08-02T10:01:33.736Z\",\n \"events\": [\n {\n \"eventClassId\": \"parametric\",\n \"eventTime\": \"2024-08-02T11:39:43.784Z\",\n \"eventType\": \"OHMA_META\",\n \"instances\": [\n {\n \"name\": \"mtPartNumber\",\n \"values\": [\n \"\"\n ]\n },\n {\n \"name\": \"mtCarlVersion\",\n \"values\": [\n \"4.5\"\n ]\n },\n {\n \"name\": \"mtRulesetVersion\",\n \"values\": [\n \"737 MAX:::AHM-37MBL8-000010L:::1\"\n ]\n }\n ]\n },\n {\n \"eventClassId\": \"parametric\",\n \"eventTime\": \"2024-08-02T11:39:43.784Z\",\n \"eventType\": \"TM1_CLIPMEDIAN_CRUISE_RPT_A\",\n \"instances\": [\n {\n \"name\": \"TM1TEMP1_MED\",\n \"values\": [\n 366.294\n ]\n },\n {\n \"name\": \"TM1TEMP2_MED\",\n \"values\": [\n 381.763\n ]\n },\n {\n \"name\": \"TM1TEMPDIFF_MED\",\n \"values\": [\n 15.131\n ]\n },\n {\n \"name\": \"PM1PRESSURE1_MED\",\n \"values\": [\n 34.766\n ]\n },\n {\n \"name\": \"PM1PRESSURE2_MED\",\n \"values\": [\n 33.406\n ]\n },\n {\n \"name\": \"PACKFLOWDIFF_MED\",\n \"values\": [\n 11.816\n ]\n },\n {\n \"name\": \"PACKINPRESDIFF_MED\",\n \"values\": [\n 6.719\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n}" + } + ] + }, + "raw": { + "ohma": "{\"version\":\"2.0\",\"message\":\"{\\\"clientId\\\":\\\"OHMA\\\",\\\"messageDate\\\":\\\"2024-08-02T11:39:43.784Z\\\",\\\"data\\\":{\\\"airplanes\\\":[{\\\"tailNumber\\\":\\\"N37554\\\",\\\"model\\\":\\\"\\\",\\\"flights\\\":[{\\\"departureAirportCode\\\":\\\"KBOS\\\",\\\"arrivalAirportCode\\\":\\\"KORD\\\",\\\"flightNumber\\\":\\\"UAL567\\\",\\\"flightLegStartTime\\\":\\\"2024-08-02T10:01:33.736Z\\\",\\\"events\\\":[{\\\"eventClassId\\\":\\\"parametric\\\",\\\"eventTime\\\":\\\"2024-08-02T11:39:43.784Z\\\",\\\"eventType\\\":\\\"OHMA_META\\\",\\\"instances\\\":[{\\\"name\\\":\\\"mtPartNumber\\\",\\\"values\\\":[\\\"\\\"]},{\\\"name\\\":\\\"mtCarlVersion\\\",\\\"values\\\":[\\\"4.5\\\"]},{\\\"name\\\":\\\"mtRulesetVersion\\\",\\\"values\\\":[\\\"737 MAX:::AHM-37MBL8-000010L:::1\\\"]}]},{\\\"eventClassId\\\":\\\"parametric\\\",\\\"eventTime\\\":\\\"2024-08-02T11:39:43.784Z\\\",\\\"eventType\\\":\\\"TM1_CLIPMEDIAN_CRUISE_RPT_A\\\",\\\"instances\\\":[{\\\"name\\\":\\\"TM1TEMP1_MED\\\",\\\"values\\\":[366.294]},{\\\"name\\\":\\\"TM1TEMP2_MED\\\",\\\"values\\\":[381.763]},{\\\"name\\\":\\\"TM1TEMPDIFF_MED\\\",\\\"values\\\":[15.131]},{\\\"name\\\":\\\"PM1PRESSURE1_MED\\\",\\\"values\\\":[34.766]},{\\\"name\\\":\\\"PM1PRESSURE2_MED\\\",\\\"values\\\":[33.406]},{\\\"name\\\":\\\"PACKFLOWDIFF_MED\\\",\\\"values\\\":[11.816]},{\\\"name\\\":\\\"PACKINPRESDIFF_MED\\\",\\\"values\\\":[6.719]}]}]}]}]}}\"}" + }, + "remaining": {} + } +} diff --git a/corpus/labels/H1/OHMA/sample-002.json b/corpus/labels/H1/OHMA/sample-002.json new file mode 100644 index 0000000..d6dea59 --- /dev/null +++ b/corpus/labels/H1/OHMA/sample-002.json @@ -0,0 +1,32 @@ +{ + "spec": "labels/H1/OHMA", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "H1", + "text": "/RTNBOCR.OHMAeJyrVipLLSrOzM9TslIy0jNQ0lHKTS0uTkxPBfJL81JS0zLzUlOUagH7TQzW" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-ohma", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "OHMA Message", + "items": [ + { + "type": "ohma", + "code": "OHMA", + "label": "OHMA Downlink", + "value": "undefined" + } + ] + }, + "raw": { + "ohma": "{\"version\":\"2.0\",\"message\":\"undefined\"}" + }, + "remaining": {} + } +} diff --git a/corpus/labels/H1/OHMA/sample-003.json b/corpus/labels/H1/OHMA/sample-003.json new file mode 100644 index 0000000..7ab87c5 --- /dev/null +++ b/corpus/labels/H1/OHMA/sample-003.json @@ -0,0 +1,32 @@ +{ + "spec": "labels/H1/OHMA", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "H1", + "text": "OHMAeJy1lF+PmkAUxb9Kw/MyGRj+v1HEYhaQ4OxqrI2Z6tSSIBpAk2bjd+8Aau3szCZ9qD4NnN899x7v+Kacad0Uh0rxFB1A5UnZ06YhO8rObytlUxa0aifbleKtlGmU+CvlaXWTjEhL+xc61A0Vuiq0MHQ90/UMBJBjL3vxlrSEqVgxUtTHklS0Ycev7NySokxP+++07qvMMj" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-ohma", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "OHMA Message", + "items": [ + { + "type": "ohma", + "code": "OHMA", + "label": "OHMA Downlink", + "value": "{\"version\":\"2.0\",\"message\":\"{\\\"clientId\\\":\\\"OHMA\\\",\\\"messageDate\\\":\\\"2024-09-06T09:59:43.387Z\\\",\\\"data\\\":{\\\"airplanes\\\":[{\\\"tailNumber\\\":\\\"SP" + } + ] + }, + "raw": { + "ohma": "{\"version\":\"2.0\",\"message\":\"{\\\"clientId\\\":\\\"OHMA\\\",\\\"messageDate\\\":\\\"2024-09-06T09:59:43.387Z\\\",\\\"data\\\":{\\\"airplanes\\\":[{\\\"tailNumber\\\":\\\"SP" + }, + "remaining": {} + } +} diff --git a/corpus/labels/H1/OHMA/sample-004.json b/corpus/labels/H1/OHMA/sample-004.json new file mode 100644 index 0000000..805265c --- /dev/null +++ b/corpus/labels/H1/OHMA/sample-004.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/H1/OHMA", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 4", + "input": { + "label": "H1", + "text": "OHMA " + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-h1-ohma", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "OHMA Message", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "OHMA " + } + } +} diff --git a/corpus/labels/H1/Paren/sample-001.json b/corpus/labels/H1/Paren/sample-001.json new file mode 100644 index 0000000..843def3 --- /dev/null +++ b/corpus/labels/H1/Paren/sample-001.json @@ -0,0 +1,72 @@ +{ + "spec": "labels/H1/Paren", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "H1", + "text": "(POS-KLM296 -3911N07600W/234212 F250\r\nRMK/FUEL 37.0 M0.69)" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-paren", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "KLM296" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "39.183 N, 76.000 W" + }, + { + "type": "time", + "code": "TIMESTAMP", + "label": "Message Timestamp", + "value": "23:42:12" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "25000 feet" + }, + { + "type": "fuel_on_board", + "code": "FOB", + "label": "Fuel On Board", + "value": "37" + }, + { + "type": "mach", + "code": "MACH", + "label": "Mach Number", + "value": "0.69 mach" + } + ] + }, + "raw": { + "flight_number": "KLM296", + "position": { + "latitude": 39.18333333333333, + "longitude": -76 + }, + "message_timestamp": 85332, + "altitude": 25000, + "fuel_on_board": 37, + "mach": 0.69 + }, + "remaining": { + "text": "RMK" + } + } +} diff --git a/corpus/labels/H1/Paren/sample-002.json b/corpus/labels/H1/Paren/sample-002.json new file mode 100644 index 0000000..7c9577f --- /dev/null +++ b/corpus/labels/H1/Paren/sample-002.json @@ -0,0 +1,23 @@ +{ + "spec": "labels/H1/Paren", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "H1", + "text": "Invalid" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-h1-paren", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Unknown", + "items": [] + }, + "raw": {}, + "remaining": {} + } +} diff --git a/corpus/labels/H1/Paren/sample-003.json b/corpus/labels/H1/Paren/sample-003.json new file mode 100644 index 0000000..abda60e --- /dev/null +++ b/corpus/labels/H1/Paren/sample-003.json @@ -0,0 +1,23 @@ +{ + "spec": "labels/H1/Paren", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "H1", + "text": "(Invalid)" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-h1-paren", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Unknown", + "items": [] + }, + "raw": {}, + "remaining": {} + } +} diff --git a/corpus/labels/H1/WRN/sample-001.json b/corpus/labels/H1/WRN/sample-001.json new file mode 100644 index 0000000..dec1ce7 --- /dev/null +++ b/corpus/labels/H1/WRN/sample-001.json @@ -0,0 +1,35 @@ +{ + "spec": "labels/H1/WRN", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "H1", + "text": "WRN/WN24030400580028000006FUEL L TK PUMP 1 2 LO PR " + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-wrn", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Warning Message", + "items": [ + { + "type": "warning", + "code": "WRN", + "label": "Warning Message", + "value": "FUEL L TK PUMP 1 2 LO PR " + } + ] + }, + "raw": { + "message_timestamp": 1709513880, + "warning_message": "FUEL L TK PUMP 1 2 LO PR " + }, + "remaining": { + "text": "28000006" + } + } +} diff --git a/corpus/labels/H1/WRN/sample-002.json b/corpus/labels/H1/WRN/sample-002.json new file mode 100644 index 0000000..1da1dc3 --- /dev/null +++ b/corpus/labels/H1/WRN/sample-002.json @@ -0,0 +1,35 @@ +{ + "spec": "labels/H1/WRN", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "H1", + "text": "WRN/PNRC12860AA07/WN24030316250034000006NAV ADS-B RPTG 1 FAULT " + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-wrn", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Warning Message", + "items": [ + { + "type": "warning", + "code": "WRN", + "label": "Warning Message", + "value": "NAV ADS-B RPTG 1 FAULT " + } + ] + }, + "raw": { + "message_timestamp": 1709483100, + "warning_message": "NAV ADS-B RPTG 1 FAULT " + }, + "remaining": { + "text": "PNRC12860AA07/34000006" + } + } +} diff --git a/corpus/labels/H1/WRN/sample-003.json b/corpus/labels/H1/WRN/sample-003.json new file mode 100644 index 0000000..480c105 --- /dev/null +++ b/corpus/labels/H1/WRN/sample-003.json @@ -0,0 +1,35 @@ +{ + "spec": "labels/H1/WRN", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "H1", + "text": "WRN/PNRC12860AA07/WN24030322050027000002F/CTL ELAC 1 FAULT " + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-wrn", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Warning Message", + "items": [ + { + "type": "warning", + "code": "WRN", + "label": "Warning Message", + "value": "F/CTL ELAC 1 FAULT " + } + ] + }, + "raw": { + "message_timestamp": 1709503500, + "warning_message": "F/CTL ELAC 1 FAULT " + }, + "remaining": { + "text": "PNRC12860AA07/27000002" + } + } +} diff --git a/corpus/labels/H1/WRN/sample-004.json b/corpus/labels/H1/WRN/sample-004.json new file mode 100644 index 0000000..5805242 --- /dev/null +++ b/corpus/labels/H1/WRN/sample-004.json @@ -0,0 +1,35 @@ +{ + "spec": "labels/H1/WRN", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 4", + "input": { + "label": "H1", + "text": "WRN/PNRC12860AA07/WN24030316580030210006ENG 1 A.ICE " + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-wrn", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Warning Message", + "items": [ + { + "type": "warning", + "code": "WRN", + "label": "Warning Message", + "value": "ENG 1 A.ICE " + } + ] + }, + "raw": { + "message_timestamp": 1709485080, + "warning_message": "ENG 1 A.ICE " + }, + "remaining": { + "text": "PNRC12860AA07/30210006" + } + } +} diff --git a/corpus/labels/H1/WRN/sample-005.json b/corpus/labels/H1/WRN/sample-005.json new file mode 100644 index 0000000..1e0cfc8 --- /dev/null +++ b/corpus/labels/H1/WRN/sample-005.json @@ -0,0 +1,35 @@ +{ + "spec": "labels/H1/WRN", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 5", + "input": { + "label": "H1", + "text": "#CFBWRN/WN24030312040034580006NAV GPS1 FAULT " + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h1-wrn", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Warning Message", + "items": [ + { + "type": "warning", + "code": "WRN", + "label": "Warning Message", + "value": "NAV GPS1 FAULT " + } + ] + }, + "raw": { + "message_timestamp": 1709467440, + "warning_message": "NAV GPS1 FAULT " + }, + "remaining": { + "text": "34580006" + } + } +} diff --git a/corpus/labels/H1/WRN/sample-006.json b/corpus/labels/H1/WRN/sample-006.json new file mode 100644 index 0000000..ba6b1a6 --- /dev/null +++ b/corpus/labels/H1/WRN/sample-006.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/H1/WRN", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 6", + "input": { + "label": "H1", + "text": "WRN " + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-h1-wrn", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Warning Message", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "WRN " + } + } +} diff --git a/corpus/labels/H2/02E/sample-001.json b/corpus/labels/H2/02E/sample-001.json new file mode 100644 index 0000000..6812ee4 --- /dev/null +++ b/corpus/labels/H2/02E/sample-001.json @@ -0,0 +1,176 @@ +{ + "spec": "labels/H2/02E", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "H2", + "text": "02E20HEGNLKPRN40359E02208116253601M627259020G QN41179E02134316323599M617247037G QN41591E02100516393603M610266040G QN42393E02026716463602M600276033G QN43197E01954316533598M592299037G QN44023E01929517003596M587313033G Q" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h2-02e", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Weather Report", + "items": [ + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "20" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "HEGN" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "LKPR" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "N40359E022081(40.598 N, 22.135 E)@16:25:00 at FL360: 259° at 20kt, -62.7°C at FL360" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "N41179E021343(41.298 N, 21.572 E)@16:32:00 at FL359: 247° at 37kt, -61.7°C at FL359" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "N41591E021005(41.985 N, 21.008 E)@16:39:00 at FL360: 266° at 40kt, -61°C at FL360" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "N42393E020267(42.655 N, 20.445 E)@16:46:00 at FL360: 276° at 33kt, -60°C at FL360" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "N43197E019543(43.328 N, 19.905 E)@16:53:00 at FL359: 299° at 37kt, -59.2°C at FL359" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "N44023E019295(44.038 N, 19.492 E)@17:00:00 at FL359: 313° at 33kt, -58.7°C at FL359" + } + ] + }, + "raw": { + "day": 20, + "departure_icao": "HEGN", + "arrival_icao": "LKPR", + "wind_data": [ + { + "waypoint": { + "name": "N40359E022081", + "latitude": 40.598333333333336, + "longitude": 22.135, + "time": 59100 + }, + "flightLevel": 360, + "windDirection": 259, + "windSpeed": 20, + "temperature": { + "flightLevel": 360, + "degreesC": -62.7 + } + }, + { + "waypoint": { + "name": "N41179E021343", + "latitude": 41.29833333333333, + "longitude": 21.571666666666665, + "time": 59520 + }, + "flightLevel": 359, + "windDirection": 247, + "windSpeed": 37, + "temperature": { + "flightLevel": 359, + "degreesC": -61.7 + } + }, + { + "waypoint": { + "name": "N41591E021005", + "latitude": 41.985, + "longitude": 21.008333333333333, + "time": 59940 + }, + "flightLevel": 360, + "windDirection": 266, + "windSpeed": 40, + "temperature": { + "flightLevel": 360, + "degreesC": -61 + } + }, + { + "waypoint": { + "name": "N42393E020267", + "latitude": 42.655, + "longitude": 20.445, + "time": 60360 + }, + "flightLevel": 360, + "windDirection": 276, + "windSpeed": 33, + "temperature": { + "flightLevel": 360, + "degreesC": -60 + } + }, + { + "waypoint": { + "name": "N43197E019543", + "latitude": 43.32833333333333, + "longitude": 19.905, + "time": 60780 + }, + "flightLevel": 359, + "windDirection": 299, + "windSpeed": 37, + "temperature": { + "flightLevel": 359, + "degreesC": -59.2 + } + }, + { + "waypoint": { + "name": "N44023E019295", + "latitude": 44.038333333333334, + "longitude": 19.491666666666667, + "time": 61200 + }, + "flightLevel": 359, + "windDirection": 313, + "windSpeed": 33, + "temperature": { + "flightLevel": 359, + "degreesC": -58.7 + } + } + ] + }, + "remaining": { + "text": "" + } + } +} diff --git a/corpus/labels/H2/02E/sample-002.json b/corpus/labels/H2/02E/sample-002.json new file mode 100644 index 0000000..933aee3 --- /dev/null +++ b/corpus/labels/H2/02E/sample-002.json @@ -0,0 +1,155 @@ +{ + "spec": "labels/H2/02E", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "H2", + "text": "02E20EGKKLBSFN45081E01757116493501M577327021G QN44401E01903016563499M575352028G QN44115E02008017033468M550319029G QN43420E02112317103296M525299036G QN43125E02214517172023M277271022G Q" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h2-02e", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Weather Report", + "items": [ + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "20" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "EGKK" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "LBSF" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "N45081E017571(45.135 N, 17.952 E)@16:49:00 at FL350: 327° at 21kt, -57.7°C at FL350" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "N44401E019030(44.668 N, 19.050 E)@16:56:00 at FL349: 352° at 28kt, -57.5°C at FL349" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "N44115E020080(44.192 N, 20.133 E)@17:03:00 at FL346: 319° at 29kt, -55°C at FL346" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "N43420E021123(43.700 N, 21.205 E)@17:10:00 at FL329: 299° at 36kt, -52.5°C at FL329" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "N43125E022145(43.208 N, 22.242 E)@17:17:00 at FL202: 271° at 22kt, -27.7°C at FL202" + } + ] + }, + "raw": { + "day": 20, + "departure_icao": "EGKK", + "arrival_icao": "LBSF", + "wind_data": [ + { + "waypoint": { + "name": "N45081E017571", + "latitude": 45.135, + "longitude": 17.951666666666668, + "time": 60540 + }, + "flightLevel": 350, + "windDirection": 327, + "windSpeed": 21, + "temperature": { + "flightLevel": 350, + "degreesC": -57.7 + } + }, + { + "waypoint": { + "name": "N44401E019030", + "latitude": 44.66833333333334, + "longitude": 19.05, + "time": 60960 + }, + "flightLevel": 349, + "windDirection": 352, + "windSpeed": 28, + "temperature": { + "flightLevel": 349, + "degreesC": -57.5 + } + }, + { + "waypoint": { + "name": "N44115E020080", + "latitude": 44.19166666666667, + "longitude": 20.133333333333333, + "time": 61380 + }, + "flightLevel": 346, + "windDirection": 319, + "windSpeed": 29, + "temperature": { + "flightLevel": 346, + "degreesC": -55 + } + }, + { + "waypoint": { + "name": "N43420E021123", + "latitude": 43.7, + "longitude": 21.205, + "time": 61800 + }, + "flightLevel": 329, + "windDirection": 299, + "windSpeed": 36, + "temperature": { + "flightLevel": 329, + "degreesC": -52.5 + } + }, + { + "waypoint": { + "name": "N43125E022145", + "latitude": 43.208333333333336, + "longitude": 22.241666666666667, + "time": 62220 + }, + "flightLevel": 202, + "windDirection": 271, + "windSpeed": 22, + "temperature": { + "flightLevel": 202, + "degreesC": -27.7 + } + } + ] + }, + "remaining": { + "text": "" + } + } +} diff --git a/corpus/labels/H2/02E/sample-003.json b/corpus/labels/H2/02E/sample-003.json new file mode 100644 index 0000000..c3fdadb --- /dev/null +++ b/corpus/labels/H2/02E/sample-003.json @@ -0,0 +1,134 @@ +{ + "spec": "labels/H2/02E", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "H2", + "text": "02E20EIDWKORDN44087W08505523383800M470251091G QN43210W08520623452813M442251113G QN42461W08539523522189M295256121G QN42380W08623723591780M227266100G Q" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-h2-02e", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Weather Report", + "items": [ + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "20" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "EIDW" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KORD" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "N44087W085055(44.145 N, 85.092 W)@23:38:00 at FL380: 251° at 91kt, -47°C at FL380" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "N43210W085206(43.350 N, 85.343 W)@23:45:00 at FL281: 251° at 113kt, -44.2°C at FL281" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "N42461W085395(42.768 N, 85.658 W)@23:52:00 at FL218: 256° at 121kt, -29.5°C at FL218" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "N42380W086237(42.633 N, 86.395 W)@23:59:00 at FL178: 266° at 100kt, -22.7°C at FL178" + } + ] + }, + "raw": { + "day": 20, + "departure_icao": "EIDW", + "arrival_icao": "KORD", + "wind_data": [ + { + "waypoint": { + "name": "N44087W085055", + "latitude": 44.145, + "longitude": -85.09166666666667, + "time": 85080 + }, + "flightLevel": 380, + "windDirection": 251, + "windSpeed": 91, + "temperature": { + "flightLevel": 380, + "degreesC": -47 + } + }, + { + "waypoint": { + "name": "N43210W085206", + "latitude": 43.35, + "longitude": -85.34333333333333, + "time": 85500 + }, + "flightLevel": 281, + "windDirection": 251, + "windSpeed": 113, + "temperature": { + "flightLevel": 281, + "degreesC": -44.2 + } + }, + { + "waypoint": { + "name": "N42461W085395", + "latitude": 42.76833333333333, + "longitude": -85.65833333333333, + "time": 85920 + }, + "flightLevel": 218, + "windDirection": 256, + "windSpeed": 121, + "temperature": { + "flightLevel": 218, + "degreesC": -29.5 + } + }, + { + "waypoint": { + "name": "N42380W086237", + "latitude": 42.63333333333333, + "longitude": -86.395, + "time": 86340 + }, + "flightLevel": 178, + "windDirection": 266, + "windSpeed": 100, + "temperature": { + "flightLevel": 178, + "degreesC": -22.7 + } + } + ] + }, + "remaining": { + "text": "" + } + } +} diff --git a/corpus/labels/H2/02E/sample-004.json b/corpus/labels/H2/02E/sample-004.json new file mode 100644 index 0000000..58491e5 --- /dev/null +++ b/corpus/labels/H2/02E/sample-004.json @@ -0,0 +1,25 @@ +{ + "spec": "labels/H2/02E", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 4", + "input": { + "label": "H2", + "text": "02E20INVALID MESSAGE TEXT" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-h2-02e", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Weather Report", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "02E20INVALID MESSAGE TEXT" + } + } +} diff --git a/corpus/labels/HX/sample-001.json b/corpus/labels/HX/sample-001.json new file mode 100644 index 0000000..51f7e22 --- /dev/null +++ b/corpus/labels/HX/sample-001.json @@ -0,0 +1,37 @@ +{ + "spec": "labels/HX", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "HX", + "text": "RA FMT LOCATION N4009.6 W07540.8" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-hx", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Undelivered Uplink Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "40.160 N, 75.680 W" + } + ] + }, + "raw": { + "position": { + "latitude": 40.16, + "longitude": -75.68 + } + }, + "remaining": { + "text": "" + } + } +} diff --git a/corpus/labels/HX/sample-002.json b/corpus/labels/HX/sample-002.json new file mode 100644 index 0000000..11f1e10 --- /dev/null +++ b/corpus/labels/HX/sample-002.json @@ -0,0 +1,34 @@ +{ + "spec": "labels/HX", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 2", + "input": { + "label": "HX", + "text": "RA FMT 43 GSP B02" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-hx", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Undelivered Uplink Report", + "items": [ + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "GSP" + } + ] + }, + "raw": { + "departure_icao": "GSP" + }, + "remaining": { + "text": "B02" + } + } +} diff --git a/corpus/labels/HX/sample-003.json b/corpus/labels/HX/sample-003.json new file mode 100644 index 0000000..21b8a3a --- /dev/null +++ b/corpus/labels/HX/sample-003.json @@ -0,0 +1,23 @@ +{ + "spec": "labels/HX", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "HX", + "text": "HX Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "label-hx", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Undelivered Uplink Report", + "items": [] + }, + "raw": {}, + "remaining": {} + } +} diff --git a/corpus/labels/MA/sample-001.json b/corpus/labels/MA/sample-001.json new file mode 100644 index 0000000..552cfd1 --- /dev/null +++ b/corpus/labels/MA/sample-001.json @@ -0,0 +1,55 @@ +{ + "spec": "labels/MA", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 1", + "input": { + "label": "MA", + "text": "T02!<<,:/k.E`;FOV@!'s.16q6R+p(RK,|D2ujNJhRah?_qrNftWiI-V,@*RQs,tn,FYN$/V1!gNIc6CO;$D,1:.4?dF952;>XP$\"B\"Ok-Fr'0^k?rP]3&UGoPX;\\/k&_k6:\"c1!'s.16q0U2odKk@|D,eVtdm+WF(#Ll8Dja==EY8V4KISInKk%((%5&@p?Z\\NSZ9tj[Xr0rXqe`+A(J,f[fotFmHmS" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-ma", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Unknown", + "items": [ + { + "type": "label", + "code": "LABEL", + "label": "Message Label", + "value": "H1" + }, + { + "type": "sublabel", + "code": "SUBLABEL", + "label": "Message Sublabel", + "value": "DF" + }, + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "ET-BAY" + } + ] + }, + "raw": { + "label": "H1", + "sublabel": "DF", + "tail": "ET-BAY" + }, + "remaining": { + "text": "A350,000243,1,1," + } + } +} diff --git a/corpus/labels/MA/sample-003.json b/corpus/labels/MA/sample-003.json new file mode 100644 index 0000000..3bf783d --- /dev/null +++ b/corpus/labels/MA/sample-003.json @@ -0,0 +1,75 @@ +{ + "spec": "labels/MA", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 3", + "input": { + "label": "MA", + "text": "T20!<<,B/k&,Z:/=Hs!'iLt0/1h8|*KJNa__;Kes\"hV'&4]J&&&;\"X/O6%0d7TMg5=d7'gcZ:j\"2EiA7k`-`$]f/r>q-b^jGP>WX4Dk3IrQ:U4%?]H$4HCd0Z(^Yq+lXXR!=niT!#;:5UAQ`JU@Bfn/*)TSu?G=LuOlLaFSmkGi]eE[!o&j@iX^g0r)T9!Ye)k6aOmSf`8G)8,hS5Fq_trW*!!" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "label-ma", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Unknown", + "items": [ + { + "type": "label", + "code": "LABEL", + "label": "Message Label", + "value": "80" + }, + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "EC-NOI" + }, + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "2501" + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "2" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "LEMD" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "MMUN" + }, + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "EC-NOI" + } + ] + }, + "raw": { + "label": "80", + "tail": "EC-NOI", + "flight_number": "2501", + "day": 2, + "departure_icao": "LEMD", + "arrival_icao": "MMUN" + }, + "remaining": { + "text": "3D01 RMPSRV//LAV Y/CAB Y/MEDA N/SEC N/WAS N/WAT N/FUEL N/WCHRR 01/WCHRS --/WCHRC --/UMNR --/MAAS N/BUENAS NOCHES, ETA 0055Z Y NOS HAN PEDIDO UNA WCHR QUE AL PARECER NO TENIAMOS CONSTANCIA DE ELLA, POR SI PODEIS PEDIRLO A OPS CUN. MUCHAS GRACIAS" + } + } +} diff --git a/corpus/labels/MA/sample-004.json b/corpus/labels/MA/sample-004.json new file mode 100644 index 0000000..799558c --- /dev/null +++ b/corpus/labels/MA/sample-004.json @@ -0,0 +1,55 @@ +{ + "spec": "labels/MA", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 4", + "input": { + "label": "MA", + "text": "T02!<<,:/k.E`;FOV@!'s.16q6R+p(RK,|D2ujNJhRah?_qrNftWiI-V,@*RQs,tn,FYN$/V1!gNIc6CO;$D,1:.4?dF952;>XP$\"B\"Ok-Fr'0^k?rP]3&UGoPX;\\> ?" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "35000 feet" + }, + { + "type": "fuel_on_board", + "code": "FOB", + "label": "Fuel On Board", + "value": "857" + }, + { + "type": "version", + "code": "VERSION", + "label": "Message Version", + "value": "v3.2" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x2b89" + } + ] + }, + "raw": { + "tail": "50007B", + "flight_number": "RCH4086", + "mission_number": "ABB02R70E037", + "message_timestamp": 1739164684, + "sequence_number": 103, + "day": 9, + "eta_time": 27480, + "position": { + "latitude": 56.02, + "longitude": -13.455 + }, + "route": { + "waypoints": [ + { + "name": "", + "time": 19084 + }, + { + "name": "", + "time": 0 + }, + { + "name": "?" + } + ] + }, + "altitude": 35000, + "fuel_on_board": 857, + "version": 3.2, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 11145 + }, + "remaining": { + "text": "M80/084081," + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-041.json b/corpus/wildcards/arinc_702/sample-041.json new file mode 100644 index 0000000..00261f6 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-041.json @@ -0,0 +1,25 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 41", + "input": { + "label": "2P", + "text": "M01AFN1234POS Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Unknown", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "M01/,M01AFN1234POS Bogus message" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-042.json b/corpus/wildcards/arinc_702/sample-042.json new file mode 100644 index 0000000..38f8fa3 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-042.json @@ -0,0 +1,49 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 42", + "input": { + "label": "H1", + "text": "PER/PR1337,262,320,222,,60,24,275103,M53,180,P52,P02917" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Performance Report", + "items": [ + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "32000 feet" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-53 degrees" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x2917" + } + ] + }, + "raw": { + "altitude": 32000, + "outside_air_temperature": -53, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 10519 + }, + "remaining": { + "text": "1337,262,222,,60,24,275103,180,P52,P0" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-043.json b/corpus/wildcards/arinc_702/sample-043.json new file mode 100644 index 0000000..5ee7185 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-043.json @@ -0,0 +1,49 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 43", + "input": { + "label": "H1", + "text": "PER/PR1218,276,340,134,,0,68,,M56,180,,,P30,P0,33936,,1084,284388D" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Performance Report", + "items": [ + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "34000 feet" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-56 degrees" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x388d" + } + ] + }, + "raw": { + "altitude": 34000, + "outside_air_temperature": -56, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 14477 + }, + "remaining": { + "text": "1218,276,134,,0,68,,180,,,P30,P0,33936,,1084,284" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-044.json b/corpus/wildcards/arinc_702/sample-044.json new file mode 100644 index 0000000..ac7eb93 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-044.json @@ -0,0 +1,25 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 44", + "input": { + "label": "H1", + "text": "PER Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Unknown", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "PER/,PER Bogus message" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-045.json b/corpus/wildcards/arinc_702/sample-045.json new file mode 100644 index 0000000..e9777e9 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-045.json @@ -0,0 +1,26 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 45", + "input": { + "label": "H1", + "sublabel": "DF", + "text": "A350,000354,1,1,TB000000/REP035,01,02;H01,035,01,02,4000,00066,.F-HREV,3,0,08,12,19,06,58,42,071/H02,KSFO LFPO,FBU711 ,S0285,S0385,S0142S0185/H03, /A10,06,58,40,+26535,+01198,045,+0493,321723,XXXXXX,--------,0423,+0573/A11,06,58,32,+26356,+01221,041,+0502,324326,XXXXXX,--------,0424,+0614/:" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Unknown", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "A35/,A350,000354,1,1,TB000000/REP035,01,02;H01,035,01,02,4000,00066,.F-HREV,3,0,08,12,19,06,58,42,071/H02,KSFO LFPO,FBU711 ,S0285,S0385,S0142S0185/H03, /A10,06,58,40,+26535,+01198,045,+0493,321723,XXXXXX,--------,0423,+0573/A11,06,58,32,+26356,+01221,041,+0502,324326,XXXXXX,--------,0424,+0614/:" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-046.json b/corpus/wildcards/arinc_702/sample-046.json new file mode 100644 index 0000000..553157d --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-046.json @@ -0,0 +1,25 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 46", + "input": { + "label": "80", + "text": "3D01 RMPSRV 2501/02 LEMD/MMUN .EC-NOI\n/LAV Y/CAB Y/MEDA N/SEC N/WAS N/WAT N/FUEL N/WCHRR 01/WCHRS --/WCHRC --/UMNR --/MAAS N\nBUENAS NOCHES, ETA 0055Z Y NOS HAN PEDIDO UNA WCHR QUE AL PARECER NO TENIAMOS CONSTANCIA DE ELLA, POR SI PODEIS PEDIRLO A OPS CUN. MUCHAS GRACIAS" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Unknown", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "3D0/,3D01 RMPSRV 2501/02 LEMD/MMUN .EC-NOI\n/LAV Y/CAB Y/MEDA N/SEC N/WAS N/WAT N/FUEL N/WCHRR 01/WCHRS --/WCHRC --/UMNR --/MAAS N\nBUENAS NOCHES, ETA 0055Z Y NOS HAN PEDIDO UNA WCHR QUE AL PARECER NO TENIAMOS CONSTANCIA DE ELLA, POR SI PODEIS PEDIRLO A OPS CUN. MUCHAS GRACIAS" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-047.json b/corpus/wildcards/arinc_702/sample-047.json new file mode 100644 index 0000000..d9eebdd --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-047.json @@ -0,0 +1,25 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 47", + "input": { + "label": "MA", + "text": "T02!<<,:/k.E`;FOV@!'s.16q6R+p(RK,|D2ujNJhRah?_qrNftWiI-V,@*RQs,tn,FYN$/V1!gNIc6CO;$D,1:.4?dF952;>XP$\"B\"Ok-Fr'0^k?rP]3&UGoPX;\\XP$\"B\"Ok-Fr'0^k?rP]3&UGoPX;\\ EBINY@22:06:01 > ELENN" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-48 degrees" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x7a40" + } + ] + }, + "raw": { + "position": { + "latitude": 43.52, + "longitude": -123.29 + }, + "altitude": 37000, + "route": { + "waypoints": [ + { + "name": "EASON", + "time": 79074 + }, + { + "name": "EBINY", + "time": 79561 + }, + { + "name": "ELENN" + } + ] + }, + "outside_air_temperature": -48, + "message_timestamp": 1663797474, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 31296 + }, + "remaining": { + "text": "02216,185" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-051.json b/corpus/wildcards/arinc_702/sample-051.json new file mode 100644 index 0000000..b20fc02 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-051.json @@ -0,0 +1,85 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 51", + "input": { + "label": "H1", + "text": "/.POS/TS100316,210324/PSS35333W058220,,100316,250,S37131W059150,101916,S39387W060377,M23,27282,241,780,MANUAL,0,813E711" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "35.555 S, 58.367 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "25000 feet" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "@10:03:16 > (37.218 S, 59.250 W)@10:19:16 > (39.645 S, 60.628 W)" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-23 degrees" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0xe711" + } + ] + }, + "raw": { + "message_timestamp": 1711015396, + "position": { + "latitude": -35.555, + "longitude": -58.36666666666667 + }, + "altitude": 25000, + "route": { + "waypoints": [ + { + "name": "", + "time": 36196 + }, + { + "name": "", + "latitude": -37.218333333333334, + "longitude": -59.25, + "time": 37156 + }, + { + "name": "", + "latitude": -39.645, + "longitude": -60.62833333333333 + } + ] + }, + "outside_air_temperature": -23, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 59153 + }, + "remaining": { + "text": "/.27282,241,780,MANUAL,0,813" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-052.json b/corpus/wildcards/arinc_702/sample-052.json new file mode 100644 index 0000000..9ecca72 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-052.json @@ -0,0 +1,88 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 52", + "input": { + "label": "H1", + "text": "/HDQDLUA.POSN38332W080082,RONZZ,135753,320,LEVII,140454,WISTA,M45,20967,194/GAHDQDLUA/CA/TS135753,1411240721" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "38.553 N, 80.137 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "32000 feet" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "RONZZ@13:57:53 > LEVII@14:04:54 > WISTA" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-45 degrees" + }, + { + "type": "ground_address", + "code": "GND_ADDR", + "label": "Ground Address", + "value": "HDQDLUA" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x0721" + } + ] + }, + "raw": { + "position": { + "latitude": 38.553333333333335, + "longitude": -80.13666666666667 + }, + "altitude": 32000, + "route": { + "waypoints": [ + { + "name": "RONZZ", + "time": 50273 + }, + { + "name": "LEVII", + "time": 50694 + }, + { + "name": "WISTA" + } + ] + }, + "outside_air_temperature": -45, + "ground_address": "HDQDLUA", + "message_timestamp": 1731592673, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 1825 + }, + "remaining": { + "text": "/HDQDLUA.20967,194/CA" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-053.json b/corpus/wildcards/arinc_702/sample-053.json new file mode 100644 index 0000000..8cd4a10 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-053.json @@ -0,0 +1,81 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 53", + "input": { + "label": "H1", + "text": "/.POS/TS140122,141124N38321W078003,,140122,450,,140122,,M56,24739,127,8306763" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "38.535 N, 78.005 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "45000 feet" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "@14:01:22 > @14:01:22 > ?" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-56 degrees" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x6763" + } + ] + }, + "raw": { + "position": { + "latitude": 38.535, + "longitude": -78.005 + }, + "altitude": 45000, + "route": { + "waypoints": [ + { + "name": "", + "time": 50482 + }, + { + "name": "", + "time": 50482 + }, + { + "name": "?" + } + ] + }, + "outside_air_temperature": -56, + "message_timestamp": 1731592882, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 26467 + }, + "remaining": { + "text": "/.24739,127" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-054.json b/corpus/wildcards/arinc_702/sample-054.json new file mode 100644 index 0000000..9e1eeb9 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-054.json @@ -0,0 +1,25 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 54", + "input": { + "label": "H1", + "text": "/.POS Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Unknown", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "/.POS Bogus message" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-055.json b/corpus/wildcards/arinc_702/sample-055.json new file mode 100644 index 0000000..f28b580 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-055.json @@ -0,0 +1,81 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 55", + "input": { + "label": "H1", + "text": "POSN43312W123174,EASON,215754,370,EBINY,220601,ELENN,M48,02216,185/TS215754,0921227A40" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "43.520 N, 123.290 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "37000 feet" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "EASON@21:57:54 > EBINY@22:06:01 > ELENN" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-48 degrees" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x7a40" + } + ] + }, + "raw": { + "position": { + "latitude": 43.52, + "longitude": -123.29 + }, + "altitude": 37000, + "route": { + "waypoints": [ + { + "name": "EASON", + "time": 79074 + }, + { + "name": "EBINY", + "time": 79561 + }, + { + "name": "ELENN" + } + ] + }, + "outside_air_temperature": -48, + "message_timestamp": 1663797474, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 31296 + }, + "remaining": { + "text": "02216,185" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-056.json b/corpus/wildcards/arinc_702/sample-056.json new file mode 100644 index 0000000..ee474d0 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-056.json @@ -0,0 +1,80 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 56", + "input": { + "label": "H1", + "text": "POSN45209W122550,PEGTY,220309,134,MINNE,220424,HISKU,M6,060013,269,366,355K,292K,730A5B" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "45.348 N, 122.917 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "13400 feet" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "PEGTY@22:03:09 > MINNE@22:04:24 > HISKU" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-6 degrees" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x0a5b" + } + ] + }, + "raw": { + "position": { + "latitude": 45.348333333333336, + "longitude": -122.91666666666667 + }, + "altitude": 13400, + "route": { + "waypoints": [ + { + "name": "PEGTY", + "time": 79389 + }, + { + "name": "MINNE", + "time": 79464 + }, + { + "name": "HISKU" + } + ] + }, + "outside_air_temperature": -6, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 2651 + }, + "remaining": { + "text": "060013,269,366,355K,292K,73" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-057.json b/corpus/wildcards/arinc_702/sample-057.json new file mode 100644 index 0000000..f81f792 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-057.json @@ -0,0 +1,81 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 57", + "input": { + "label": "H1", + "text": "POSN43030W122406,IBALL,220516,380,AARON,220816,MOXEE,M47,0047,86/TS220516,092122BF64" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "43.050 N, 122.677 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "38000 feet" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "IBALL@22:05:16 > AARON@22:08:16 > MOXEE" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-47 degrees" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0xbf64" + } + ] + }, + "raw": { + "position": { + "latitude": 43.05, + "longitude": -122.67666666666666 + }, + "altitude": 38000, + "route": { + "waypoints": [ + { + "name": "IBALL", + "time": 79516 + }, + { + "name": "AARON", + "time": 79696 + }, + { + "name": "MOXEE" + } + ] + }, + "outside_air_temperature": -47, + "message_timestamp": 1663797916, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 48996 + }, + "remaining": { + "text": "0047,86" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-058.json b/corpus/wildcards/arinc_702/sample-058.json new file mode 100644 index 0000000..89aeae9 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-058.json @@ -0,0 +1,80 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 58", + "input": { + "label": "H1", + "text": "POSN33225W079428,SCOOB,232933,340,ENEME,235712,FETAL,M42,003051,15857F6" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "33.375 N, 79.713 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "34000 feet" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "SCOOB@23:29:33 > ENEME@23:57:12 > FETAL" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-42 degrees" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x57f6" + } + ] + }, + "raw": { + "position": { + "latitude": 33.375, + "longitude": -79.71333333333334 + }, + "altitude": 34000, + "route": { + "waypoints": [ + { + "name": "SCOOB", + "time": 84573 + }, + { + "name": "ENEME", + "time": 86232 + }, + { + "name": "FETAL" + } + ] + }, + "outside_air_temperature": -42, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 22518 + }, + "remaining": { + "text": "003051,158" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-059.json b/corpus/wildcards/arinc_702/sample-059.json new file mode 100644 index 0000000..84de3c1 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-059.json @@ -0,0 +1,80 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 59", + "input": { + "label": "H1", + "text": "POSN38531W078000,CSN-01,112309,310,CYN-02,114151,ACK,M40,26067,22479226" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "38.885 N, 78.000 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "31000 feet" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "CSN-01@11:23:09 > CYN-02@11:41:51 > ACK" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-40 degrees" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x9226" + } + ] + }, + "raw": { + "position": { + "latitude": 38.885, + "longitude": -78 + }, + "altitude": 31000, + "route": { + "waypoints": [ + { + "name": "CSN-01", + "time": 40989 + }, + { + "name": "CYN-02", + "time": 42111 + }, + { + "name": "ACK" + } + ] + }, + "outside_air_temperature": -40, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 37414 + }, + "remaining": { + "text": "26067,2247" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-060.json b/corpus/wildcards/arinc_702/sample-060.json new file mode 100644 index 0000000..4398943 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-060.json @@ -0,0 +1,109 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 60", + "input": { + "label": "H1", + "text": "POS/RFSCOOB.KEMPR.ECG.OHPEA.TOMMZ.OXANA.ZZTOP.OMALA.WILYY.KANUX.GALVN.KASAR.LNHOM.SLUKA.FIPEK.PUYYA.PLING.KOLAO.JETSSF2FC" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "status", + "code": "ROUTE_STATUS", + "label": "Route Status", + "value": "Route Filed" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "SCOOB > KEMPR > ECG > OHPEA > TOMMZ > OXANA > ZZTOP > OMALA > WILYY > KANUX > GALVN > KASAR > LNHOM > SLUKA > FIPEK > PUYYA > PLING > KOLAO > JETSS" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0xf2fc" + } + ] + }, + "raw": { + "route_status": "RF", + "route": { + "waypoints": [ + { + "name": "SCOOB" + }, + { + "name": "KEMPR" + }, + { + "name": "ECG" + }, + { + "name": "OHPEA" + }, + { + "name": "TOMMZ" + }, + { + "name": "OXANA" + }, + { + "name": "ZZTOP" + }, + { + "name": "OMALA" + }, + { + "name": "WILYY" + }, + { + "name": "KANUX" + }, + { + "name": "GALVN" + }, + { + "name": "KASAR" + }, + { + "name": "LNHOM" + }, + { + "name": "SLUKA" + }, + { + "name": "FIPEK" + }, + { + "name": "PUYYA" + }, + { + "name": "PLING" + }, + { + "name": "KOLAO" + }, + { + "name": "JETSS" + } + ] + }, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 62204 + }, + "remaining": { + "text": "" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-061.json b/corpus/wildcards/arinc_702/sample-061.json new file mode 100644 index 0000000..84a25f4 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-061.json @@ -0,0 +1,25 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 61", + "input": { + "label": "H1", + "text": "POS Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Unknown", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "POS/,POS Bogus message" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-062.json b/corpus/wildcards/arinc_702/sample-062.json new file mode 100644 index 0000000..2a60004 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-062.json @@ -0,0 +1,80 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 62", + "input": { + "label": "H1", + "text": "#M1BPOSN37533W096476,ROKNE,185212,330,DOSOA,190059,BUM,M50,272100,1571541" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "37.888 N, 96.793 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "33000 feet" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "ROKNE@18:52:12 > DOSOA@19:00:59 > BUM" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-50 degrees" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x1541" + } + ] + }, + "raw": { + "position": { + "latitude": 37.888333333333335, + "longitude": -96.79333333333334 + }, + "altitude": 33000, + "route": { + "waypoints": [ + { + "name": "ROKNE", + "time": 67932 + }, + { + "name": "DOSOA", + "time": 68459 + }, + { + "name": "BUM" + } + ] + }, + "outside_air_temperature": -50, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 5441 + }, + "remaining": { + "text": "272100,157" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-063.json b/corpus/wildcards/arinc_702/sample-063.json new file mode 100644 index 0000000..d3f6671 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-063.json @@ -0,0 +1,152 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 63", + "input": { + "label": "H1", + "text": "F37AMCLL93#M1BPOS/ID746026,,/DC03032024,173207/MR1,/ET031846/PSN42579W108090,173207,320,WAIDE,031759,WEDAK,M49,267070,T468/CG264,110,360/FB742/VR324E17" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "746026" + }, + { + "type": "sequence", + "code": "SEQ", + "label": "Sequence Number", + "value": 1 + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "3" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "18:46:00" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "42.965 N, 108.150 W" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "WAIDE@17:32:07 > WEDAK@03:17:59 > ?" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "32000 feet" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-49 degrees" + }, + { + "type": "center_of_gravity", + "code": "CG", + "label": "Center of Gravity", + "value": "26.4 %" + }, + { + "type": "cg_lower_limit", + "code": "CG_LOWER", + "label": "Center of Gravity Lower Limit", + "value": "11 %" + }, + { + "type": "cg_upper_limit", + "code": "CG_UPPER", + "label": "Center of Gravity Upper Limit", + "value": "36 %" + }, + { + "type": "fuel_on_board", + "code": "FOB", + "label": "Fuel On Board", + "value": "742" + }, + { + "type": "version", + "code": "VERSION", + "label": "Message Version", + "value": "v3.2" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x4e17" + }, + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "MCLL93" + } + ] + }, + "raw": { + "tail": "746026", + "mission_number": "", + "message_timestamp": 1709487127, + "sequence_number": 1, + "day": 3, + "eta_time": 67560, + "position": { + "latitude": 42.965, + "longitude": -108.15 + }, + "route": { + "waypoints": [ + { + "name": "WAIDE", + "time": 63127 + }, + { + "name": "WEDAK", + "time": 11879 + }, + { + "name": "?" + } + ] + }, + "altitude": 32000, + "outside_air_temperature": -49, + "center_of_gravity": 26.4, + "cg_lower_limit": 11, + "cg_upper_limit": 36, + "fuel_on_board": 742, + "version": 3.2, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 19991, + "flight_number": "MCLL93" + }, + "remaining": { + "text": "F37A#M1B/267070,T468" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-064.json b/corpus/wildcards/arinc_702/sample-064.json new file mode 100644 index 0000000..39496f3 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-064.json @@ -0,0 +1,165 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 64", + "input": { + "label": "H1", + "text": "POS/TS080616,210324/DTMMGL,29O,64,103316/PR1754,231,350,189,,0,0,,M45,185,,,P16,P0,36000,,1565,250/RP:DA:MMTJ:AA:MMGL:R:27O:D:TUMA2B..SANFE.UT4..LMM:A:LONV1D:AP:ILSZ29.PLADE(29O)9D1C" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "MMGL" + }, + { + "type": "runway", + "code": "ARWY", + "label": "Arrival Runway", + "value": "29O" + }, + { + "type": "fuel_on_board", + "code": "FOB", + "label": "Fuel On Board", + "value": "64" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "10:33:16" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "35000 feet" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-45 degrees" + }, + { + "type": "status", + "code": "ROUTE_STATUS", + "label": "Route Status", + "value": "Route Planned" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "MMTJ" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "MMGL" + }, + { + "type": "runway", + "code": "DEPRWY", + "label": "Departure Runway", + "value": "27O" + }, + { + "type": "procedure", + "code": "proc", + "label": "Departure Procedure", + "value": "TUMA2B: >> SANFE > UT4 >> LMM" + }, + { + "type": "procedure", + "code": "proc", + "label": "Arrival Procedure", + "value": "LONV1D" + }, + { + "type": "procedure", + "code": "proc", + "label": "Approach Procedure", + "value": "ILSZ29 starting at PLADE(29O)" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x9d1c" + } + ] + }, + "raw": { + "message_timestamp": 1711008376, + "arrival_icao": "MMGL", + "arrival_runway": "29O", + "fuel_on_board": 64, + "eta_time": 37996, + "altitude": 35000, + "outside_air_temperature": -45, + "route_status": "RP", + "departure_icao": "MMTJ", + "departure_runway": "27O", + "procedures": [ + { + "type": "departure", + "route": { + "name": "TUMA2B", + "waypoints": [ + { + "name": "" + }, + { + "name": "SANFE" + }, + { + "name": "UT4" + }, + { + "name": "" + }, + { + "name": "LMM" + } + ] + } + }, + { + "type": "arrival", + "route": { + "name": "LONV1D" + } + }, + { + "type": "approach", + "route": { + "name": "ILSZ29", + "waypoints": [ + { + "name": "PLADE(29O)" + } + ] + } + } + ], + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 40220 + }, + "remaining": { + "text": "1754,231,189,,0,0,,185,,,P16,P0,36000,,1565,250" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-065.json b/corpus/wildcards/arinc_702/sample-065.json new file mode 100644 index 0000000..fa37e21 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-065.json @@ -0,0 +1,92 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 65", + "input": { + "label": "H1", + "text": "POSN33204W114082,BLH176-0093,062056,330,SALOM180-0127,062211,KOFFA180-0148,M49,30628,3201251" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "33.340 N, 114.137 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "33000 feet" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "BLH[176° 9.3nm]@06:20:56 > SALOM[180° 12.7nm]@06:22:11 > KOFFA[180° 14.8nm]" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-49 degrees" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x1251" + } + ] + }, + "raw": { + "position": { + "latitude": 33.34, + "longitude": -114.13666666666667 + }, + "altitude": 33000, + "route": { + "waypoints": [ + { + "name": "BLH", + "offset": { + "bearing": 176, + "distance": 9.3 + }, + "time": 22856 + }, + { + "name": "SALOM", + "offset": { + "bearing": 180, + "distance": 12.7 + }, + "time": 22931 + }, + { + "name": "KOFFA", + "offset": { + "bearing": 180, + "distance": 14.8 + } + } + ] + }, + "outside_air_temperature": -49, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 4689 + }, + "remaining": { + "text": "30628,320" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-066.json b/corpus/wildcards/arinc_702/sample-066.json new file mode 100644 index 0000000..b6bc7fc --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-066.json @@ -0,0 +1,81 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 66", + "input": { + "label": "H1", + "text": "POSN39164W077259,FORKL,231828,51,THRET,231917,TOOPR,P16,1726,167,/TS231828,2807244841" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "39.273 N, 77.432 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "5100 feet" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "FORKL@23:18:28 > THRET@23:19:17 > TOOPR" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "16 degrees" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x4841" + } + ] + }, + "raw": { + "position": { + "latitude": 39.27333333333333, + "longitude": -77.43166666666667 + }, + "altitude": 5100, + "route": { + "waypoints": [ + { + "name": "FORKL", + "time": 83908 + }, + { + "name": "THRET", + "time": 83957 + }, + { + "name": "TOOPR" + } + ] + }, + "outside_air_temperature": 16, + "message_timestamp": 1722208708, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 18497 + }, + "remaining": { + "text": "1726,167" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-067.json b/corpus/wildcards/arinc_702/sample-067.json new file mode 100644 index 0000000..3710880 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-067.json @@ -0,0 +1,141 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 67", + "input": { + "label": "H1", + "text": "POSN51194E004527,EH556,104241,174,CIV,105208,MEDIL,M20,290016,191/RP:DA:EHEH:AA:LEIB..CIV.N872.MEDIL.UN872.KOVIN.UM728.RESMI.UN857.DISAK..DIRMO..ETAMO..ADEKA..MOKDI..MEN..BADAM..KANIG..KENAS.N855.POS/PR1496,150,370,191,,55,10,248028,M47,30,P19,P0/FHCIV,105208,273K,3226,175,M41,252027,450,N,221,62.MEDIL,105411,267K,3439,172,M44,250028,459,N,203,15.PITHI,105533,259K,3584,170,M47,249028,456,N,203,10.LESDO,105859,252K,3700,167,M47,248028,456,N,203,25.KOVIN,110153,252K,3700,164,M47,248028,456,N,203,21.DUCRA,110705,252K,3700,160,M47,248028,456,N,213,37.RESMI,111101,251K,3700,156,M47,248028,455,N,213,28.DEKOD,111325,251K,3700,154,M47,248028,455,N,192,17.DISAK,111438,251K,3700,153,M47,248028,454,N,172,9.DIRMO,112306,251K,3700,145,M47,248028,454,N,178,63.ETAMO,112514,250K,3700,143,M47,248028,453,N,158,16.ADEKA,113339,250K,3700,136,M47,248028,454,N,147,64.MOKDI,114139,251K,3700,129,M47,248028,454,N,181,59.MEN,114429,251K,3700,127,M47,248028,454,N,181,21.BADAM,114843,251K,3700,123,M47,248028,454,N,179,31.KANIG,120154,250K,3700,111,M47,248028,453,N,185,97.KENAS,121800,250K,3700,98,M47,248028,453,N,177,119.POS,122257,250K,3018,96,M45,248023,395,N,182,34.LEIB,124503,150K,2,89,P15,000000,161,N,231,103/DTLEIB,,89,124503,7353B2" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "51.323 N, 4.878 E" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "17400 feet" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "EH556@10:42:41 > CIV@10:52:08 > MEDIL" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-20 degrees" + }, + { + "type": "status", + "code": "ROUTE_STATUS", + "label": "Route Status", + "value": "Route Planned" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "EHEH" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "LEIB..CIV.N872.MEDIL.UN872.KOVIN.UM728.RESMI.UN857.DISAK..DIRMO..ETAMO..ADEKA..MOKDI..MEN..BADAM..KANIG..KENAS.N855.POS" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "37000 feet" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-47 degrees" + }, + { + "type": "runway", + "code": "ARWY", + "label": "Arrival Runway", + "value": "" + }, + { + "type": "fuel_on_board", + "code": "FOB", + "label": "Fuel On Board", + "value": "89" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "12:45:03" + }, + { + "type": "fuel_remaining", + "code": " FUEL_REM", + "label": "Fuel Remaining", + "value": "73" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x53b2" + } + ] + }, + "raw": { + "position": { + "latitude": 51.32333333333333, + "longitude": 4.878333333333334 + }, + "altitude": 37000, + "route": { + "waypoints": [ + { + "name": "EH556", + "time": 38561 + }, + { + "name": "CIV", + "time": 39128 + }, + { + "name": "MEDIL" + } + ] + }, + "outside_air_temperature": -47, + "route_status": "RP", + "departure_icao": "EHEH", + "arrival_icao": "LEIB..CIV.N872.MEDIL.UN872.KOVIN.UM728.RESMI.UN857.DISAK..DIRMO..ETAMO..ADEKA..MOKDI..MEN..BADAM..KANIG..KENAS.N855.POS", + "arrival_runway": "", + "fuel_on_board": 89, + "eta_time": 45903, + "fuel_remaining": 73, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 21426 + }, + "remaining": { + "text": "290016,191/PR1496,150,191,,55,10,248028,30,P19,P0/FHCIV,105208,273K,3226,175,M41,252027,450,N,221,62.MEDIL,105411,267K,3439,172,M44,250028,459,N,203,15.PITHI,105533,259K,3584,170,M47,249028,456,N,203,10.LESDO,105859,252K,3700,167,M47,248028,456,N,203,25.KOVIN,110153,252K,3700,164,M47,248028,456,N,203,21.DUCRA,110705,252K,3700,160,M47,248028,456,N,213,37.RESMI,111101,251K,3700,156,M47,248028,455,N,213,28.DEKOD,111325,251K,3700,154,M47,248028,455,N,192,17.DISAK,111438,251K,3700,153,M47,248028,454,N,172,9.DIRMO,112306,251K,3700,145,M47,248028,454,N,178,63.ETAMO,112514,250K,3700,143,M47,248028,453,N,158,16.ADEKA,113339,250K,3700,136,M47,248028,454,N,147,64.MOKDI,114139,251K,3700,129,M47,248028,454,N,181,59.MEN,114429,251K,3700,127,M47,248028,454,N,181,21.BADAM,114843,251K,3700,123,M47,248028,454,N,179,31.KANIG,120154,250K,3700,111,M47,248028,453,N,185,97.KENAS,121800,250K,3700,98,M47,248028,453,N,177,119.POS,122257,250K,3018,96,M45,248023,395,N,182,34.LEIB,124503,150K,2,89,P15,000000,161,N,231,103" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-068.json b/corpus/wildcards/arinc_702/sample-068.json new file mode 100644 index 0000000..a836fc9 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-068.json @@ -0,0 +1,85 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 68", + "input": { + "label": "H1", + "text": "/.POS/TS100316,210324/PSS35333W058220,,100316,250,S37131W059150,101916,S39387W060377,M23,27282,241,780,MANUAL,0,813E711" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "35.555 S, 58.367 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "25000 feet" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "@10:03:16 > (37.218 S, 59.250 W)@10:19:16 > (39.645 S, 60.628 W)" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-23 degrees" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0xe711" + } + ] + }, + "raw": { + "message_timestamp": 1711015396, + "position": { + "latitude": -35.555, + "longitude": -58.36666666666667 + }, + "altitude": 25000, + "route": { + "waypoints": [ + { + "name": "", + "time": 36196 + }, + { + "name": "", + "latitude": -37.218333333333334, + "longitude": -59.25, + "time": 37156 + }, + { + "name": "", + "latitude": -39.645, + "longitude": -60.62833333333333 + } + ] + }, + "outside_air_temperature": -23, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 59153 + }, + "remaining": { + "text": "/.27282,241,780,MANUAL,0,813" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-069.json b/corpus/wildcards/arinc_702/sample-069.json new file mode 100644 index 0000000..fdfc92a --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-069.json @@ -0,0 +1,104 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 69", + "input": { + "label": "H1", + "text": "POSN39220W078258,MRB18,044034,340,EYT19,044427,COL20,M49,27369,436,813/PSN39220W078258,MRB18,044034,340,EYT19,044427,COL20,M49,27369,436,813,ECON CRZ,0,25140035" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "39.367 N, 78.430 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "34000 feet" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "MRB18@04:40:34 > EYT19@04:44:27 > COL20" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-49 degrees" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "39.367 N, 78.430 W" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "34000 feet" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "MRB18@04:40:34 > EYT19@04:44:27 > COL20" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-49 degrees" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x0035" + } + ] + }, + "raw": { + "position": { + "latitude": 39.36666666666667, + "longitude": -78.43 + }, + "altitude": 34000, + "route": { + "waypoints": [ + { + "name": "MRB18", + "time": 16834 + }, + { + "name": "EYT19", + "time": 17067 + }, + { + "name": "COL20" + } + ] + }, + "outside_air_temperature": -49, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 53 + }, + "remaining": { + "text": "27369,436,27369,436,813,ECON CRZ,0,2514" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-070.json b/corpus/wildcards/arinc_702/sample-070.json new file mode 100644 index 0000000..c72030f --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-070.json @@ -0,0 +1,25 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 70", + "input": { + "label": "H1", + "text": "#M1BPOS Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Unknown", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "#M1/,#M1BPOS Bogus message" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-071.json b/corpus/wildcards/arinc_702/sample-071.json new file mode 100644 index 0000000..6035199 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-071.json @@ -0,0 +1,79 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 71", + "input": { + "label": "80", + "text": "INR/ID91511S,,/DC04032026,143534/MR19,/NR,,,,,,,,950,0/ET041505/FB983/VR32BF4C" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "In-Range Report", + "items": [ + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "91511S" + }, + { + "type": "sequence", + "code": "SEQ", + "label": "Sequence Number", + "value": 19 + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "4" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "15:05:00" + }, + { + "type": "fuel_on_board", + "code": "FOB", + "label": "Fuel On Board", + "value": "983" + }, + { + "type": "version", + "code": "VERSION", + "label": "Message Version", + "value": "v3.2" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0xbf4c" + } + ] + }, + "raw": { + "tail": "91511S", + "mission_number": "", + "message_timestamp": 1772634934, + "sequence_number": 19, + "day": 4, + "eta_time": 54300, + "fuel_on_board": 983, + "version": 3.2, + "checksum_algorithm": "GENIBUS", + "checksum": 48972 + }, + "remaining": { + "text": "NR,,,,,,,,950,0" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-072.json b/corpus/wildcards/arinc_702/sample-072.json new file mode 100644 index 0000000..b1b2fa9 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-072.json @@ -0,0 +1,153 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 72", + "input": { + "label": "H1", + "text": "FPN/RI:DA:KEWR:AA:KDFW:CR:EWRDFW01(17L)..SAAME.J6.HVQ.Q68.LITTR..MEEOW..FEWWW:A:SEEVR4.FEWWW:F:VECTOR..DISCO..RIVET:AP:ILS 17L.RIVET:F:TACKEC8B5" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Flight Plan", + "items": [ + { + "type": "status", + "code": "ROUTE_STATUS", + "label": "Route Status", + "value": "Route Inactive" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KEWR" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KDFW" + }, + { + "type": "company_route", + "code": "CR", + "label": "Company Route", + "value": "EWRDFW01(17L): >> SAAME > J6 > HVQ > Q68 > LITTR >> MEEOW >> FEWWW" + }, + { + "type": "procedure", + "code": "proc", + "label": "Arrival Procedure", + "value": "SEEVR4 starting at FEWWW" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "VECTOR >> DISCO >> RIVET" + }, + { + "type": "procedure", + "code": "proc", + "label": "Approach Procedure", + "value": "ILS 17L starting at RIVET" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "TACKE" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0xc8b5" + } + ] + }, + "raw": { + "route_status": "RI", + "departure_icao": "KEWR", + "arrival_icao": "KDFW", + "company_route": { + "name": "EWRDFW01", + "runway": "17L", + "waypoints": [ + { + "name": "" + }, + { + "name": "SAAME" + }, + { + "name": "J6" + }, + { + "name": "HVQ" + }, + { + "name": "Q68" + }, + { + "name": "LITTR" + }, + { + "name": "" + }, + { + "name": "MEEOW" + }, + { + "name": "" + }, + { + "name": "FEWWW" + } + ] + }, + "procedures": [ + { + "type": "arrival", + "route": { + "name": "SEEVR4", + "waypoints": [ + { + "name": "FEWWW" + } + ] + } + }, + { + "type": "approach", + "route": { + "name": "ILS 17L", + "waypoints": [ + { + "name": "RIVET" + } + ] + } + } + ], + "route": { + "waypoints": [ + { + "name": "TACKE" + } + ] + }, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 51381 + }, + "remaining": { + "text": "" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-073.json b/corpus/wildcards/arinc_702/sample-073.json new file mode 100644 index 0000000..29b7165 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-073.json @@ -0,0 +1,195 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 73", + "input": { + "label": "H1", + "text": "FPN/FNAAL1956/RP:DA:KPHL:AA:KPHX:CR:PHLPHX61:R:27L(26O):D:PHL3:A:EAGUL6.ZUN:AP:ILS26..AIR,N40010W080490.J110.BOWRR..VLA,N39056W089097..STL,N38516W090289..GIBSN,N38430W092244..TYGER,N38410W094050..GCK,N37551W100435..DIXAN,N36169W105573..ZUN,N34579W109093293B" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Flight Plan", + "items": [ + { + "type": "status", + "code": "ROUTE_STATUS", + "label": "Route Status", + "value": "Route Planned" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KPHL" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KPHX" + }, + { + "type": "company_route", + "code": "CR", + "label": "Company Route", + "value": "PHLPHX61" + }, + { + "type": "runway", + "code": "ARWY", + "label": "Arrival Runway", + "value": "26O" + }, + { + "type": "runway", + "code": "DEPRWY", + "label": "Departure Runway", + "value": "27L" + }, + { + "type": "procedure", + "code": "proc", + "label": "Departure Procedure", + "value": "PHL3" + }, + { + "type": "procedure", + "code": "proc", + "label": "Arrival Procedure", + "value": "EAGUL6 starting at ZUN" + }, + { + "type": "procedure", + "code": "proc", + "label": "Approach Procedure", + "value": "ILS26: >> AIR(40.017 N, 80.817 W) > J110 > BOWRR >> VLA(39.093 N, 89.162 W) >> STL(38.860 N, 90.482 W) >> GIBSN(38.717 N, 92.407 W) >> TYGER(38.683 N, 94.083 W) >> GCK(37.918 N, 100.725 W) >> DIXAN(36.282 N, 105.955 W) >> ZUN(34.965 N, 109.155 W)" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x293b" + } + ] + }, + "raw": { + "flight_number": "AAL1956", + "route_status": "RP", + "departure_icao": "KPHL", + "arrival_icao": "KPHX", + "company_route": { + "name": "PHLPHX61" + }, + "arrival_runway": "26O", + "departure_runway": "27L", + "procedures": [ + { + "type": "departure", + "route": { + "name": "PHL3" + } + }, + { + "type": "arrival", + "route": { + "name": "EAGUL6", + "waypoints": [ + { + "name": "ZUN" + } + ] + } + }, + { + "type": "approach", + "route": { + "name": "ILS26", + "waypoints": [ + { + "name": "" + }, + { + "name": "AIR", + "latitude": 40.016666666666666, + "longitude": -80.81666666666666 + }, + { + "name": "J110" + }, + { + "name": "BOWRR" + }, + { + "name": "" + }, + { + "name": "VLA", + "latitude": 39.093333333333334, + "longitude": -89.16166666666666 + }, + { + "name": "" + }, + { + "name": "STL", + "latitude": 38.86, + "longitude": -90.48166666666667 + }, + { + "name": "" + }, + { + "name": "GIBSN", + "latitude": 38.71666666666667, + "longitude": -92.40666666666667 + }, + { + "name": "" + }, + { + "name": "TYGER", + "latitude": 38.68333333333333, + "longitude": -94.08333333333333 + }, + { + "name": "" + }, + { + "name": "GCK", + "latitude": 37.91833333333334, + "longitude": -100.725 + }, + { + "name": "" + }, + { + "name": "DIXAN", + "latitude": 36.281666666666666, + "longitude": -105.955 + }, + { + "name": "" + }, + { + "name": "ZUN", + "latitude": 34.965, + "longitude": -109.155 + } + ] + } + } + ], + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 10555 + }, + "remaining": { + "text": "" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-074.json b/corpus/wildcards/arinc_702/sample-074.json new file mode 100644 index 0000000..fd1c4eb --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-074.json @@ -0,0 +1,96 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 74", + "input": { + "label": "H1", + "text": "FPN/FNUAL1187/RP:DA:KSFO:AA:KPHX:F:KAYEX,N36292W120569..LOSHN,N35509W120000..BOILE,N34253W118016..BLH,N33358W114457DDFB" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Flight Plan", + "items": [ + { + "type": "status", + "code": "ROUTE_STATUS", + "label": "Route Status", + "value": "Route Planned" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KSFO" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KPHX" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "KAYEX(36.487 N, 120.948 W) >> LOSHN(35.848 N, 120.000 W) >> BOILE(34.422 N, 118.027 W) >> BLH(33.597 N, 114.762 W)" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0xddfb" + } + ] + }, + "raw": { + "flight_number": "UAL1187", + "route_status": "RP", + "departure_icao": "KSFO", + "arrival_icao": "KPHX", + "route": { + "waypoints": [ + { + "name": "KAYEX", + "latitude": 36.486666666666665, + "longitude": -120.94833333333334 + }, + { + "name": "" + }, + { + "name": "LOSHN", + "latitude": 35.848333333333336, + "longitude": -120 + }, + { + "name": "" + }, + { + "name": "BOILE", + "latitude": 34.42166666666667, + "longitude": -118.02666666666667 + }, + { + "name": "" + }, + { + "name": "BLH", + "latitude": 33.596666666666664, + "longitude": -114.76166666666667 + } + ] + }, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 56827 + }, + "remaining": { + "text": "" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-075.json b/corpus/wildcards/arinc_702/sample-075.json new file mode 100644 index 0000000..eff9b3e --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-075.json @@ -0,0 +1,111 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 75", + "input": { + "label": "H1", + "text": "FPN/TS140017,021724/RP:DA:EHAM:AA:KMSP..N55064W000477..N55163W001141..ERAKA..N60000W020000..N61000W030000:WS:N61000W030000,370..N61000W040000..N60000W050000..URTAK:WS:URTAK,380..LAKES:WS:LAKES,400..N57000W070000..N54300W080000..N49000W090000..DLH..COLDD:A:BAINY3:AP:ILS30L(30L)/PR4356,344,360,1060,,,13,,,30,,,P50,M40,36090,,3296,292/DTKMSP,30L,172,215117156D" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Flight Plan", + "items": [ + { + "type": "status", + "code": "ROUTE_STATUS", + "label": "Route Status", + "value": "Route Planned" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "EHAM" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KMSP..N55064W000477..N55163W001141..ERAKA..N60000W020000..N61000W030000" + }, + { + "type": "procedure", + "code": "proc", + "label": "Arrival Procedure", + "value": "BAINY3" + }, + { + "type": "procedure", + "code": "proc", + "label": "Approach Procedure", + "value": "ILS30L(30L)" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "36000 feet" + }, + { + "type": "runway", + "code": "ARWY", + "label": "Arrival Runway", + "value": "30L" + }, + { + "type": "fuel_on_board", + "code": "FOB", + "label": "Fuel On Board", + "value": "172" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "21:51:17" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x156d" + } + ] + }, + "raw": { + "message_timestamp": 1708178417, + "route_status": "RP", + "departure_icao": "EHAM", + "arrival_icao": "KMSP..N55064W000477..N55163W001141..ERAKA..N60000W020000..N61000W030000", + "procedures": [ + { + "type": "arrival", + "route": { + "name": "BAINY3" + } + }, + { + "type": "approach", + "route": { + "name": "ILS30L(30L)" + } + } + ], + "altitude": 36000, + "arrival_runway": "30L", + "fuel_on_board": 172, + "eta_time": 78677, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 5485 + }, + "remaining": { + "text": ":WS:N61000W030000,370..N61000W040000..N60000W050000..URTAK:WS:URTAK,380..LAKES:WS:LAKES,400..N57000W070000..N54300W080000..N49000W090000..DLH..COLDD/PR4356,344,1060,,,13,,,30,,,P50,M40,36090,,3296,292" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-076.json b/corpus/wildcards/arinc_702/sample-076.json new file mode 100644 index 0000000..902fff7 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-076.json @@ -0,0 +1,67 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 76", + "input": { + "label": "H1", + "text": "FPN/SN2125/FNQFA780/RI:DA:YPPH:CR:PERMEL001:AA:YMML..MEMUP,S33451E\r\n120525.Y53.WENDY0560" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Flight Plan", + "items": [ + { + "type": "status", + "code": "ROUTE_STATUS", + "label": "Route Status", + "value": "Route Inactive" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "YPPH" + }, + { + "type": "company_route", + "code": "CR", + "label": "Company Route", + "value": "PERMEL001" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "YMML..MEMUP,S33451E120525.Y53.WENDY" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x0560" + } + ] + }, + "raw": { + "serial_number": "2125", + "flight_number": "QFA780", + "route_status": "RI", + "departure_icao": "YPPH", + "company_route": { + "name": "PERMEL001" + }, + "arrival_icao": "YMML..MEMUP,S33451E120525.Y53.WENDY", + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 1376 + }, + "remaining": { + "text": "" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-077.json b/corpus/wildcards/arinc_702/sample-077.json new file mode 100644 index 0000000..60b4716 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-077.json @@ -0,0 +1,148 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 77", + "input": { + "label": "H1", + "text": "FPN/TS155631,170224/SN155631/RP:DA:PHNL:AA:KASE:D:MKK5.KOLEA:F:KOLEA,N22354W155133..CLUTS,N23002W154393.R465.CINNY,N36109W124456..OAL,N38002W117462.J58.ILC,N38150W114237..EYELO,N38455W110469..SAKES,N38500W110163.J80.DBL,N39264W106537F5E1" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Flight Plan", + "items": [ + { + "type": "status", + "code": "ROUTE_STATUS", + "label": "Route Status", + "value": "Route Planned" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "PHNL" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KASE" + }, + { + "type": "procedure", + "code": "proc", + "label": "Departure Procedure", + "value": "MKK5 starting at KOLEA" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "KOLEA(22.590 N, 155.222 W) >> CLUTS(23.003 N, 154.655 W) > R465 > CINNY(36.182 N, 124.760 W) >> OAL(38.003 N, 117.770 W) > J58 > ILC(38.250 N, 114.395 W) >> EYELO(38.758 N, 110.782 W) >> SAKES(38.833 N, 110.272 W) > J80 > DBL(39.440 N, 106.895 W)" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0xf5e1" + } + ] + }, + "raw": { + "message_timestamp": 1708185391, + "serial_number": "155631", + "route_status": "RP", + "departure_icao": "PHNL", + "arrival_icao": "KASE", + "procedures": [ + { + "type": "departure", + "route": { + "name": "MKK5", + "waypoints": [ + { + "name": "KOLEA" + } + ] + } + } + ], + "route": { + "waypoints": [ + { + "name": "KOLEA", + "latitude": 22.59, + "longitude": -155.22166666666666 + }, + { + "name": "" + }, + { + "name": "CLUTS", + "latitude": 23.003333333333334, + "longitude": -154.655 + }, + { + "name": "R465" + }, + { + "name": "CINNY", + "latitude": 36.181666666666665, + "longitude": -124.76 + }, + { + "name": "" + }, + { + "name": "OAL", + "latitude": 38.00333333333333, + "longitude": -117.77 + }, + { + "name": "J58" + }, + { + "name": "ILC", + "latitude": 38.25, + "longitude": -114.395 + }, + { + "name": "" + }, + { + "name": "EYELO", + "latitude": 38.75833333333333, + "longitude": -110.78166666666667 + }, + { + "name": "" + }, + { + "name": "SAKES", + "latitude": 38.833333333333336, + "longitude": -110.27166666666666 + }, + { + "name": "J80" + }, + { + "name": "DBL", + "latitude": 39.44, + "longitude": -106.895 + } + ] + }, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 62945 + }, + "remaining": { + "text": "" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-078.json b/corpus/wildcards/arinc_702/sample-078.json new file mode 100644 index 0000000..c0ac89e --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-078.json @@ -0,0 +1,143 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 78", + "input": { + "label": "H1", + "text": "FPN/RP:DA:KSEA:AA:KPHX:D:SUMMA2.SUMMA:F:SUMMA,N46371W121593..LTJ,N45428W121061..IMB,N44389W119427.Q35..CORKR,N36050W112240..TENTS,N35295W112271:A:BRUSR1.TENTS/FNFFT17245D16" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Flight Plan", + "items": [ + { + "type": "status", + "code": "ROUTE_STATUS", + "label": "Route Status", + "value": "Route Planned" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KSEA" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KPHX" + }, + { + "type": "procedure", + "code": "proc", + "label": "Departure Procedure", + "value": "SUMMA2 starting at SUMMA" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "SUMMA(46.618 N, 121.988 W) >> LTJ(45.713 N, 121.102 W) >> IMB(44.648 N, 119.712 W) > Q35 >> CORKR(36.083 N, 112.400 W) >> TENTS(35.492 N, 112.452 W)" + }, + { + "type": "procedure", + "code": "proc", + "label": "Arrival Procedure", + "value": "BRUSR1 starting at TENTS" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x5d16" + } + ] + }, + "raw": { + "route_status": "RP", + "departure_icao": "KSEA", + "arrival_icao": "KPHX", + "procedures": [ + { + "type": "departure", + "route": { + "name": "SUMMA2", + "waypoints": [ + { + "name": "SUMMA" + } + ] + } + }, + { + "type": "arrival", + "route": { + "name": "BRUSR1", + "waypoints": [ + { + "name": "TENTS" + } + ] + } + } + ], + "route": { + "waypoints": [ + { + "name": "SUMMA", + "latitude": 46.61833333333333, + "longitude": -121.98833333333333 + }, + { + "name": "" + }, + { + "name": "LTJ", + "latitude": 45.71333333333333, + "longitude": -121.10166666666667 + }, + { + "name": "" + }, + { + "name": "IMB", + "latitude": 44.64833333333333, + "longitude": -119.71166666666667 + }, + { + "name": "Q35" + }, + { + "name": "" + }, + { + "name": "CORKR", + "latitude": 36.083333333333336, + "longitude": -112.4 + }, + { + "name": "" + }, + { + "name": "TENTS", + "latitude": 35.49166666666667, + "longitude": -112.45166666666667 + } + ] + }, + "flight_number": "FFT1724", + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 23830 + }, + "remaining": { + "text": "" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-079.json b/corpus/wildcards/arinc_702/sample-079.json new file mode 100644 index 0000000..49fa27e --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-079.json @@ -0,0 +1,153 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 79", + "input": { + "label": "H1", + "text": "#M1BFPN/RI:DA:KJFK:AA:KPHX:CR:JFKPHX20..KG701..KG702..KP702..KP703..KD601..KD602..PUB..ALS.J102.GUP:A:EAGUL6.GUP:F:HOMRR90E1" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Flight Plan", + "items": [ + { + "type": "status", + "code": "ROUTE_STATUS", + "label": "Route Status", + "value": "Route Inactive" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KJFK" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "KPHX" + }, + { + "type": "company_route", + "code": "CR", + "label": "Company Route", + "value": "JFKPHX20: >> KG701 >> KG702 >> KP702 >> KP703 >> KD601 >> KD602 >> PUB >> ALS > J102 > GUP" + }, + { + "type": "procedure", + "code": "proc", + "label": "Arrival Procedure", + "value": "EAGUL6 starting at GUP" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "HOMRR" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x90e1" + } + ] + }, + "raw": { + "route_status": "RI", + "departure_icao": "KJFK", + "arrival_icao": "KPHX", + "company_route": { + "name": "JFKPHX20", + "waypoints": [ + { + "name": "" + }, + { + "name": "KG701" + }, + { + "name": "" + }, + { + "name": "KG702" + }, + { + "name": "" + }, + { + "name": "KP702" + }, + { + "name": "" + }, + { + "name": "KP703" + }, + { + "name": "" + }, + { + "name": "KD601" + }, + { + "name": "" + }, + { + "name": "KD602" + }, + { + "name": "" + }, + { + "name": "PUB" + }, + { + "name": "" + }, + { + "name": "ALS" + }, + { + "name": "J102" + }, + { + "name": "GUP" + } + ] + }, + "procedures": [ + { + "type": "arrival", + "route": { + "name": "EAGUL6", + "waypoints": [ + { + "name": "GUP" + } + ] + } + } + ], + "route": { + "waypoints": [ + { + "name": "HOMRR" + } + ] + }, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 37089 + }, + "remaining": { + "text": "" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-080.json b/corpus/wildcards/arinc_702/sample-080.json new file mode 100644 index 0000000..b08a1d0 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-080.json @@ -0,0 +1,149 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 80", + "input": { + "label": "H1", + "text": "F37AKL0767#M1BFPN/TS232008,022324/RP:DA:TNCA:AA:TNCB:R:11O:D:ADRI1F..IRLEP.A574..PJG:AP:RNV10(10O)/PR,,110,,183,7,13,,M7,25,,,P30,M40,36090,13,3455,300/DTTNCB,10O,119,23440847C0" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Flight Plan", + "items": [ + { + "type": "status", + "code": "ROUTE_STATUS", + "label": "Route Status", + "value": "Route Planned" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "TNCA" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "TNCB" + }, + { + "type": "runway", + "code": "DEPRWY", + "label": "Departure Runway", + "value": "11O" + }, + { + "type": "procedure", + "code": "proc", + "label": "Departure Procedure", + "value": "ADRI1F: >> IRLEP > A574 >> PJG" + }, + { + "type": "procedure", + "code": "proc", + "label": "Approach Procedure", + "value": "RNV10(10O)" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "11000 feet" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-7 degrees" + }, + { + "type": "runway", + "code": "ARWY", + "label": "Arrival Runway", + "value": "10O" + }, + { + "type": "fuel_on_board", + "code": "FOB", + "label": "Fuel On Board", + "value": "119" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "23:44:08" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x47c0" + }, + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "KL0767" + } + ] + }, + "raw": { + "message_timestamp": 1708730408, + "route_status": "RP", + "departure_icao": "TNCA", + "arrival_icao": "TNCB", + "departure_runway": "11O", + "procedures": [ + { + "type": "departure", + "route": { + "name": "ADRI1F", + "waypoints": [ + { + "name": "" + }, + { + "name": "IRLEP" + }, + { + "name": "A574" + }, + { + "name": "" + }, + { + "name": "PJG" + } + ] + } + }, + { + "type": "approach", + "route": { + "name": "RNV10(10O)" + } + } + ], + "altitude": 11000, + "outside_air_temperature": -7, + "arrival_runway": "10O", + "fuel_on_board": 119, + "eta_time": 85448, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 18368, + "flight_number": "KL0767" + }, + "remaining": { + "text": "F37A#M1B/,,,183,7,13,,25,,,P30,M40,36090,13,3455,300" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-081.json b/corpus/wildcards/arinc_702/sample-081.json new file mode 100644 index 0000000..aab900f --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-081.json @@ -0,0 +1,25 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 81", + "input": { + "label": "H1", + "text": "FPN Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Unknown", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "FPN/,FPN Bogus message" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-082.json b/corpus/wildcards/arinc_702/sample-082.json new file mode 100644 index 0000000..86e1d63 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-082.json @@ -0,0 +1,143 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 82", + "input": { + "label": "H1", + "text": "FPN/ID88194A,RCH857,PMZM107QP021/MR2,/RM:AA:FJDG:F:DOH.N300..NOLSU.P307..SETSI.P307..PARAR..N20000E063000..RIGLO.L516..ELKEL.L516..BUMMR/WP,,,,E3E9" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Flight Plan", + "items": [ + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "88194A" + }, + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "RCH857" + }, + { + "type": "sequence", + "code": "SEQ", + "label": "Sequence Number", + "value": 2 + }, + { + "type": "status", + "code": "ROUTE_STATUS", + "label": "Route Status", + "value": "Route Mapped" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "FJDG" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "DOH > N300 >> NOLSU > P307 >> SETSI > P307 >> PARAR >> (20.000 N, 63.000 E) >> RIGLO > L516 >> ELKEL > L516 >> BUMMR" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0xe3e9" + } + ] + }, + "raw": { + "tail": "88194A", + "flight_number": "RCH857", + "mission_number": "PMZM107QP021", + "sequence_number": 2, + "route_status": "RM", + "arrival_icao": "FJDG", + "route": { + "waypoints": [ + { + "name": "DOH" + }, + { + "name": "N300" + }, + { + "name": "" + }, + { + "name": "NOLSU" + }, + { + "name": "P307" + }, + { + "name": "" + }, + { + "name": "SETSI" + }, + { + "name": "P307" + }, + { + "name": "" + }, + { + "name": "PARAR" + }, + { + "name": "" + }, + { + "name": "", + "latitude": 20, + "longitude": 63 + }, + { + "name": "" + }, + { + "name": "RIGLO" + }, + { + "name": "L516" + }, + { + "name": "" + }, + { + "name": "ELKEL" + }, + { + "name": "L516" + }, + { + "name": "" + }, + { + "name": "BUMMR" + } + ] + }, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 58345 + }, + "remaining": { + "text": "WP,,,," + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-083.json b/corpus/wildcards/arinc_702/sample-083.json new file mode 100644 index 0000000..f751943 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-083.json @@ -0,0 +1,35 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 83", + "input": { + "label": "H1", + "text": "FPNEEE6" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Flight Plan", + "items": [ + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0xeee6" + } + ] + }, + "raw": { + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 61158 + }, + "remaining": { + "text": "" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-084.json b/corpus/wildcards/arinc_702/sample-084.json new file mode 100644 index 0000000..d0352f0 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-084.json @@ -0,0 +1,311 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 84", + "input": { + "label": "H1", + "text": "FPN/ID80094S,RCH411,8JZ41NG3S048/MR3,5/RP:DA:KMCF:AA:LBSF:F:PIE..BRUTS.Q109.CAMJO.Q109.YURCK.Q97.PAACK.Q97.BLENO.Q97.FRIAR..TOPPS..FROSS..RIKAL..N53000W050000..N55000W040000..N56000W030000..N56000W020000..PIKIL..SOVED..MIMKU..MAC..BELOX.L603.DOLAS..NAVPI..MAVAS..SOGPO..TIVUN..ESAMA..OSBIT..KOMIB..SULUS.Z650.VEMUT..PEPIK..BERVA..ERGOM..TEGRI..OSTOV..GOL:V:CAMJO,301,AT3100,,:V:PAACK,282,AT3300,,:V:BLENO,258,AT3700,,:V:N53000W050000,256,AT3700,,:V:N55000W040000,260,AT3700,,:V:SOVED,303,AT2700,,:V:MIMKU,302,AT2700,,:V:DOLAS,290,AT2900,,:V:PEPIK,242,AT3700,,:V:BERVA,260,AT3700,,7147/WD,,,,C850" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Flight Plan", + "items": [ + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "80094S" + }, + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "RCH411" + }, + { + "type": "sequence", + "code": "SEQ", + "label": "Sequence Number", + "value": 3 + }, + { + "type": "sequence", + "code": "SEQ_RESP", + "label": "Sequence Response", + "value": 5 + }, + { + "type": "status", + "code": "ROUTE_STATUS", + "label": "Route Status", + "value": "Route Planned" + }, + { + "type": "icao", + "code": "ORG", + "label": "Origin", + "value": "KMCF" + }, + { + "type": "icao", + "code": "DST", + "label": "Destination", + "value": "LBSF" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "PIE >> BRUTS > Q109 > CAMJO > Q109 > YURCK > Q97 > PAACK > Q97 > BLENO > Q97 > FRIAR >> TOPPS >> FROSS >> RIKAL >> (53.000 N, 50.000 W) >> (55.000 N, 40.000 W) >> (56.000 N, 30.000 W) >> (56.000 N, 20.000 W) >> PIKIL >> SOVED >> MIMKU >> MAC >> BELOX > L603 > DOLAS >> NAVPI >> MAVAS >> SOGPO >> TIVUN >> ESAMA >> OSBIT >> KOMIB >> SULUS > Z650 > VEMUT >> PEPIK >> BERVA >> ERGOM >> TEGRI >> OSTOV >> GOL" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0xc850" + } + ] + }, + "raw": { + "tail": "80094S", + "flight_number": "RCH411", + "mission_number": "8JZ41NG3S048", + "sequence_number": 3, + "sequence_response": 5, + "route_status": "RP", + "departure_icao": "KMCF", + "arrival_icao": "LBSF", + "route": { + "waypoints": [ + { + "name": "PIE" + }, + { + "name": "" + }, + { + "name": "BRUTS" + }, + { + "name": "Q109" + }, + { + "name": "CAMJO" + }, + { + "name": "Q109" + }, + { + "name": "YURCK" + }, + { + "name": "Q97" + }, + { + "name": "PAACK" + }, + { + "name": "Q97" + }, + { + "name": "BLENO" + }, + { + "name": "Q97" + }, + { + "name": "FRIAR" + }, + { + "name": "" + }, + { + "name": "TOPPS" + }, + { + "name": "" + }, + { + "name": "FROSS" + }, + { + "name": "" + }, + { + "name": "RIKAL" + }, + { + "name": "" + }, + { + "name": "", + "latitude": 53, + "longitude": -50 + }, + { + "name": "" + }, + { + "name": "", + "latitude": 55, + "longitude": -40 + }, + { + "name": "" + }, + { + "name": "", + "latitude": 56, + "longitude": -30 + }, + { + "name": "" + }, + { + "name": "", + "latitude": 56, + "longitude": -20 + }, + { + "name": "" + }, + { + "name": "PIKIL" + }, + { + "name": "" + }, + { + "name": "SOVED" + }, + { + "name": "" + }, + { + "name": "MIMKU" + }, + { + "name": "" + }, + { + "name": "MAC" + }, + { + "name": "" + }, + { + "name": "BELOX" + }, + { + "name": "L603" + }, + { + "name": "DOLAS" + }, + { + "name": "" + }, + { + "name": "NAVPI" + }, + { + "name": "" + }, + { + "name": "MAVAS" + }, + { + "name": "" + }, + { + "name": "SOGPO" + }, + { + "name": "" + }, + { + "name": "TIVUN" + }, + { + "name": "" + }, + { + "name": "ESAMA" + }, + { + "name": "" + }, + { + "name": "OSBIT" + }, + { + "name": "" + }, + { + "name": "KOMIB" + }, + { + "name": "" + }, + { + "name": "SULUS" + }, + { + "name": "Z650" + }, + { + "name": "VEMUT" + }, + { + "name": "" + }, + { + "name": "PEPIK" + }, + { + "name": "" + }, + { + "name": "BERVA" + }, + { + "name": "" + }, + { + "name": "ERGOM" + }, + { + "name": "" + }, + { + "name": "TEGRI" + }, + { + "name": "" + }, + { + "name": "OSTOV" + }, + { + "name": "" + }, + { + "name": "GOL" + } + ] + }, + "wind_data": [], + "checksum_algorithm": "GENIBUS", + "checksum": 51280 + }, + "remaining": { + "text": ":V:CAMJO,301,AT3100,,:V:PAACK,282,AT3300,,:V:BLENO,258,AT3700,,:V:N53000W050000,256,AT3700,,:V:N55000W040000,260,AT3700,,:V:SOVED,303,AT2700,,:V:MIMKU,302,AT2700,,:V:DOLAS,290,AT2900,,:V:PEPIK,242,AT3700,,:V:BERVA,260,AT3700,,7147" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-085.json b/corpus/wildcards/arinc_702/sample-085.json new file mode 100644 index 0000000..7da6987 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-085.json @@ -0,0 +1,229 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 85", + "input": { + "label": "H1", + "text": "PWI/WD390,COLZI,258070.AWYAT,252071.IPTAY,250065.CHOPZ,244069.MGMRY,234065.CATLN,230060/WD340,COLZI,256073,340M41.AWYAT,252070,340M41.IPTAY,244059,340M41.CHOPZ,240059,340M41.MGMRY,232056,340M41.CATLN,218053,340M40/WD300,COLZI,256065.AWYAT,254062.IPTAY,250051.CHOPZ,248050.MGMRY,232044.CATLN,222047/WD240,COLZI,260045.AWYAT,258048.IPTAY,254043.CHOPZ,256041.MGMRY,238035.CATLN,226034/DD300214059.240214040.180236024.100250018:,,,,/CB300246040.240246017.180226015.1002100080338" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Pilot Weather Information", + "items": [ + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "COLZI at FL390: 258° at 70kt" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "AWYAT at FL390: 252° at 71kt" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "IPTAY at FL390: 250° at 65kt" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "CHOPZ at FL390: 244° at 69kt" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "MGMRY at FL390: 234° at 65kt" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "CATLN at FL390: 230° at 60kt" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "COLZI at FL340: 256° at 73kt, -41°C at FL340" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "AWYAT at FL340: 252° at 70kt, -41°C at FL340" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "IPTAY at FL340: 244° at 59kt, -41°C at FL340" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "CHOPZ at FL340: 240° at 59kt, -41°C at FL340" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "MGMRY at FL340: 232° at 56kt, -41°C at FL340" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "CATLN at FL340: 218° at 53kt, -40°C at FL340" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "COLZI at FL300: 256° at 65kt" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "AWYAT at FL300: 254° at 62kt" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "IPTAY at FL300: 250° at 51kt" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "CHOPZ at FL300: 248° at 50kt" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "MGMRY at FL300: 232° at 44kt" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "CATLN at FL300: 222° at 47kt" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "COLZI at FL240: 260° at 45kt" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "AWYAT at FL240: 258° at 48kt" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "IPTAY at FL240: 254° at 43kt" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "CHOPZ at FL240: 256° at 41kt" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "MGMRY at FL240: 238° at 35kt" + }, + { + "type": "wind_data", + "code": "WIND", + "label": "Wind Data", + "value": "CATLN at FL240: 226° at 34kt" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x0338" + } + ] + }, + "raw": { + "wind_data": [ + { + "waypoint": { + "name": "COLZI" + }, + "flightLevel": 240, + "windDirection": 260, + "windSpeed": 45 + }, + { + "waypoint": { + "name": "AWYAT" + }, + "flightLevel": 240, + "windDirection": 258, + "windSpeed": 48 + }, + { + "waypoint": { + "name": "IPTAY" + }, + "flightLevel": 240, + "windDirection": 254, + "windSpeed": 43 + }, + { + "waypoint": { + "name": "CHOPZ" + }, + "flightLevel": 240, + "windDirection": 256, + "windSpeed": 41 + }, + { + "waypoint": { + "name": "MGMRY" + }, + "flightLevel": 240, + "windDirection": 238, + "windSpeed": 35 + }, + { + "waypoint": { + "name": "CATLN" + }, + "flightLevel": 240, + "windDirection": 226, + "windSpeed": 34 + } + ], + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 824 + }, + "remaining": { + "text": "DD300214059.240214040.180236024.100250018:,,,,/CB300246040.240246017.180226015.100210008" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-086.json b/corpus/wildcards/arinc_702/sample-086.json new file mode 100644 index 0000000..393c23e --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-086.json @@ -0,0 +1,25 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 86", + "input": { + "label": "H1", + "text": "PWI Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Unknown", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "PWI/,PWI Bogus message" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-087.json b/corpus/wildcards/arinc_702/sample-087.json new file mode 100644 index 0000000..791446a --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-087.json @@ -0,0 +1,163 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 87", + "input": { + "label": "4J", + "text": "POS/ID91459S,BANKR31,/DC03032024,142813/MR64,0/ET31539/PSN39277W077359,142800,240,N39300W077110,031430,N38560W077150,M28,27619,MT370/CG311,160,350/FB732/VR329071" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Position Report", + "items": [ + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "91459S" + }, + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "BANKR31" + }, + { + "type": "sequence", + "code": "SEQ", + "label": "Sequence Number", + "value": 64 + }, + { + "type": "sequence", + "code": "SEQ_RESP", + "label": "Sequence Response", + "value": 0 + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "3" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "15:39:00" + }, + { + "type": "aircraft_position", + "code": "POS", + "label": "Aircraft Position", + "value": "39.462 N, 77.598 W" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "(39.500 N, 77.183 W)@14:28:00 > (38.933 N, 77.250 W)@03:14:30 > ?" + }, + { + "type": "altitude", + "code": "ALT", + "label": "Altitude", + "value": "24000 feet" + }, + { + "type": "outside_air_temperature", + "code": "OATEMP", + "label": "Outside Air Temperature (C)", + "value": "-28 degrees" + }, + { + "type": "center_of_gravity", + "code": "CG", + "label": "Center of Gravity", + "value": "31.1 %" + }, + { + "type": "cg_lower_limit", + "code": "CG_LOWER", + "label": "Center of Gravity Lower Limit", + "value": "16 %" + }, + { + "type": "cg_upper_limit", + "code": "CG_UPPER", + "label": "Center of Gravity Upper Limit", + "value": "35 %" + }, + { + "type": "fuel_on_board", + "code": "FOB", + "label": "Fuel On Board", + "value": "732" + }, + { + "type": "version", + "code": "VERSION", + "label": "Message Version", + "value": "v3.2" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x9071" + } + ] + }, + "raw": { + "tail": "91459S", + "flight_number": "BANKR31", + "mission_number": "", + "message_timestamp": 1709476093, + "sequence_number": 64, + "sequence_response": 0, + "day": 3, + "eta_time": 56340, + "position": { + "latitude": 39.461666666666666, + "longitude": -77.59833333333333 + }, + "route": { + "waypoints": [ + { + "name": "", + "latitude": 39.5, + "longitude": -77.18333333333334, + "time": 52080 + }, + { + "name": "", + "latitude": 38.93333333333333, + "longitude": -77.25, + "time": 11670 + }, + { + "name": "?" + } + ] + }, + "altitude": 24000, + "outside_air_temperature": -28, + "center_of_gravity": 31.1, + "cg_lower_limit": 16, + "cg_upper_limit": 35, + "fuel_on_board": 732, + "version": 3.2, + "checksum_algorithm": "GENIBUS", + "checksum": 36977 + }, + "remaining": { + "text": "27619,MT370" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-088.json b/corpus/wildcards/arinc_702/sample-088.json new file mode 100644 index 0000000..a4f9c18 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-088.json @@ -0,0 +1,25 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 88", + "input": { + "label": "4J", + "text": "POS/ Bogus message" + }, + "expected": { + "decoded": false, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "none" + }, + "formatted": { + "description": "Unknown", + "items": [] + }, + "raw": {}, + "remaining": { + "text": "POS/ Bogus message" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-089.json b/corpus/wildcards/arinc_702/sample-089.json new file mode 100644 index 0000000..4eaf565 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-089.json @@ -0,0 +1,36 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 89", + "input": { + "label": "H1", + "text": "RESPWI/AC,5B/TS140956,070226/DI140953,140956,140956128C" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Response for Pilot Weather Information", + "items": [ + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x128c" + } + ] + }, + "raw": { + "message_timestamp": 1770473396, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 4748 + }, + "remaining": { + "text": "AC,5B/DI140953,140956,140956" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-090.json b/corpus/wildcards/arinc_702/sample-090.json new file mode 100644 index 0000000..7ce9302 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-090.json @@ -0,0 +1,35 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 90", + "input": { + "label": "H1", + "text": "RESPOS/AK,0711909" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Response for Position Report", + "items": [ + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x1909" + } + ] + }, + "raw": { + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 6409 + }, + "remaining": { + "text": "AK,071" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-091.json b/corpus/wildcards/arinc_702/sample-091.json new file mode 100644 index 0000000..c33354d --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-091.json @@ -0,0 +1,79 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 91", + "input": { + "label": "H1", + "text": "INR/ID91511S,,/DC04032026,143534/MR19,/NR,,,,,,,,950,0/ET041505/FB983/VR32BF4C" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "In-Range Report", + "items": [ + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "91511S" + }, + { + "type": "sequence", + "code": "SEQ", + "label": "Sequence Number", + "value": 19 + }, + { + "type": "day", + "code": "MSG_DAY", + "label": "Day of Month", + "value": "4" + }, + { + "type": "time", + "code": "ETA", + "label": "Estimated Time of Arrival", + "value": "15:05:00" + }, + { + "type": "fuel_on_board", + "code": "FOB", + "label": "Fuel On Board", + "value": "983" + }, + { + "type": "version", + "code": "VERSION", + "label": "Message Version", + "value": "v3.2" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0xbf4c" + } + ] + }, + "raw": { + "tail": "91511S", + "mission_number": "", + "message_timestamp": 1772634934, + "sequence_number": 19, + "day": 4, + "eta_time": 54300, + "fuel_on_board": 983, + "version": 3.2, + "checksum_algorithm": "GENIBUS", + "checksum": 48972 + }, + "remaining": { + "text": "NR,,,,,,,,950,0" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-092.json b/corpus/wildcards/arinc_702/sample-092.json new file mode 100644 index 0000000..1515953 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-092.json @@ -0,0 +1,122 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 92", + "input": { + "label": "H1", + "text": "REQPWI/WQ320:GEQUE.HACKS.XUB.VHP.KK60K.KIVDE.KK60E.KOVJY.KD54Y.ACZES.KD48S.DVC.KITTN.KATTS.RUMPS.OAL.INYOE.DYAMD/DQ320/SPGEQUE77CE" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Request for Pilot Weather Information", + "items": [ + { + "type": "requested_altitudes", + "code": "REQ_ALTS", + "label": "Requested Altitudes", + "value": "32000" + }, + { + "type": "aircraft_route", + "code": "ROUTE", + "label": "Aircraft Route", + "value": "GEQUE > HACKS > XUB > VHP > KK60K > KIVDE > KK60E > KOVJY > KD54Y > ACZES > KD48S > DVC > KITTN > KATTS > RUMPS > OAL > INYOE > DYAMD" + }, + { + "type": "desired_altitude", + "code": "DES_ALT", + "label": "Desired Altitude", + "value": "32000" + }, + { + "type": "start_point", + "code": "START", + "label": "Start Point", + "value": "GEQUE" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x77ce" + } + ] + }, + "raw": { + "requested_alts": [ + 32000 + ], + "route": { + "waypoints": [ + { + "name": "GEQUE" + }, + { + "name": "HACKS" + }, + { + "name": "XUB" + }, + { + "name": "VHP" + }, + { + "name": "KK60K" + }, + { + "name": "KIVDE" + }, + { + "name": "KK60E" + }, + { + "name": "KOVJY" + }, + { + "name": "KD54Y" + }, + { + "name": "ACZES" + }, + { + "name": "KD48S" + }, + { + "name": "DVC" + }, + { + "name": "KITTN" + }, + { + "name": "KATTS" + }, + { + "name": "RUMPS" + }, + { + "name": "OAL" + }, + { + "name": "INYOE" + }, + { + "name": "DYAMD" + } + ] + }, + "desired_alt": 32000, + "start_point": "GEQUE", + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 30670 + }, + "remaining": { + "text": "" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-093.json b/corpus/wildcards/arinc_702/sample-093.json new file mode 100644 index 0000000..e84da30 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-093.json @@ -0,0 +1,35 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 93", + "input": { + "label": "H1", + "text": "REQPOS037B" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Request for Position Report", + "items": [ + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x037b" + } + ] + }, + "raw": { + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 891 + }, + "remaining": { + "text": "" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-094.json b/corpus/wildcards/arinc_702/sample-094.json new file mode 100644 index 0000000..01bfd10 --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-094.json @@ -0,0 +1,57 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 94", + "input": { + "label": "H1", + "text": "#MDREQPOS/ID55150A,RCH892,LVZF1185C049/MR1,/AU39310" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "partial" + }, + "formatted": { + "description": "Request for Position Report", + "items": [ + { + "type": "tail", + "code": "TAIL", + "label": "Tail", + "value": "55150A" + }, + { + "type": "flight_number", + "code": "FLIGHT", + "label": "Flight Number", + "value": "RCH892" + }, + { + "type": "sequence", + "code": "SEQ", + "label": "Sequence Number", + "value": 1 + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x9310" + } + ] + }, + "raw": { + "tail": "55150A", + "flight_number": "RCH892", + "mission_number": "LVZF1185C049", + "sequence_number": 1, + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 37648 + }, + "remaining": { + "text": "AU3" + } + } +} diff --git a/corpus/wildcards/arinc_702/sample-095.json b/corpus/wildcards/arinc_702/sample-095.json new file mode 100644 index 0000000..cfc3c7b --- /dev/null +++ b/corpus/wildcards/arinc_702/sample-095.json @@ -0,0 +1,56 @@ +{ + "spec": "wildcards/arinc_702", + "source": "extracted from acars-decoder-typescript test suite via /tmp/corpus-extractor.js", + "description": "Auto-extracted sample 95", + "input": { + "label": "H1", + "text": "REQFPN/RN10001/RS:FP:Z5585 9736" + }, + "expected": { + "decoded": true, + "decoder": { + "name": "arinc-702", + "type": "pattern-match", + "decodeLevel": "full" + }, + "formatted": { + "description": "Request for Flight Plan", + "items": [ + { + "type": "route_number", + "code": "RTE_NUM", + "label": "Route Number", + "value": "10001" + }, + { + "type": "status", + "code": "ROUTE_STATUS", + "label": "Route Status", + "value": "Route Saved" + }, + { + "type": "flight_plan", + "code": "FPN", + "label": "Flight Plan", + "value": "Z5585" + }, + { + "type": "message_checksum", + "code": "CHECKSUM", + "label": "Message Checksum", + "value": "0x9736" + } + ] + }, + "raw": { + "route_number": "10001", + "route_status": "RS", + "flight_plan": "Z5585", + "checksum_algorithm": "IBM-SDLC reversed", + "checksum": 38710 + }, + "remaining": { + "text": "" + } + } +} From da5665d6d896d8c7a291439328de37d04f0186b1 Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Wed, 27 May 2026 23:59:21 -0700 Subject: [PATCH 08/23] Add reusable corpus-test.yml workflow for cross-language parity testing Companion to codegen-check.yml. Language repos call this from their CI to run the shared corpus through their decoder and fail on any divergence vs the expected output in each corpus sample file. Caller example: jobs: corpus: uses: airframesio/acars-decoder/.github/workflows/corpus-test.yml@main with: language: ts run-cmd: npm run test:corpus setup-cmd: npm ci Handles checkout-with-submodules and toolchain installation per language (Node for ts/rust/c, plus Rust toolchain for rust, plus CMake + cjson + zlib for c). The caller specifies the actual corpus-test run-cmd. Security hardening matches codegen-check.yml: inputs flow through env vars (not direct ${{ }} interpolation in shell), and an input-validation step rejects newlines, backticks, and $(...) in command strings. Sets up Stage 2.5 / Stage 3: each language repo's `test:corpus` implementation (TS first, then Rust, then C) will land in follow-up PRs; this workflow is the CI glue that runs them. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/corpus-test.yml | 100 ++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 .github/workflows/corpus-test.yml diff --git a/.github/workflows/corpus-test.yml b/.github/workflows/corpus-test.yml new file mode 100644 index 0000000..cfe03ae --- /dev/null +++ b/.github/workflows/corpus-test.yml @@ -0,0 +1,100 @@ +# Reusable workflow: language repos call this to run the shared ADS corpus +# through their decoder and fail on any divergence vs the expected output +# in each corpus sample file. +# +# Caller example: +# +# jobs: +# corpus: +# uses: airframesio/acars-decoder/.github/workflows/corpus-test.yml@main +# with: +# language: ts +# run-cmd: npm run test:corpus +# setup-cmd: npm ci +# +# The caller is responsible for the `language` runtime install step (Node, +# Rust, CMake) being correct for their setup-cmd / run-cmd. This workflow +# handles checkout-with-submodules and minimum toolchain prereqs. + +name: corpus-test + +on: + workflow_call: + inputs: + language: + type: string + required: true + description: "ts | rust | c — drives which language toolchain is installed." + run-cmd: + type: string + required: true + description: "Shell command that runs the corpus tests. Must exit non-zero on failure." + setup-cmd: + type: string + default: "" + description: "Optional setup command (e.g. 'npm ci', 'cargo build'). Runs before run-cmd." + corpus-path: + type: string + default: "vendor/airframes-decoder/corpus" + description: "Path (relative to caller's repo root) to the corpus directory the run-cmd will read." + +jobs: + corpus: + runs-on: ubuntu-latest + env: + LANGUAGE: ${{ inputs.language }} + RUN_CMD: ${{ inputs.run-cmd }} + SETUP_CMD: ${{ inputs.setup-cmd }} + CORPUS_PATH: ${{ inputs.corpus-path }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Validate inputs + run: | + case "$LANGUAGE" in + ts|rust|c) ;; + *) echo "::error::language must be one of: ts, rust, c (got '$LANGUAGE')"; exit 1 ;; + esac + # Reject shell metacharacters in commands' obvious-injection forms. + for var in RUN_CMD SETUP_CMD CORPUS_PATH; do + val="${!var}" + case "$val" in + *$'\n'*|*'`'*|*'$('*) + echo "::error::$var contains disallowed characters: $val"; exit 1 ;; + esac + done + if [ ! -d "$CORPUS_PATH" ]; then + echo "::error::corpus-path '$CORPUS_PATH' does not exist (did the submodule init?)"; exit 1 + fi + echo "corpus has $(find "$CORPUS_PATH" -name '*.json' | wc -l) sample(s)" + + - name: Set up Node + if: inputs.language == 'ts' || inputs.language == 'rust' || inputs.language == 'c' + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Set up Rust + if: inputs.language == 'rust' + uses: dtolnay/rust-toolchain@stable + + - name: Set up CMake + if: inputs.language == 'c' + uses: jwlawson/actions-setup-cmake@v2 + with: + cmake-version: "3.27.x" + + - name: Install C deps (cjson + zlib) + if: inputs.language == 'c' + run: | + sudo apt-get update + sudo apt-get install -y libcjson-dev zlib1g-dev + + - name: Setup + if: inputs.setup-cmd != '' + run: bash -c "$SETUP_CMD" + + - name: Run corpus tests + run: bash -c "$RUN_CMD" From e839bbc6fdc28825ba25d7cc1907573fc607db13 Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Thu, 28 May 2026 00:05:19 -0700 Subject: [PATCH 09/23] Emitter: suppress raw auto-emit for fields consumed by a formatter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes the double-bookkeeping issue identified in airframes-decoder#1's known-follow-ups: previously every field unconditionally wrote `result.raw. = ` even when a downstream formatter call also wrote raw under the formatter's canonical key (position, altitude, callsign, …). The original hand-written plugins only have the formatter write. The generated code's extra raw entries caused divergence from the existing tests and blocked the Stage 2 behavioral swap. Fix: pre-scan FormattedIR.items for `$varname` references and skip the auto-emit for fields whose name is consumed by a formatter. Custom formatters and free-text formatters are conservatively treated as non-consuming (so the auto-emit still happens), since we can't statically tell what the hatch will do with raw. Applied identically in emit-typescript.ts, emit-rust.ts, and emit-c.ts so the three targets produce equivalent shapes. Verified: regenerating from `spec/labels/10/POS.yaml` now produces a Label_10_POS.ts whose raw output matches the original acars-decoder-typescript/lib/plugins/Label_10_POS.ts byte-for-byte (raw has {position, altitude} only, not {latitude, longitude, altitude, position}). Unblocks the Stage 2 behavioral swap for the ~4 declarative ports (Label_10_POS, Label_44_POS, Label_44_IN/ON/OFF/ETA). Whole-plugin escape-hatch specs are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) --- codegen/src/emit-c.ts | 66 ++++++++++++++++++++++++++++------ codegen/src/emit-rust.ts | 54 ++++++++++++++++++++++++---- codegen/src/emit-typescript.ts | 64 +++++++++++++++++++++++++++++---- 3 files changed, 159 insertions(+), 25 deletions(-) diff --git a/codegen/src/emit-c.ts b/codegen/src/emit-c.ts index 32cb73a..b853aed 100644 --- a/codegen/src/emit-c.ts +++ b/codegen/src/emit-c.ts @@ -122,15 +122,18 @@ function emitDecode(spec: SpecIR, snake: string, _slug: string, out: string[]): return; } + // Suppress raw auto-emit for fields consumed by a formatter (see TS emitter). + const consumedByFormatter = collectFormatterRefs(spec.formatted); + for (const step of spec.parse.steps) { emitParseStep(step, out, " "); } if (spec.variants) { - emitVariants(spec.variants, out, " "); + emitVariants(spec.variants, out, " ", consumedByFormatter); } else if (spec.fields) { for (const field of spec.fields) { - emitField(field, out, " "); + emitField(field, out, " ", consumedByFormatter); } } @@ -215,23 +218,38 @@ function emitParseStep(step: ParseStep, out: string[], indent: string): void { } } -function emitField(field: FieldIR, out: string[], indent: string): void { +function emitField( + field: FieldIR, + out: string[], + indent: string, + consumedByFormatter: Set, +): void { const valueExpr = renderExpr(field.from); const decodeExpr = field.decode ? renderDecodeCall(field.decode, valueExpr) : valueExpr; + const skipAutoRaw = consumedByFormatter.has(field.name); if (field.when) { out.push(`${indent}if (${renderCondition(field.when)}) {`); - out.push(`${indent} ads_value_t ${field.name} = ${decodeExpr};`); - out.push( - `${indent} ads_result_raw_set(result, ${cString(field.name)}, ${field.name});`, - ); + out.push(`${indent} ads_value_t *${field.name} = ${decodeExpr};`); + if (!skipAutoRaw) { + out.push( + `${indent} ads_result_raw_set(result, ${cString(field.name)}, ${field.name});`, + ); + } out.push(`${indent}}`); } else { - out.push(`${indent}ads_value_t ${field.name} = ${decodeExpr};`); - out.push(`${indent}ads_result_raw_set(result, ${cString(field.name)}, ${field.name});`); + out.push(`${indent}ads_value_t *${field.name} = ${decodeExpr};`); + if (!skipAutoRaw) { + out.push(`${indent}ads_result_raw_set(result, ${cString(field.name)}, ${field.name});`); + } } } -function emitVariants(variants: VariantIR[], out: string[], indent: string): void { +function emitVariants( + variants: VariantIR[], + out: string[], + indent: string, + consumedByFormatter: Set, +): void { let first = true; for (const v of variants) { if (v.isDefault) { @@ -244,7 +262,7 @@ function emitVariants(variants: VariantIR[], out: string[], indent: string): voi out.push(`${indent}${first ? "if" : "else if"} (${cond}) {`); if (v.fields) { for (const f of v.fields) { - emitField(f, out, indent + " "); + emitField(f, out, indent + " ", consumedByFormatter); } } out.push(`${indent}}`); @@ -252,6 +270,32 @@ function emitVariants(variants: VariantIR[], out: string[], indent: string): voi } } +function collectFormatterRefs(formatted: FormattedIR): Set { + const refs = new Set(); + if (formatted.kind !== "structured") return refs; + for (const item of formatted.items) { + walkArgsForRefs(item.args, refs); + } + return refs; +} + +function walkArgsForRefs(node: unknown, refs: Set): void { + if (typeof node === "string") { + if (node.startsWith("$")) { + const m = node.match(/^\$([A-Za-z_][A-Za-z0-9_]*)/); + if (m && m[1]) refs.add(m[1]); + } + return; + } + if (Array.isArray(node)) { + for (const v of node) walkArgsForRefs(v, refs); + return; + } + if (node && typeof node === "object") { + for (const v of Object.values(node)) walkArgsForRefs(v, refs); + } +} + function emitFormatted(formatted: FormattedIR, out: string[], indent: string): void { if (formatted.kind === "custom") { out.push(`${indent}ads_hatch_${formatted.name}(result);`); diff --git a/codegen/src/emit-rust.ts b/codegen/src/emit-rust.ts index 3c1541a..85e166d 100644 --- a/codegen/src/emit-rust.ts +++ b/codegen/src/emit-rust.ts @@ -72,15 +72,18 @@ export function emitRust(spec: SpecIR): string { return out.join("\n") + "\n"; } + // Suppress raw auto-emit for fields consumed by a formatter (see TS emitter). + const consumedByFormatter = collectFormatterRefs(spec.formatted); + for (const step of spec.parse.steps) { emitParseStep(step, out, " "); } if (spec.variants) { - emitVariants(spec.variants, out, " "); + emitVariants(spec.variants, out, " ", consumedByFormatter); } else if (spec.fields) { for (const field of spec.fields) { - emitField(field, out, " "); + emitField(field, out, " ", consumedByFormatter); } } @@ -182,21 +185,32 @@ function emitParseStep(step: ParseStep, out: string[], indent: string): void { } } -function emitField(field: FieldIR, out: string[], indent: string): void { +function emitField( + field: FieldIR, + out: string[], + indent: string, + consumedByFormatter: Set, +): void { const valueExpr = renderExpr(field.from); const decodeExpr = field.decode ? renderDecodeCall(field.decode, valueExpr) : valueExpr; + const skipAutoRaw = consumedByFormatter.has(field.name); if (field.when) { out.push(`${indent}if ${renderCondition(field.when)} {`); out.push(`${indent} let ${field.name} = ${decodeExpr};`); - out.push(`${indent} result.raw.insert(${rustString(field.name)}, ${field.name}.into());`); + if (!skipAutoRaw) out.push(`${indent} result.raw.insert(${rustString(field.name)}, ${field.name}.into());`); out.push(`${indent}}`); } else { out.push(`${indent}let ${field.name} = ${decodeExpr};`); - out.push(`${indent}result.raw.insert(${rustString(field.name)}, ${field.name}.clone().into());`); + if (!skipAutoRaw) out.push(`${indent}result.raw.insert(${rustString(field.name)}, ${field.name}.clone().into());`); } } -function emitVariants(variants: VariantIR[], out: string[], indent: string): void { +function emitVariants( + variants: VariantIR[], + out: string[], + indent: string, + consumedByFormatter: Set, +): void { let first = true; for (const v of variants) { if (v.isDefault) { @@ -209,7 +223,7 @@ function emitVariants(variants: VariantIR[], out: string[], indent: string): voi out.push(`${indent}${first ? "if" : "else if"} ${cond} {`); if (v.fields) { for (const f of v.fields) { - emitField(f, out, indent + " "); + emitField(f, out, indent + " ", consumedByFormatter); } } out.push(`${indent}}`); @@ -217,6 +231,32 @@ function emitVariants(variants: VariantIR[], out: string[], indent: string): voi } } +function collectFormatterRefs(formatted: FormattedIR): Set { + const refs = new Set(); + if (formatted.kind !== "structured") return refs; + for (const item of formatted.items) { + walkArgsForRefs(item.args, refs); + } + return refs; +} + +function walkArgsForRefs(node: unknown, refs: Set): void { + if (typeof node === "string") { + if (node.startsWith("$")) { + const m = node.match(/^\$([A-Za-z_][A-Za-z0-9_]*)/); + if (m && m[1]) refs.add(m[1]); + } + return; + } + if (Array.isArray(node)) { + for (const v of node) walkArgsForRefs(v, refs); + return; + } + if (node && typeof node === "object") { + for (const v of Object.values(node)) walkArgsForRefs(v, refs); + } +} + function emitFormatted(formatted: FormattedIR, out: string[], indent: string): void { if (formatted.kind === "custom") { out.push(`${indent}escape_hatches::${formatted.name}(&mut result);`); diff --git a/codegen/src/emit-typescript.ts b/codegen/src/emit-typescript.ts index 09be2ff..8150d1b 100644 --- a/codegen/src/emit-typescript.ts +++ b/codegen/src/emit-typescript.ts @@ -67,16 +67,24 @@ export function emitTypeScript(spec: SpecIR): string { return out.join("\n") + "\n"; } + // Pre-scan the formatter for $varname references. Fields whose name is + // consumed by a formatter call don't need an auto-emit `result.raw.X = X`, + // because the formatter writes to `result.raw` under its own canonical key + // (position, altitude, callsign, …). Without this suppression, the + // generated code writes both raw.latitude AND raw.position, diverging from + // the hand-written plugins (which only have raw.position). + const consumedByFormatter = collectFormatterRefs(spec.formatted); + for (const step of spec.parse.steps) { emitParseStep(step, out, " "); } // Fields or Variants. if (spec.variants) { - emitVariants(spec.variants, out, " "); + emitVariants(spec.variants, out, " ", consumedByFormatter); } else if (spec.fields) { for (const field of spec.fields) { - emitField(field, out, " "); + emitField(field, out, " ", consumedByFormatter); } } @@ -188,22 +196,33 @@ function emitParseStep(step: ParseStep, out: string[], indent: string): void { } } -function emitField(field: FieldIR, out: string[], indent: string): void { +function emitField( + field: FieldIR, + out: string[], + indent: string, + consumedByFormatter: Set, +): void { const decodeExpr = field.decode ? renderDecodeCall(field.decode, renderExpr(field.from)) : renderExpr(field.from); + const skipAutoRaw = consumedByFormatter.has(field.name); if (field.when) { out.push(`${indent}if (${renderCondition(field.when)}) {`); out.push(`${indent} const ${field.name} = ${decodeExpr};`); - out.push(`${indent} result.raw.${field.name} = ${field.name};`); + if (!skipAutoRaw) out.push(`${indent} result.raw.${field.name} = ${field.name};`); out.push(`${indent}}`); } else { out.push(`${indent}const ${field.name} = ${decodeExpr};`); - out.push(`${indent}result.raw.${field.name} = ${field.name};`); + if (!skipAutoRaw) out.push(`${indent}result.raw.${field.name} = ${field.name};`); } } -function emitVariants(variants: VariantIR[], out: string[], indent: string): void { +function emitVariants( + variants: VariantIR[], + out: string[], + indent: string, + consumedByFormatter: Set, +): void { let first = true; for (const v of variants) { if (v.isDefault) { @@ -216,7 +235,7 @@ function emitVariants(variants: VariantIR[], out: string[], indent: string): voi out.push(`${indent}${first ? "if" : "else if"} (${cond}) {`); if (v.fields) { for (const f of v.fields) { - emitField(f, out, indent + " "); + emitField(f, out, indent + " ", consumedByFormatter); } } out.push(`${indent}}`); @@ -224,6 +243,37 @@ function emitVariants(variants: VariantIR[], out: string[], indent: string): voi } } +/** + * Pre-scan a FormattedIR for $varname references in formatter call args. + * Returns the set of bare variable names that are read by a formatter. + */ +function collectFormatterRefs(formatted: FormattedIR): Set { + const refs = new Set(); + if (formatted.kind !== "structured") return refs; + for (const item of formatted.items) { + walkArgsForRefs(item.args, refs); + } + return refs; +} + +function walkArgsForRefs(node: unknown, refs: Set): void { + if (typeof node === "string") { + if (node.startsWith("$")) { + // $name or $name.something or $name[N] — pull out the bare leading name. + const m = node.match(/^\$([A-Za-z_][A-Za-z0-9_]*)/); + if (m && m[1]) refs.add(m[1]); + } + return; + } + if (Array.isArray(node)) { + for (const v of node) walkArgsForRefs(v, refs); + return; + } + if (node && typeof node === "object") { + for (const v of Object.values(node)) walkArgsForRefs(v, refs); + } +} + function emitFormatted(formatted: FormattedIR, out: string[], indent: string): void { if (formatted.kind === "custom") { out.push(`${indent}hatches.${formatted.name}(result);`); From 80095f51a63e09ef830f43673c6d8931e438c53b Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Thu, 28 May 2026 00:32:01 -0700 Subject: [PATCH 10/23] Emitter: hoist when-gated field declarations so downstream formatters see them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the generated TS emitter wrote `if (cond) { const X = ...; }` which made X block-scoped — a downstream `ResultFormatter.X(result, X)` outside the if would throw ReferenceError when the guard was false. The original hand-written plugins implicitly relied on X being undefined in that case (the formatter handles undefined gracefully). Fix: declare `let X;` outside the if, assign inside. Variable is now in scope after the if, undefined when the guard fails — matching the original behavior. Surfaced by the Stage 2.5 pilot extension to Label_44_ON, whose fuel_remaining field is `when`-gated. Same shape of fix queued for Rust + C emitters (Option outside / NULL outside respectively) — pushed separately once verified in their respective build pipelines. Co-Authored-By: Claude Opus 4.7 (1M context) --- codegen/src/emit-typescript.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/codegen/src/emit-typescript.ts b/codegen/src/emit-typescript.ts index 8150d1b..5f6cc50 100644 --- a/codegen/src/emit-typescript.ts +++ b/codegen/src/emit-typescript.ts @@ -207,8 +207,13 @@ function emitField( : renderExpr(field.from); const skipAutoRaw = consumedByFormatter.has(field.name); if (field.when) { + // Declare outside the if so downstream formatters / variant-shared code + // can still reference the variable when the guard fails — it'll be + // undefined, matching the original hand-written plugins' implicit + // missing-field pattern. + out.push(`${indent}let ${field.name};`); out.push(`${indent}if (${renderCondition(field.when)}) {`); - out.push(`${indent} const ${field.name} = ${decodeExpr};`); + out.push(`${indent} ${field.name} = ${decodeExpr};`); if (!skipAutoRaw) out.push(`${indent} result.raw.${field.name} = ${field.name};`); out.push(`${indent}}`); } else { From ef58ee351a4a9815b4115716bf695553087221bf Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Thu, 28 May 2026 00:33:17 -0700 Subject: [PATCH 11/23] Emitter: map fuel formatter type to ResultFormatter.currentFuel (TS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TS runtime's ResultFormatter exposes the method as currentFuel, not fuel. Generated plugins were calling .fuel(...) which threw 'is not a function' at runtime — surfaced by the Label_44_ON labelindex test under the Stage 2.5 pilot. Wider audit of formatter method-name mismatches between the emitter map and the runtime is queued; this commit fixes the one that's currently exercised by tests. Co-Authored-By: Claude Opus 4.7 (1M context) --- codegen/src/emit-typescript.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codegen/src/emit-typescript.ts b/codegen/src/emit-typescript.ts index 5f6cc50..66d9905 100644 --- a/codegen/src/emit-typescript.ts +++ b/codegen/src/emit-typescript.ts @@ -306,7 +306,7 @@ function emitFormatterCall(item: FormatterCall, out: string[], indent: string): tail_number: "tail", airport_origin: "departureAirport", airport_destination: "arrivalAirport", - fuel: "fuel", + fuel: "currentFuel", free_text: "unknownArr", }; const method = methodMap[item.type]; From ce7d385257f4b57cc2454ba56c11795b7abad3a0 Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Thu, 28 May 2026 00:35:31 -0700 Subject: [PATCH 12/23] Runtime (TS): currentFuel tolerates undefined/NaN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a spec has a when-gated fuel field whose guard fails, the variable becomes undefined. Generated plugins still call ResultFormatter.currentFuel unconditionally, which previously crashed on .toString() of undefined. Original hand-written plugins guarded with 'if (value !== sentinel) ResultFormatter.X(...)' — i.e. they didn't call the formatter at all when the value was missing. The defensive guard inside currentFuel matches that semantic without requiring formatter-level when-clauses in the DSL. Same defensive pattern queued for the other ResultFormatter methods that may receive missing values; this commit fixes the one currently exercised by tests (Label_44_ON via the labelindex test). Co-Authored-By: Claude Opus 4.7 (1M context) --- runtimes/typescript/utils/result_formatter.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/runtimes/typescript/utils/result_formatter.ts b/runtimes/typescript/utils/result_formatter.ts index bba9448..341229c 100644 --- a/runtimes/typescript/utils/result_formatter.ts +++ b/runtimes/typescript/utils/result_formatter.ts @@ -198,7 +198,12 @@ export class ResultFormatter { }); } - static currentFuel(decodeResult: DecodeResult, value: number) { + static currentFuel(decodeResult: DecodeResult, value: number | undefined) { + // Tolerate undefined/NaN (e.g. when a spec's when-gated fuel field's + // guard failed and the value never got assigned). Matches the + // original hand-written plugins' pattern of only calling the + // formatter when they had a real value. + if (value === undefined || value === null || Number.isNaN(value)) return; decodeResult.raw.fuel_on_board = value; decodeResult.formatted.items.push({ type: 'fuel_on_board', From d5d478498a02e1112dbb52511b3a21f95dd2b041 Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Thu, 28 May 2026 01:00:08 -0700 Subject: [PATCH 13/23] Runtime (TS): make DecoderPlugin helpers public + export Arinc702Helper, FlightPlanUtils, parseIcaoFpl, MIAMCoreUtils, RouteUtils, ascii85Decode, base64ToUint8Array, inflateData Unblocks Stage 2.5 bulk escape-hatch implementations: hatches are free functions that need to call plugin.failUnknown / plugin.setDecodeLevel / plugin.debug / plugin.initResult, but those were protected. Now public. Also exports the helpers used by ARINC 702 / MIAM / OFP / route plugins that previously were only accessible via deep submodule paths. Documented inline why each was promoted (so a future reviewer doesn't silently re-protect them). Verified runtimes/typescript type-checks clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- runtimes/typescript/DecoderPlugin.ts | 16 ++++++++++++---- runtimes/typescript/index.ts | 7 +++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/runtimes/typescript/DecoderPlugin.ts b/runtimes/typescript/DecoderPlugin.ts index 34f3bff..378e195 100644 --- a/runtimes/typescript/DecoderPlugin.ts +++ b/runtimes/typescript/DecoderPlugin.ts @@ -33,8 +33,10 @@ export abstract class DecoderPlugin implements DecoderPluginInterface { /** * Creates a DecodeResult pre-populated with the plugin name, description, and message. * Replaces the common boilerplate at the start of every decode() method. + * + * Public so that escape-hatch functions invoked by generated plugins can call it. */ - protected initResult(message: Message, description: string): DecodeResult { + public initResult(message: Message, description: string): DecodeResult { const result = this.defaultResult(); result.decoder.name = this.name; result.formatted.description = description; @@ -46,8 +48,10 @@ export abstract class DecoderPlugin implements DecoderPluginInterface { * Sets the decoded flag and decodeLevel on a result. * If decoded is true and no explicit level is given, infers 'full' or 'partial' * based on whether remaining.text is set. + * + * Public so that escape-hatch functions invoked by generated plugins can call it. */ - protected setDecodeLevel( + public setDecodeLevel( result: DecodeResult, decoded: boolean, level?: 'full' | 'partial', @@ -63,8 +67,10 @@ export abstract class DecoderPlugin implements DecoderPluginInterface { /** * Logs a debug message prefixed with the plugin name, only if options.debug is true. + * + * Public so that escape-hatch functions invoked by generated plugins can call it. */ - protected debug(options: Options, ...args: unknown[]): void { + public debug(options: Options, ...args: unknown[]): void { if (options.debug) { console.log(`[${this.name}]`, ...args); } @@ -73,8 +79,10 @@ export abstract class DecoderPlugin implements DecoderPluginInterface { /** * Marks a result as a failed decode with 'none' level, sets the remaining text * as unknown, and logs a debug message. Returns the result for convenient early return. + * + * Public so that escape-hatch functions invoked by generated plugins can call it. */ - protected failUnknown( + public failUnknown( result: DecodeResult, text: string, options: Options = {}, diff --git a/runtimes/typescript/index.ts b/runtimes/typescript/index.ts index c35c8e9..5168e67 100644 --- a/runtimes/typescript/index.ts +++ b/runtimes/typescript/index.ts @@ -23,6 +23,13 @@ export type { export { ResultFormatter } from "./utils/result_formatter"; export { CoordinateUtils } from "./utils/coordinate_utils"; export { DateTimeUtils } from "./DateTimeUtils"; +export { Arinc702Helper } from "./utils/arinc_702_helper"; +export { FlightPlanUtils } from "./utils/flight_plan_utils"; +export { RouteUtils } from "./utils/route_utils"; +export { parseIcaoFpl } from "./utils/icao_fpl_utils"; +export { MIAMCoreUtils } from "./utils/miam"; +export { base64ToUint8Array, inflateData } from "./utils/compression"; +export { ascii85Decode } from "./utils/ascii85"; export type { Route } from "./types/route"; export type { Waypoint } from "./types/waypoint"; From c037de4cc1cef3144c26990dbdac1b2b473369e4 Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Thu, 28 May 2026 01:05:10 -0700 Subject: [PATCH 14/23] =?UTF-8?q?Emitter:=20smart=20slug=20with=20camelCas?= =?UTF-8?q?e=20boundaries=20(CBand=E2=86=92c-band,=20StarPOS=E2=86=92star-?= =?UTF-8?q?pos)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three boundary rules now insert hyphens to match legacy hand-written plugin names byte-for-byte: - lowercase→Uppercase (CBand → c-band) - digit→Uppercase if next is lowercase (3Line → 3-line; 4A stays 4a) - Uppercase→Uppercase if next is lowercase (StarPOS → star-pos) Fixes the Stage 2.5 bulk-swap labelindex test failure where CBand's generated name 'cband' didn't match the legacy 'c-band' that test assertions check for. Also future-proofs Label_1L_3Line and Label_H1_StarPOS. Verified slugs: Label_10_POS → label-10-pos Label_4A → label-4a ARINC_702 → arinc-702 CBand → c-band Label_1L_3Line → label-1l-3-line Label_H1_StarPOS → label-h1-star-pos Co-Authored-By: Claude Opus 4.7 (1M context) --- codegen/src/emit-typescript.ts | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/codegen/src/emit-typescript.ts b/codegen/src/emit-typescript.ts index 66d9905..9183a44 100644 --- a/codegen/src/emit-typescript.ts +++ b/codegen/src/emit-typescript.ts @@ -398,8 +398,27 @@ function hasExplicitDecodeLevelSetting(_spec: SpecIR): boolean { } function pluginNameToSlug(name: string): string { - // Plugin names are already snake-cased with capitals (Label_10_POS, ARINC_702, - // Label_H1_OHMA). Just lowercase and swap _ for - so we match the existing - // TS convention ("label-10-pos", "arinc-702", "label-h1-ohma"). - return name.replace(/_/g, "-").toLowerCase(); + // Insert hyphens at camelCase boundaries so legacy slugs are preserved + // byte-for-byte. Three boundary rules: + // - lowercase→Uppercase (CBand → C-Band) + // - digit→Uppercase if the next char is lowercase (3Line → 3-Line, but 4A → 4A) + // - Uppercase→Uppercase if the next char is lowercase (StarPOS → Star-POS) + // Then lowercase and swap _ for -. + let out = ""; + for (let i = 0; i < name.length; i++) { + const c = name[i] as string; + const prev = i > 0 ? (name[i - 1] as string) : ""; + const next = i + 1 < name.length ? (name[i + 1] as string) : ""; + if (i > 0 && /[A-Z]/.test(c)) { + if (/[a-z]/.test(prev)) { + out += "-"; + } else if (/[0-9]/.test(prev) && /[a-z]/.test(next)) { + out += "-"; + } else if (/[A-Z]/.test(prev) && /[a-z]/.test(next)) { + out += "-"; + } + } + out += c; + } + return out.replace(/_/g, "-").toLowerCase(); } From de6137f31e6e3561723bd56686db926bc8c9b99a Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Thu, 28 May 2026 01:08:48 -0700 Subject: [PATCH 15/23] =?UTF-8?q?Spec:=20Label=5F4A=20=E2=86=92=20whole-pl?= =?UTF-8?q?ugin=20hatch=20(matches=20the=20pattern=20of=20other=20complex?= =?UTF-8?q?=20plugins)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The variant+field+formatter-custom shape worked in theory but had no clean contract for who owns the formatted.items list when the formatter is custom. Other complex plugins (CBand, ARINC_702, MIAM, etc.) use the whole-plugin parse-custom + format-description shape; Label_4A follows suit. Implementation lives in lib/plugins/escape_hatches/Label_4A.ts in each language repo. Validates clean: 68 specs OK. Co-Authored-By: Claude Opus 4.7 (1M context) --- spec/labels/4A.yaml | 64 ++++++--------------------------------------- 1 file changed, 8 insertions(+), 56 deletions(-) diff --git a/spec/labels/4A.yaml b/spec/labels/4A.yaml index 98ffb35..428939e 100644 --- a/spec/labels/4A.yaml +++ b/spec/labels/4A.yaml @@ -7,62 +7,14 @@ plugin: qualifiers: labels: ["4A"] parse: - - { split: ",", into: fields } -variants: - # Variant 1: 11 fields → timestamp + tail + callsign + airports - - name: latest_new_format_11 - when: { equals: [{ length: $fields }, 11] } - fields: - - name: timestamp - from: $fields[0] - decode: { fn: timestamp_hhmmss } - - name: tail - from: $fields[2] - decode: { fn: tail_number, args: { strip_chars: "." } } - - name: callsign - from: $fields[3] - when: { not_equal: ["$fields[3]", ""] } - decode: { fn: callsign } - - name: departure_icao - from: $fields[4] - decode: { fn: airport } - - name: arrival_icao - from: $fields[5] - decode: { fn: airport } - - # Variant 2: 6 fields, first one starts with N or S — coords + waypoints. - # Complex enough that it gets an escape hatch. - - name: latest_new_format_6_position - when: - all: - - { equals: [{ length: $fields }, 6] } - - { matches: ["$fields[0]", "^[NS]"] } - fields: - - name: variant_2_result - from: $fields - decode: - fn: custom - custom: label_4a_variant_2_decode - - # Variant 3: 6 fields, not starting with N or S — timestamp + ETA + position - - name: latest_new_format_6_status - when: { equals: [{ length: $fields }, 6] } - fields: - - name: timestamp - from: $fields[0] - decode: { fn: timestamp_hhmmss } - - name: eta - from: $fields[1] - decode: { fn: timestamp_hhmmss } - - name: altitude - from: $fields[3] - decode: { fn: integer } - - name: position - from: $fields - decode: - fn: custom - custom: label_4a_variant_3_position - - default: fail + # Whole-plugin hatch. The original Label_4A.ts has three variants selected + # by field-count + first-char inspection, with each variant doing inline + # ResultFormatter calls (position via substring slicing, route with two + # waypoints, temperature, eta, altitude, callsign, tail, airports). The + # field-level + format-level decomposition the spec previously attempted + # didn't have a clean contract for formatter-owning-raw, so the hatch + # form is the right shape here. + custom: label_4a_dispatch formatted: description: "Latest New Format" custom: label_4a_format From 789bd9d36368037da9df92b1665cf8a89019173c Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Thu, 28 May 2026 01:18:53 -0700 Subject: [PATCH 16/23] Emitters: port when-hoist + smart-slug fixes to Rust + C MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the TS emitter fixes (init/ads-v1@80095f5 and @c037de4) so all three targets produce consistent generated code: - when-gated fields: declare outside the if, assign inside. Rust uses Option, C uses NULL-initialized pointer. - smart slug: insert hyphens at camelCase boundaries (CBand→c-band, StarPOS→star-pos, 3Line→3-line, Label_4A stays label-4a). Verified: rust + c emitters produce 68 plugins cleanly; CBand slug is now 'c-band' in both targets. Required prep before Stage 2.5 Rust + C escape-hatch bulk implementation. Co-Authored-By: Claude Opus 4.7 (1M context) --- codegen/src/emit-c.ts | 20 ++++++++++++++++++-- codegen/src/emit-rust.ts | 17 ++++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/codegen/src/emit-c.ts b/codegen/src/emit-c.ts index b853aed..87b5550 100644 --- a/codegen/src/emit-c.ts +++ b/codegen/src/emit-c.ts @@ -228,8 +228,11 @@ function emitField( const decodeExpr = field.decode ? renderDecodeCall(field.decode, valueExpr) : valueExpr; const skipAutoRaw = consumedByFormatter.has(field.name); if (field.when) { + // Hoist as NULL outside the if so downstream formatters see the + // pointer (NULL when the guard fails) instead of getting a scope error. + out.push(`${indent}ads_value_t *${field.name} = NULL;`); out.push(`${indent}if (${renderCondition(field.when)}) {`); - out.push(`${indent} ads_value_t *${field.name} = ${decodeExpr};`); + out.push(`${indent} ${field.name} = ${decodeExpr};`); if (!skipAutoRaw) { out.push( `${indent} ads_result_raw_set(result, ${cString(field.name)}, ${field.name});`, @@ -451,5 +454,18 @@ function pluginNameToSnake(name: string): string { } function pluginNameToSlug(name: string): string { - return name.replace(/_/g, "-").toLowerCase(); + // Smart slug — see emit-typescript.ts for the boundary rules. + let out = ""; + for (let i = 0; i < name.length; i++) { + const c = name[i] as string; + const prev = i > 0 ? (name[i - 1] as string) : ""; + const next = i + 1 < name.length ? (name[i + 1] as string) : ""; + if (i > 0 && /[A-Z]/.test(c)) { + if (/[a-z]/.test(prev)) out += "-"; + else if (/[0-9]/.test(prev) && /[a-z]/.test(next)) out += "-"; + else if (/[A-Z]/.test(prev) && /[a-z]/.test(next)) out += "-"; + } + out += c; + } + return out.replace(/_/g, "-").toLowerCase(); } diff --git a/codegen/src/emit-rust.ts b/codegen/src/emit-rust.ts index 85e166d..54cd480 100644 --- a/codegen/src/emit-rust.ts +++ b/codegen/src/emit-rust.ts @@ -396,5 +396,20 @@ function formattedDescription(formatted: FormattedIR): string { } function pluginNameToSlug(name: string): string { - return name.replace(/_/g, "-").toLowerCase(); + // Smart slug: insert hyphens at camelCase boundaries so generated names + // match the legacy TS names byte-for-byte (CBand → c-band, StarPOS → + // star-pos, 3Line → 3-line, but Label_4A stays label-4a). + let out = ""; + for (let i = 0; i < name.length; i++) { + const c = name[i] as string; + const prev = i > 0 ? (name[i - 1] as string) : ""; + const next = i + 1 < name.length ? (name[i + 1] as string) : ""; + if (i > 0 && /[A-Z]/.test(c)) { + if (/[a-z]/.test(prev)) out += "-"; + else if (/[0-9]/.test(prev) && /[a-z]/.test(next)) out += "-"; + else if (/[A-Z]/.test(prev) && /[a-z]/.test(next)) out += "-"; + } + out += c; + } + return out.replace(/_/g, "-").toLowerCase(); } From a21da8b9509c92d98d7a169b9f2bf5561076955a Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Thu, 28 May 2026 01:30:01 -0700 Subject: [PATCH 17/23] Emitter (Rust): actually apply when-hoist fix (was silently skipped earlier) A prior edit to emit-rust.ts's emitField when-branch failed silently due to a 'File has not been read yet' error in the harness; the smart- slug change landed but the when-hoist did not. Surfaced when Stage 2.5 Rust wiring produced 'cannot find value fuel_remaining' errors. Now matches the TS emitter's pattern: when-gated fields hoist to 'let X: Option = if cond { ... Some(v.into()) } else { None };' so downstream formatter calls see the variable. Co-Authored-By: Claude Opus 4.7 (1M context) --- codegen/src/emit-rust.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/codegen/src/emit-rust.ts b/codegen/src/emit-rust.ts index 54cd480..afb4fb3 100644 --- a/codegen/src/emit-rust.ts +++ b/codegen/src/emit-rust.ts @@ -195,10 +195,16 @@ function emitField( const decodeExpr = field.decode ? renderDecodeCall(field.decode, valueExpr) : valueExpr; const skipAutoRaw = consumedByFormatter.has(field.name); if (field.when) { - out.push(`${indent}if ${renderCondition(field.when)} {`); - out.push(`${indent} let ${field.name} = ${decodeExpr};`); - if (!skipAutoRaw) out.push(`${indent} result.raw.insert(${rustString(field.name)}, ${field.name}.into());`); - out.push(`${indent}}`); + // Hoist as Option so downstream formatters see the variable (None when + // the guard fails) instead of getting a scope error. Mirrors TS's + // `let X; if (cond) { X = ...; }` pattern. + out.push(`${indent}let ${field.name}: Option = if ${renderCondition(field.when)} {`); + out.push(`${indent} let v = ${decodeExpr};`); + if (!skipAutoRaw) out.push(`${indent} result.raw.insert(${rustString(field.name)}, v.clone().into());`); + out.push(`${indent} Some(v.into())`); + out.push(`${indent}} else {`); + out.push(`${indent} None`); + out.push(`${indent}};`); } else { out.push(`${indent}let ${field.name} = ${decodeExpr};`); if (!skipAutoRaw) out.push(`${indent}result.raw.insert(${rustString(field.name)}, ${field.name}.clone().into());`); From 6b475ffa13d670da1f2197abf1e4114b596a7748 Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Thu, 28 May 2026 01:34:33 -0700 Subject: [PATCH 18/23] Rust convergence: uniform helper signatures + tolerant formatters Three coordinated changes that bring the Rust emitter and runtime into agreement: 1. Emitter: always emits 2-arg helper calls (value + args_json), even when args is empty. Removes the special-case 1-arg dispatch that diverged from the runtime. 2. Emitter: stops appending .as_str() on regex Captures index access (the indexed access already returns &str; the .as_str() relied on the unstable str_as_str library feature). 3. Emitter: unknown_arr arg coercion uses .to_string() instead of .clone() so the Vec param matches. 4. Runtime helpers.rs: every decode-fn helper now accepts (value, args_json) uniformly. Args-free helpers ignore the second arg. 5. Runtime result_formatter.rs: every formatter method accepts impl Into> so when-gated fields (which now hoist to Option) flow through cleanly. None / Null / NaN inputs no-op, matching the TS pattern of guarding before calling the formatter. Expected to take Rust crate compile errors from 33 down to a small remainder for follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) --- codegen/src/emit-rust.ts | 35 +++++--- runtimes/rust/src/helpers.rs | 55 +++++++------ runtimes/rust/src/result_formatter.rs | 111 ++++++++++++++++---------- 3 files changed, 124 insertions(+), 77 deletions(-) diff --git a/codegen/src/emit-rust.ts b/codegen/src/emit-rust.ts index afb4fb3..21f1c59 100644 --- a/codegen/src/emit-rust.ts +++ b/codegen/src/emit-rust.ts @@ -314,7 +314,16 @@ function emitFormatterCall(item: FormatterCall, out: string[], indent: string): } } if (item.type === "free_text" && Array.isArray(item.args["values"])) { - const vals = (item.args["values"] as unknown[]).map(renderRustArg).join(", "); + // ResultFormatter::unknown_arr expects Vec; convert &str args + // via .to_string() at the call site rather than .clone() which yields &str. + const vals = (item.args["values"] as unknown[]) + .map((v) => { + if (typeof v === "string" && v.startsWith("$")) { + return `${renderExpr({ kind: "var", ref: v })}.to_string()`; + } + return renderRustArg(v); + }) + .join(", "); out.push(`${indent}ResultFormatter::unknown_arr(&mut result, vec![${vals}]);`); return; } @@ -338,14 +347,13 @@ function renderRustArg(v: unknown): string { } function renderDecodeCall(call: DecodeCall, valueExpr: string): string { + // Always emit two arguments — runtime helpers all accept (value, args_json) + // even when args is empty ('{}'). Uniform signature simplifies the runtime. + const argsJson = rustString(JSON.stringify(call.args)); if (call.fn === "custom") { - return `escape_hatches::${call.name}(${valueExpr}, ${rustString(JSON.stringify(call.args))})`; + return `escape_hatches::${call.name}(${valueExpr}, ${argsJson})`; } - const fn = call.fn; - if (Object.keys(call.args).length > 0) { - return `helpers::${fn}(${valueExpr}, ${rustString(JSON.stringify(call.args))})`; - } - return `helpers::${fn}(${valueExpr})`; + return `helpers::${call.fn}(${valueExpr}, ${argsJson})`; } function renderExpr(expr: ValueExpr): string { @@ -362,14 +370,17 @@ function renderExpr(expr: ValueExpr): string { } function varRefToRust(ref: string): string { - // $message.text → message.text (struct field) - // $parts[1] → parts[1] - // $m.unsplit_coords → &m["unsplit_coords"] (regex captures use named-group access) + // $message.text → message.text (struct field) + // $parts[1] → parts[1] + // $m.unsplit_coords → &m["unsplit_coords"] (regex::Captures Index<&str> → str) + // + // The earlier code appended .as_str() on the named-group access which is + // an unstable feature (str_as_str). Indexing a Captures by &str already + // returns a &str, so the extra .as_str() is unnecessary and unstable. let body = ref.slice(1); - // For named regex group access ($m.foo), rewrite as &m["foo"].as_str() body = body.replace(/^([a-z_][a-z0-9_]*)\.([a-z_][a-z0-9_]*)$/i, (_m, head, group) => { if (head === "message") return `${head}.${group}`; - return `&${head}[${rustString(group)}].as_str()`; + return `&${head}[${rustString(group)}]`; }); return body; } diff --git a/runtimes/rust/src/helpers.rs b/runtimes/rust/src/helpers.rs index 3ccdc41..138ed27 100644 --- a/runtimes/rust/src/helpers.rs +++ b/runtimes/rust/src/helpers.rs @@ -1,9 +1,11 @@ //! Decode-fn helpers the codegen emits calls into. //! //! Mirrors `runtimes/typescript/helpers.ts`. Each function corresponds to a -//! `decode.fn` value in spec YAML. Args arrive as JSON-encoded strings (the -//! emitter uses `JSON.stringify(args)` for portability); v1.1 should switch -//! to typed args. +//! `decode.fn` value in spec YAML. +//! +//! Uniform signature: every helper accepts `(value: &str, args_json: &str)`, +//! where `args_json` is `"{}"` when the spec specifies no args. This lets +//! the emitter always emit a 2-arg call, simplifying both sides. use serde_json::Value as JsonValue; use crate::ascii85; @@ -49,11 +51,7 @@ pub fn coordinate_decimal_minutes(value: &str, _args_json: &str) -> JsonValue { // ─── numerics ──────────────────────────────────────────────────────────────── -pub fn integer(value: &str) -> JsonValue { - integer_with_args(value, "{}") -} - -pub fn integer_with_args(value: &str, args_json: &str) -> JsonValue { +pub fn integer(value: &str, args_json: &str) -> JsonValue { let args = parse_args(args_json); let mut s = value; let owned: String; @@ -70,23 +68,37 @@ pub fn integer_with_args(value: &str, args_json: &str) -> JsonValue { JsonValue::from(n * mult) } -pub fn float(value: &str) -> JsonValue { +pub fn float(value: &str, _args_json: &str) -> JsonValue { let n: f64 = value.parse().unwrap_or(0.0); JsonValue::from(n) } // ─── strings ───────────────────────────────────────────────────────────────── -pub fn string(value: &str) -> JsonValue { JsonValue::String(value.to_string()) } -pub fn trim(value: &str) -> JsonValue { JsonValue::String(value.trim().to_string()) } -pub fn uppercase(value: &str) -> JsonValue { JsonValue::String(value.to_uppercase()) } -pub fn lowercase(value: &str) -> JsonValue { JsonValue::String(value.to_lowercase()) } +pub fn string(value: &str, _args_json: &str) -> JsonValue { + JsonValue::String(value.to_string()) +} +pub fn trim(value: &str, _args_json: &str) -> JsonValue { + JsonValue::String(value.trim().to_string()) +} +pub fn uppercase(value: &str, _args_json: &str) -> JsonValue { + JsonValue::String(value.to_uppercase()) +} +pub fn lowercase(value: &str, _args_json: &str) -> JsonValue { + JsonValue::String(value.to_lowercase()) +} // ─── identifiers ───────────────────────────────────────────────────────────── -pub fn callsign(value: &str) -> JsonValue { JsonValue::String(value.trim().to_string()) } -pub fn flight_number(value: &str) -> JsonValue { JsonValue::String(value.trim().to_string()) } -pub fn airport(value: &str) -> JsonValue { JsonValue::String(value.trim().to_uppercase()) } +pub fn callsign(value: &str, _args_json: &str) -> JsonValue { + JsonValue::String(value.trim().to_string()) +} +pub fn flight_number(value: &str, _args_json: &str) -> JsonValue { + JsonValue::String(value.trim().to_string()) +} +pub fn airport(value: &str, _args_json: &str) -> JsonValue { + JsonValue::String(value.trim().to_uppercase()) +} pub fn tail_number(value: &str, args_json: &str) -> JsonValue { let args = parse_args(args_json); @@ -101,11 +113,7 @@ pub fn tail_number(value: &str, args_json: &str) -> JsonValue { // ─── timestamps ────────────────────────────────────────────────────────────── -pub fn timestamp_hhmmss(value: &str) -> JsonValue { - parse_hhmmss_to_tod(value) -} - -pub fn timestamp_hhmmss_with_args(value: &str, args_json: &str) -> JsonValue { +pub fn timestamp_hhmmss(value: &str, args_json: &str) -> JsonValue { let args = parse_args(args_json); let s = if let Some(app) = args.get("append").and_then(JsonValue::as_str) { format!("{}{}", value, app) @@ -123,7 +131,7 @@ fn parse_hhmmss_to_tod(s: &str) -> JsonValue { JsonValue::from(h * 3600 + m * 60 + sec) } -// ─── binary / encoding ─────────────────────────────────────────────────────── +// ─── binary / encoding (called via parse steps, not decode-fns) ────────────── pub fn base64_decode(value: &str) -> Vec { use base64::{engine::general_purpose::STANDARD, Engine}; @@ -143,8 +151,6 @@ pub fn inflate(bytes: &[u8], format: &str) -> Vec { } pub fn text_decode(bytes: &[u8], _encoding: &str) -> String { - // ASCII / Latin1 / UTF-8 all round-trip safely through from_utf8_lossy - // for ACARS message text; specialize when a real divergence appears. String::from_utf8_lossy(bytes).into_owned() } @@ -161,7 +167,6 @@ pub fn hex_decode(value: &str) -> Vec { // ─── bitfield ──────────────────────────────────────────────────────────────── -/// Extract bits [start..end] (inclusive) from a byte with bit 0 = MSB. pub fn bitslice(byte: u8, start: u8, end: u8) -> u32 { let width = end - start + 1; let shift = 8 - end - 1; diff --git a/runtimes/rust/src/result_formatter.rs b/runtimes/rust/src/result_formatter.rs index 6561377..258d073 100644 --- a/runtimes/rust/src/result_formatter.rs +++ b/runtimes/rust/src/result_formatter.rs @@ -3,12 +3,27 @@ //! Mirrors the TS `ResultFormatter` static-class API. Function names match //! `runtimes/typescript/utils/result_formatter.ts` so the cross-language corpus //! sees identical output shapes. +//! +//! Every method that takes a value accepts `impl Into>` so +//! callers can pass either `JsonValue` or `Option` (the latter +//! arises from when-gated fields where the guard may have failed). Methods +//! no-op on None / Null / NaN, matching the original hand-written plugins' +//! pattern of only emitting items when they had a real value. use serde_json::Value as JsonValue; use crate::plugin::{DecodeResult, FormattedItem}; pub struct ResultFormatter; +fn into_value(v: impl Into>) -> Option { + let opt = v.into(); + match opt { + Some(JsonValue::Null) => None, + Some(JsonValue::Number(ref n)) if n.as_f64().map(|f| f.is_nan()).unwrap_or(false) => None, + other => other, + } +} + impl ResultFormatter { fn push(result: &mut DecodeResult, kind: &'static str, code: &'static str, label: &str, value: String) { result.formatted.items.push(FormattedItem { @@ -19,83 +34,99 @@ impl ResultFormatter { }); } - pub fn position(result: &mut DecodeResult, latitude: JsonValue, longitude: JsonValue) { + pub fn position( + result: &mut DecodeResult, + latitude: impl Into>, + longitude: impl Into>, + ) { + let lat_v = match into_value(latitude) { Some(v) => v, None => return }; + let lon_v = match into_value(longitude) { Some(v) => v, None => return }; result.raw.insert("position", serde_json::json!({ - "latitude": latitude, - "longitude": longitude, + "latitude": lat_v, + "longitude": lon_v, })); - let lat = latitude.as_f64().unwrap_or(0.0); - let lon = longitude.as_f64().unwrap_or(0.0); + let lat = lat_v.as_f64().unwrap_or(0.0); + let lon = lon_v.as_f64().unwrap_or(0.0); let lat_dir = if lat >= 0.0 { "N" } else { "S" }; let lon_dir = if lon >= 0.0 { "E" } else { "W" }; let display = format!("{:.3} {}, {:.3} {}", lat.abs(), lat_dir, lon.abs(), lon_dir); Self::push(result, "aircraft_position", "ARP", "Aircraft Position", display); } - /// Position from a pre-computed `{latitude, longitude}` value. - pub fn position_value(result: &mut DecodeResult, value: JsonValue) { - let lat = value.get("latitude").and_then(JsonValue::as_f64).unwrap_or(0.0); - let lon = value.get("longitude").and_then(JsonValue::as_f64).unwrap_or(0.0); - Self::position(result, lat.into(), lon.into()); + pub fn position_value(result: &mut DecodeResult, value: impl Into>) { + let v = match into_value(value) { Some(v) => v, None => return }; + let lat = v.get("latitude").and_then(JsonValue::as_f64).unwrap_or(0.0); + let lon = v.get("longitude").and_then(JsonValue::as_f64).unwrap_or(0.0); + Self::position(result, JsonValue::from(lat), JsonValue::from(lon)); } - pub fn altitude(result: &mut DecodeResult, value: JsonValue) { - let n = value.as_f64().unwrap_or(0.0); - result.raw.insert("altitude", value); + pub fn altitude(result: &mut DecodeResult, value: impl Into>) { + let v = match into_value(value) { Some(v) => v, None => return }; + let n = v.as_f64().unwrap_or(0.0); + result.raw.insert("altitude", v); Self::push(result, "altitude", "ALT", "Altitude", format!("{} feet", n)); } - pub fn speed(result: &mut DecodeResult, value: JsonValue) { - let n = value.as_f64().unwrap_or(0.0); - result.raw.insert("speed", value); + pub fn speed(result: &mut DecodeResult, value: impl Into>) { + let v = match into_value(value) { Some(v) => v, None => return }; + let n = v.as_f64().unwrap_or(0.0); + result.raw.insert("speed", v); Self::push(result, "speed", "SPD", "Speed", format!("{} knots", n)); } - pub fn heading(result: &mut DecodeResult, value: JsonValue) { - let n = value.as_f64().unwrap_or(0.0); - result.raw.insert("heading", value); + pub fn heading(result: &mut DecodeResult, value: impl Into>) { + let v = match into_value(value) { Some(v) => v, None => return }; + let n = v.as_f64().unwrap_or(0.0); + result.raw.insert("heading", v); Self::push(result, "heading", "HDG", "Heading", format!("{}°", n)); } - pub fn timestamp(result: &mut DecodeResult, value: JsonValue) { - let n = value.as_i64().unwrap_or(0); - result.raw.insert("message_timestamp", value); + pub fn timestamp(result: &mut DecodeResult, value: impl Into>) { + let v = match into_value(value) { Some(v) => v, None => return }; + let n = v.as_i64().unwrap_or(0); + result.raw.insert("message_timestamp", v); Self::push(result, "timestamp", "TS", "Timestamp", n.to_string()); } - pub fn callsign(result: &mut DecodeResult, value: JsonValue) { - let s = value.as_str().unwrap_or("").to_string(); - result.raw.insert("callsign", value); + pub fn callsign(result: &mut DecodeResult, value: impl Into>) { + let v = match into_value(value) { Some(v) => v, None => return }; + let s = v.as_str().unwrap_or("").to_string(); + result.raw.insert("callsign", v); Self::push(result, "callsign", "CS", "Callsign", s); } - pub fn flight_number(result: &mut DecodeResult, value: JsonValue) { - let s = value.as_str().unwrap_or("").to_string(); - result.raw.insert("flight_number", value); + pub fn flight_number(result: &mut DecodeResult, value: impl Into>) { + let v = match into_value(value) { Some(v) => v, None => return }; + let s = v.as_str().unwrap_or("").to_string(); + result.raw.insert("flight_number", v); Self::push(result, "flight_number", "FLT", "Flight Number", s); } - pub fn tail(result: &mut DecodeResult, value: JsonValue) { - let s = value.as_str().unwrap_or("").to_string(); - result.raw.insert("tail", value); + pub fn tail(result: &mut DecodeResult, value: impl Into>) { + let v = match into_value(value) { Some(v) => v, None => return }; + let s = v.as_str().unwrap_or("").to_string(); + result.raw.insert("tail", v); Self::push(result, "tail", "TAIL", "Tail Number", s); } - pub fn departure_airport(result: &mut DecodeResult, value: JsonValue) { - let s = value.as_str().unwrap_or("").to_string(); - result.raw.insert("departure_icao", value); + pub fn departure_airport(result: &mut DecodeResult, value: impl Into>) { + let v = match into_value(value) { Some(v) => v, None => return }; + let s = v.as_str().unwrap_or("").to_string(); + result.raw.insert("departure_icao", v); Self::push(result, "airport_origin", "DEP", "Origin", s); } - pub fn arrival_airport(result: &mut DecodeResult, value: JsonValue) { - let s = value.as_str().unwrap_or("").to_string(); - result.raw.insert("arrival_icao", value); + pub fn arrival_airport(result: &mut DecodeResult, value: impl Into>) { + let v = match into_value(value) { Some(v) => v, None => return }; + let s = v.as_str().unwrap_or("").to_string(); + result.raw.insert("arrival_icao", v); Self::push(result, "airport_destination", "ARR", "Destination", s); } - pub fn fuel(result: &mut DecodeResult, value: JsonValue) { - let n = value.as_f64().unwrap_or(0.0); - result.raw.insert("fuel_on_board", value); + pub fn fuel(result: &mut DecodeResult, value: impl Into>) { + let v = match into_value(value) { Some(v) => v, None => return }; + let n = v.as_f64().unwrap_or(0.0); + result.raw.insert("fuel_on_board", v); Self::push(result, "fuel", "FUEL", "Fuel", n.to_string()); } From 0b56f9c6fb36256b73d0574fd05ccc8c5368b90e Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Thu, 28 May 2026 01:36:55 -0700 Subject: [PATCH 19/23] Emitter (Rust): borrow shapes for regex captures, inflate, text_decode, decode calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the remaining 4 type-mismatch errors from the convergence pass: - regex captures: pass &onExpr so String values (message.text) coerce to &str. - deflate: inflate takes &[u8]; src is Vec, so always borrow. - text_decode: same — borrow the Vec source. - renderDecodeCall: borrow the value expr by default (skip when expr already starts with & / * / numeric literal). Lets owned values (String from text_decode, Vec) flow through to helpers/hatches that take references. Co-Authored-By: Claude Opus 4.7 (1M context) --- codegen/src/emit-rust.ts | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/codegen/src/emit-rust.ts b/codegen/src/emit-rust.ts index 21f1c59..d542777 100644 --- a/codegen/src/emit-rust.ts +++ b/codegen/src/emit-rust.ts @@ -106,7 +106,9 @@ function emitParseStep(step: ParseStep, out: string[], indent: string): void { case "regex": { const onExpr = renderExpr({ kind: "var", ref: step.on }); out.push(`${indent}let ${step.into}_re = helpers::regex(${rustString(step.pattern)});`); - out.push(`${indent}let ${step.into} = match ${step.into}_re.captures(${onExpr}) {`); + // .captures(&str) — pass a reference; works whether onExpr is String + // (message.text) or &str (parts[n]) via Rust's deref coercion. + out.push(`${indent}let ${step.into} = match ${step.into}_re.captures(&${onExpr}) {`); out.push(`${indent} Some(c) => c,`); out.push(`${indent} None => return result.fail_unknown(&message.text),`); out.push(`${indent}};`); @@ -151,7 +153,8 @@ function emitParseStep(step: ParseStep, out: string[], indent: string): void { break; case "deflate": { const srcExpr = renderExpr({ kind: "var", ref: step.source }); - const sliced = step.offset ? `&${srcExpr}[${step.offset}..]` : srcExpr; + // helpers::inflate takes &[u8]; src is Vec, so always borrow. + const sliced = step.offset ? `&${srcExpr}[${step.offset}..]` : `&${srcExpr}`; out.push( `${indent}let ${step.into} = helpers::inflate(${sliced}, ${rustString(step.format)});`, ); @@ -163,8 +166,9 @@ function emitParseStep(step: ParseStep, out: string[], indent: string): void { ); break; case "text_decode": + // helpers::text_decode takes &[u8]; src is Vec, so borrow. out.push( - `${indent}let ${step.into} = helpers::text_decode(${renderExpr({ kind: "var", ref: step.source })}, ${rustString(step.encoding)});`, + `${indent}let ${step.into} = helpers::text_decode(&${renderExpr({ kind: "var", ref: step.source })}, ${rustString(step.encoding)});`, ); break; case "hex_decode": @@ -349,11 +353,25 @@ function renderRustArg(v: unknown): string { function renderDecodeCall(call: DecodeCall, valueExpr: string): string { // Always emit two arguments — runtime helpers all accept (value, args_json) // even when args is empty ('{}'). Uniform signature simplifies the runtime. + // + // Borrow the value expr so String / owned vars are passed as &str. For + // already-&str vars (e.g. parts[N] from Vec<&str>), Rust's auto-deref + // collapses the extra reference layer. const argsJson = rustString(JSON.stringify(call.args)); + const borrowed = needsBorrow(valueExpr) ? `&${valueExpr}` : valueExpr; if (call.fn === "custom") { - return `escape_hatches::${call.name}(${valueExpr}, ${argsJson})`; + return `escape_hatches::${call.name}(${borrowed}, ${argsJson})`; } - return `helpers::${call.fn}(${valueExpr}, ${argsJson})`; + return `helpers::${call.fn}(${borrowed}, ${argsJson})`; +} + +function needsBorrow(expr: string): boolean { + // Skip explicit borrows / dereferences / numeric literals — they're + // already in the right shape. + if (expr.startsWith("&")) return false; + if (expr.startsWith("*")) return false; + if (/^-?\d/.test(expr)) return false; + return true; } function renderExpr(expr: ValueExpr): string { From fae352df61c955261a075ca82ee9b8f6dff81910 Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Thu, 28 May 2026 01:46:01 -0700 Subject: [PATCH 20/23] Runtime (C): declare prototypes for all 61 hatches + 4 field/formatter hatches Stage 2.5 C convergence prep. C compiler needs forward declarations; the generated plugins call ads_hatch_(...) and would otherwise produce implicit-function-declaration errors. Three signatures: - parse hatches (61): (msg, result, opts) -> result - field decode (3): (value, args_json) -> value - formatter (1): (result) -> void Implementations live in each language repo's src/escape_hatches/ (the central runtime can't implement plugin-specific logic). Co-Authored-By: Claude Opus 4.7 (1M context) --- runtimes/c/include/ads_escape_hatches.h | 103 ++++++++++++++++++++---- 1 file changed, 89 insertions(+), 14 deletions(-) diff --git a/runtimes/c/include/ads_escape_hatches.h b/runtimes/c/include/ads_escape_hatches.h index d0778ed..097edb4 100644 --- a/runtimes/c/include/ads_escape_hatches.h +++ b/runtimes/c/include/ads_escape_hatches.h @@ -1,18 +1,21 @@ -/* ads_escape_hatches.h — placeholder for per-plugin custom functions. +/* ads_escape_hatches.h — prototypes for per-plugin custom functions. * - * As specs are ported and reveal needed hatches, add prototypes here and - * implementations under src/escape_hatches/. See docs/ESCAPE_HATCHES.md. + * Implementations live in each language repo's src/escape_hatches/ + * (the central runtime can't implement them — they're plugin-specific). * - * Currently expected (from the 5 reference specs): - * ads_hatch_arinc_702_dispatch - * ads_hatch_arinc_702_format - * ads_hatch_label_4a_variant_2_decode - * ads_hatch_label_4a_variant_3_position - * ads_hatch_label_4a_format - * ads_hatch_ohma_unwrap_message - * ads_hatch_ohma_message_item - * ads_hatch_parse_flight_level_or_ground - * ads_hatch_flight_level_to_altitude_feet + * Three hatch shapes: + * + * Whole-plugin parse hatch: called from a generated plugin's decode(): + * ads_decode_result_t *ads_hatch_(const ads_message_t *msg, + * ads_decode_result_t *result, + * const ads_options_t *opts); + * + * Field-level decode hatch: called from a generated field assignment: + * ads_value_t *ads_hatch_(const char *value, const char *args_json); + * (or const ads_value_t * for value-from-prior-step variants) + * + * Formatter hatch: called from a generated formatter item: + * void ads_hatch_(ads_decode_result_t *result); */ #ifndef ADS_ESCAPE_HATCHES_H #define ADS_ESCAPE_HATCHES_H @@ -23,7 +26,79 @@ extern "C" { #endif -/* Prototypes added per-plugin during Stage 2 implementation. */ +/* ─── Whole-plugin parse hatches (61 plugins) ────────────────────────────── */ + +ads_decode_result_t *ads_hatch_arinc_702_dispatch(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_cband_dispatch(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_colon_comma_parse(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_10_ldr_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_10_slash_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_12_n_space_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_12_pos_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_13_18_slash_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_15_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_15_fst_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_16_autpos_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_16_honeywell_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_16_n_space_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_16_posa1_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_16_tod_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_1l_070_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_1l_3line_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_1l_660_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_1l_slash_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_1m_slash_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_20_cfb01_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_20_pos_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_21_pos_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_22_off_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_22_pos_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_24_slash_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_2p_fm3_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_2p_fm4_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_2p_fm5_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_30_slash_ea_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_44_slash_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_4a_dispatch(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_4a_01_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_4a_dis_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_4a_door_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_4a_slash_01_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_4n_decode(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_4t_agfsr_parse(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_4t_eta_parse(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_58_parse(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_5z_slash_parse(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_80_parse(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_83_parse(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_8e_parse(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_b6_forwardslash_parse(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_h1_atis_parse(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_h1_ezf_parse(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_h1_flr_parse(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_h1_m_pos_parse(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_h1_ofp_parse(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_h1_paren_parse(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_h1_starpos_parse(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_h1_wrn_parse(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_h2_02e_dispatch(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_hx_dispatch(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_ma_dispatch(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_qp_dispatch(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_qq_dispatch(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_qr_dispatch(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_qs_dispatch(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); +ads_decode_result_t *ads_hatch_label_sq_dispatch(const ads_message_t *msg, ads_decode_result_t *result, const ads_options_t *opts); + +/* ─── Field-level decode hatches ──────────────────────────────────────────── */ + +ads_value_t *ads_hatch_parse_flight_level_or_ground(const char *value, const char *args_json); +ads_value_t *ads_hatch_flight_level_to_altitude_feet(const ads_value_t *value, const char *args_json); +ads_value_t *ads_hatch_ohma_unwrap_message(const char *value, const char *args_json); + +/* ─── Formatter-level hatches ─────────────────────────────────────────────── */ + +void ads_hatch_ohma_message_item(ads_decode_result_t *result); #ifdef __cplusplus } From 9ae7aa8dc6e2d1effba1046cc4e888d5bc741297 Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Thu, 28 May 2026 01:47:38 -0700 Subject: [PATCH 21/23] Runtime (C): uniform 2-arg helper signatures (matches Rust convergence) Every decode-fn helper now accepts (value, args_json), where args_json is '{}' when the spec specifies no args. Args-free helpers (decode_float, decode_string, etc.) just ignore the second arg. Removes the *_args / *_with_args duplicate functions; they had unified implementations behind separate names which confused the emitter dispatch. Now the emitter always emits the same 2-arg form regardless of whether args is empty. Same fix shape as the Rust convergence pass (init/ads-v1@6b475ff). Both runtimes are now consistent. Co-Authored-By: Claude Opus 4.7 (1M context) --- runtimes/c/include/ads_helpers.h | 25 +++++++------- runtimes/c/src/helpers.c | 58 ++++++++++++++++++++------------ 2 files changed, 49 insertions(+), 34 deletions(-) diff --git a/runtimes/c/include/ads_helpers.h b/runtimes/c/include/ads_helpers.h index a3413db..c71f3bb 100644 --- a/runtimes/c/include/ads_helpers.h +++ b/runtimes/c/include/ads_helpers.h @@ -30,22 +30,23 @@ void ads_regex_match_free(ads_regex_match_t *m); bool ads_regex_test(const char *pattern, const char *input); /* ─── Decode-fn helpers ──────────────────────────────────────────────────── */ +/* Uniform signature: every decode-fn helper accepts (value, args_json), + * where args_json is "{}" when the spec specifies no args. Simplifies the + * emitter and matches the Rust runtime's convergence pass. */ ads_value_t *ads_decode_coordinate(const char *value, const char *args_json); ads_value_t *ads_decode_coordinate_decimal_minutes(const char *value, const char *args_json); -ads_value_t *ads_decode_integer(const char *value); -ads_value_t *ads_decode_integer_args(const char *value, const char *args_json); -ads_value_t *ads_decode_float(const char *value); -ads_value_t *ads_decode_string(const char *value); -ads_value_t *ads_decode_trim(const char *value); -ads_value_t *ads_decode_uppercase(const char *value); -ads_value_t *ads_decode_lowercase(const char *value); -ads_value_t *ads_decode_timestamp_hhmmss(const char *value); -ads_value_t *ads_decode_timestamp_hhmmss_args(const char *value, const char *args_json); -ads_value_t *ads_decode_callsign(const char *value); +ads_value_t *ads_decode_integer(const char *value, const char *args_json); +ads_value_t *ads_decode_float(const char *value, const char *args_json); +ads_value_t *ads_decode_string(const char *value, const char *args_json); +ads_value_t *ads_decode_trim(const char *value, const char *args_json); +ads_value_t *ads_decode_uppercase(const char *value, const char *args_json); +ads_value_t *ads_decode_lowercase(const char *value, const char *args_json); +ads_value_t *ads_decode_timestamp_hhmmss(const char *value, const char *args_json); +ads_value_t *ads_decode_callsign(const char *value, const char *args_json); ads_value_t *ads_decode_tail_number(const char *value, const char *args_json); -ads_value_t *ads_decode_flight_number(const char *value); -ads_value_t *ads_decode_airport(const char *value); +ads_value_t *ads_decode_flight_number(const char *value, const char *args_json); +ads_value_t *ads_decode_airport(const char *value, const char *args_json); /* ─── Binary / encoding ──────────────────────────────────────────────────── */ diff --git a/runtimes/c/src/helpers.c b/runtimes/c/src/helpers.c index 16f8b9e..1c6bd7e 100644 --- a/runtimes/c/src/helpers.c +++ b/runtimes/c/src/helpers.c @@ -1,4 +1,8 @@ -/* Decode-fn helpers (numerics, strings, identifiers, encodings, bitfields). */ +/* Decode-fn helpers (numerics, strings, identifiers, encodings, bitfields). + * + * Uniform signature: every helper accepts (value, args_json). Args-free + * helpers ignore the second arg. Matches the Rust runtime's pattern. + */ #include "ads_helpers.h" #include @@ -19,11 +23,7 @@ static char *substr_copy(const char *s, size_t start, size_t len) { return out; } -ads_value_t *ads_decode_integer(const char *value) { - return ads_value_from_int(value ? atoll(value) : 0); -} - -ads_value_t *ads_decode_integer_args(const char *value, const char *args_json) { +ads_value_t *ads_decode_integer(const char *value, const char *args_json) { cJSON *args = args_json ? cJSON_Parse(args_json) : NULL; size_t start = 0; long len = -1; @@ -37,6 +37,7 @@ ads_value_t *ads_decode_integer_args(const char *value, const char *args_json) { if (cJSON_IsNumber(m)) mult = m->valuedouble; } cJSON_Delete(args); + if (!value) return ads_value_from_double(0.0); char *s = len >= 0 ? substr_copy(value, start, (size_t)len) : substr_copy(value, start, strlen(value)); double n = atof(s) * mult; @@ -44,10 +45,23 @@ ads_value_t *ads_decode_integer_args(const char *value, const char *args_json) { return ads_value_from_double(n); } -ads_value_t *ads_decode_float(const char *value) { return ads_value_from_double(value ? atof(value) : 0.0); } -ads_value_t *ads_decode_string(const char *value) { return ads_value_from_string(value ? value : ""); } -ads_value_t *ads_decode_callsign(const char *value) { return ads_decode_string(value); } -ads_value_t *ads_decode_flight_number(const char *v) { return ads_decode_string(v); } +ads_value_t *ads_decode_float(const char *value, const char *args_json) { + (void)args_json; + return ads_value_from_double(value ? atof(value) : 0.0); +} + +ads_value_t *ads_decode_string(const char *value, const char *args_json) { + (void)args_json; + return ads_value_from_string(value ? value : ""); +} + +ads_value_t *ads_decode_callsign(const char *value, const char *args_json) { + return ads_decode_string(value, args_json); +} + +ads_value_t *ads_decode_flight_number(const char *value, const char *args_json) { + return ads_decode_string(value, args_json); +} static char *trim_inplace(char *s) { char *start = s; @@ -58,7 +72,8 @@ static char *trim_inplace(char *s) { return s; } -ads_value_t *ads_decode_trim(const char *value) { +ads_value_t *ads_decode_trim(const char *value, const char *args_json) { + (void)args_json; char *copy = strdup(value ? value : ""); trim_inplace(copy); ads_value_t *v = ads_value_from_string(copy); @@ -66,7 +81,8 @@ ads_value_t *ads_decode_trim(const char *value) { return v; } -ads_value_t *ads_decode_uppercase(const char *value) { +ads_value_t *ads_decode_uppercase(const char *value, const char *args_json) { + (void)args_json; char *copy = strdup(value ? value : ""); for (char *p = copy; *p; p++) *p = (char)toupper((unsigned char)*p); ads_value_t *v = ads_value_from_string(copy); @@ -74,7 +90,8 @@ ads_value_t *ads_decode_uppercase(const char *value) { return v; } -ads_value_t *ads_decode_lowercase(const char *value) { +ads_value_t *ads_decode_lowercase(const char *value, const char *args_json) { + (void)args_json; char *copy = strdup(value ? value : ""); for (char *p = copy; *p; p++) *p = (char)tolower((unsigned char)*p); ads_value_t *v = ads_value_from_string(copy); @@ -82,7 +99,8 @@ ads_value_t *ads_decode_lowercase(const char *value) { return v; } -ads_value_t *ads_decode_airport(const char *value) { +ads_value_t *ads_decode_airport(const char *value, const char *args_json) { + (void)args_json; char *copy = strdup(value ? value : ""); trim_inplace(copy); for (char *p = copy; *p; p++) *p = (char)toupper((unsigned char)*p); @@ -121,20 +139,16 @@ static int64_t hhmmss_to_tod(const char *s) { return (int64_t)h * 3600 + (int64_t)m * 60 + sec; } -ads_value_t *ads_decode_timestamp_hhmmss(const char *value) { - return ads_value_from_int(hhmmss_to_tod(value)); -} - -ads_value_t *ads_decode_timestamp_hhmmss_args(const char *value, const char *args_json) { +ads_value_t *ads_decode_timestamp_hhmmss(const char *value, const char *args_json) { cJSON *args = args_json ? cJSON_Parse(args_json) : NULL; const cJSON *app = args ? cJSON_GetObjectItemCaseSensitive(args, "append") : NULL; char *combined; if (app && cJSON_IsString(app)) { - size_t n = strlen(value) + strlen(app->valuestring) + 1; + size_t n = strlen(value ? value : "") + strlen(app->valuestring) + 1; combined = malloc(n); - snprintf(combined, n, "%s%s", value, app->valuestring); + snprintf(combined, n, "%s%s", value ? value : "", app->valuestring); } else { - combined = strdup(value); + combined = strdup(value ? value : ""); } cJSON_Delete(args); int64_t tod = hhmmss_to_tod(combined); From c6438df66d9cea6cbb140703223bcb41a2c3e3c9 Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Thu, 28 May 2026 01:48:30 -0700 Subject: [PATCH 22/23] Emitter (C): always emit 2-arg decode-fn form (matches uniform runtime sigs) --- codegen/src/emit-c.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/codegen/src/emit-c.ts b/codegen/src/emit-c.ts index 87b5550..3cc51ae 100644 --- a/codegen/src/emit-c.ts +++ b/codegen/src/emit-c.ts @@ -372,13 +372,14 @@ function renderCArg(v: unknown): string { } function renderDecodeCall(call: DecodeCall, valueExpr: string): string { + // Always emit 2-arg form. Runtime helpers and hatches all accept + // (value, args_json) uniformly; args_json is "{}" when no args. + // Matches the Rust emitter's convergence-pass behavior. + const argsJson = cString(JSON.stringify(call.args)); if (call.fn === "custom") { - return `ads_hatch_${call.name}(${valueExpr}, ${cString(JSON.stringify(call.args))})`; + return `ads_hatch_${call.name}(${valueExpr}, ${argsJson})`; } - if (Object.keys(call.args).length > 0) { - return `ads_decode_${call.fn}(${valueExpr}, ${cString(JSON.stringify(call.args))})`; - } - return `ads_decode_${call.fn}(${valueExpr})`; + return `ads_decode_${call.fn}(${valueExpr}, ${argsJson})`; } function renderExpr(expr: ValueExpr): string { From 520f7b243e0fcc77dccdf1d438e5798c62558419 Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Thu, 28 May 2026 01:49:44 -0700 Subject: [PATCH 23/23] Emitter (C): use ads_regex_match_new (pointer) instead of value form ads_regex_match_t is opaque (forward-declared in ads_runtime.h), so the emitted code can't declare a value of it. Switch to the heap-allocated ads_regex_match_new() and pass the pointer to ads_regex_match_ok / ads_regex_group. --- codegen/src/emit-c.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/codegen/src/emit-c.ts b/codegen/src/emit-c.ts index 3cc51ae..0fe8571 100644 --- a/codegen/src/emit-c.ts +++ b/codegen/src/emit-c.ts @@ -151,10 +151,12 @@ function emitParseStep(step: ParseStep, out: string[], indent: string): void { ); break; case "regex": + // ads_regex_match_t is opaque (forward-declared); use the heap-allocated + // pointer form instead of trying to declare a value. out.push( - `${indent}ads_regex_match_t ${step.into} = ads_regex_match(${cString(step.pattern)}, ${renderExpr({ kind: "var", ref: step.on })});`, + `${indent}ads_regex_match_t *${step.into} = ads_regex_match_new(${cString(step.pattern)}, ${renderExpr({ kind: "var", ref: step.on })});`, ); - out.push(`${indent}if (!ads_regex_match_ok(&${step.into})) {`); + out.push(`${indent}if (!ads_regex_match_ok(${step.into})) {`); out.push(`${indent} return ads_result_fail_unknown(result, msg->text);`); out.push(`${indent}}`); break; @@ -397,17 +399,16 @@ function renderExpr(expr: ValueExpr): string { } function varRefToC(body: string): string { - // message.text → msg->text - // parts[1] → parts.items[1] - // m.unsplit_coords → ads_regex_group(&m, "unsplit_coords") + // message.text → msg->text + // parts[1] → parts.items[1] + // m.unsplit_coords → ads_regex_group(m, "unsplit_coords") (m is now a pointer) if (body.startsWith("message.")) { return body.replace(/^message\./, "msg->"); } const m = body.match(/^([a-z_][a-z0-9_]*)\.([a-z_][a-z0-9_]*)$/i); if (m) { - return `ads_regex_group(&${m[1]}, ${cString(m[2]!)})`; + return `ads_regex_group(${m[1]}, ${cString(m[2]!)})`; } - // parts[N] → parts.items[N] return body.replace(/^([a-z_][a-z0-9_]*)\[(\d+)\]$/i, "$1.items[$2]"); }