Module bridge

Module bridge 

Source
Expand description

Versioned JSON command bridge for embedded (mobile/FFI) consumers.

One entry point, bridge_call, accepts a JSON envelope and returns a JSON envelope. Request semantics live in the transport-free library services (starting with crate::solve_v1()); this module contains only the envelope contract, command dispatch, and panic containment. The C ABI wrapper lives in crate::bridge::ffi (feature ffi).

§Envelope contract (v1)

Request:

{ "api_version": 1, "command": "solve", "request": { ... } }

Success response:

{ "ok": true, "api_version": 1, "engine_version": "0.33.1",
  "command": "solve", "result": { ... } }

Error response (always in-band; the bridge never signals failure any other way):

{ "ok": false, "api_version": 1, "engine_version": "0.33.1",
  "error": { "code": "command_failed", "message": "...", "details": { ... } } }

Compatibility policy: the envelope itself rejects unknown fields (a caller that misspells command should hear about it), while inner request payloads follow each command’s own schema discipline (e.g. solve uses the solve-json v1 decoder, which also rejects unknown fields with location info). New commands and new OPTIONAL response fields may appear within api_version 1; anything that would break an existing well-formed caller bumps BRIDGE_API_VERSION. Callers feature-detect with meta.capabilities instead of sniffing versions.

§The atmosphere.* family

atmosphere.density_altitude describes the air a solve would fly through, without solving anything (crate::atmosphere_service). Its request is {"atmosphere": {...}} where the inner object is EXACTLY solve-json v1’s atmosphere — SI units, humidity as a fraction, optional pressure_reference — decoded by the solve’s own shape validator and resolved by the solve’s own resolve_atmosphere. Validation errors, ICAO defaults (announced in assumptions) and the QNH reduction are therefore the solve’s: the air described is the air solved through. On top of that it refuses air the solve does not yet refuse — a temperature outside −100..100 °C, a pressure or density altitude outside the troposphere, a humidity beyond the total pressure — which is what °C sent as kelvin or hPa sent as pascals looks like.

The result reports pressure altitude, the CIPM-2007 air density the solver uses, and density altitude under TWO names, never one: faa_rule (NWS pressure altitude plus the FAA 120 ft/°C rule; humidity-free, the formula the DOPE card header uses, the exact inverse of density-altitude entry) and density_matched (the ISA altitude of equal actual density, humidity included — what humidity-aware tools, the NWS’s own calculator among them, report). They differ for reasons other than humidity — the FAA rule is a straight line — so an app must pick one by name and must not label their difference a humidity correction. Every altitude carries both m and ft.

Errors are shaped like solve’s: a missing payload is invalid_request; anything wrong with the payload — a malformed or unit-suffixed field, an out-of-range value, impossible air — is command_failed with the solve-json error envelope ($.atmosphere.* path) in error.details. Unconditional — present on every target including wasm32 — and additive within api_version 1.

§.a7p interop: profile.import_a7p and profile.export_a7p

Both directions of the ArcherBC2 format, and both are LOSSY in a way the caller is required to surface. Import reports unmapped — what the file held that a ProfileData cannot. Export (MBA-1556) reports not_carried — every ProfileData field the file cannot hold, listed unconditionally with a populated flag, plus the pre-filtered dropped_fields for the ones this profile really did lose.

The asymmetry is deliberate. On import the user is gaining a profile and can see what arrived; on export they are handing a rifle to somebody else and cannot see what arrives at the far end. An app that renders “exported” without rendering the drop list has told a shooter something untrue, so the list travels in the result rather than being something a caller has to ask for.

Export never extends the format to make room: .a7p belongs to somebody else, and smuggling our fields into unused field numbers would produce files Archer’s own tools misread.

§The reticle.* family

reticle.describe, reticle.hold and reticle.import put the engine’s reticle stack (MBA-1361, MBA-1440, MBA-1544) in reach of an app. Before MBA-1558 none of it was: the bridge named no reticle command, and since the apps vendor only ballistics_bridge_call and friends, a capability the bridge does not name does not exist for them.

  • reticle.hold is the one the family exists for — given a firing solution ALREADY reduced to angles and the optic’s current magnification, it reports which mark to hold on. It runs no physics: the angles are inputs, and crate::reticle keeps its no-physics property precisely because nothing here re-derives them.
  • reticle.describe resolves a reticle, supplied inline or built from a generator, and optionally reports every mark’s TRUE angular position at a magnification. This is what a picker and a drawing are built on. It carries focal_plane and magnification_dependent so a UI knows whether a magnification control changes anything.
  • reticle.import reads the two third-party formats the crate already parses (Ventum JSON, .reticle XML), each with its report. Both are TEXT and travel inline, so unlike profile.import_a7p there is no base64 step.
  • reticle.catalog lists the NAMED reticles this build can produce (crate::reticle_catalog, MBA-1545), which is what an app picker is populated from — hardcoding ids in an app means a picker that offers what a pinned engine refuses. Each entry carries its source, and that provenance reaches the wire on purpose: a wrong subtension is a hold wrong by a mark and looks entirely normal, so what makes it traceable belongs where the person reading the reticle can see it.

Two shapes are shared across the family and are worth stating once. A reticle is named by EXACTLY ONE of reticle (a full description), generator, or catalog (an id from reticle.catalog) — a request naming more than one is refused rather than resolved by precedence, because supplying more than one is a caller bug and picking a winner hides it. An unknown catalog id is refused for the same reason and never substituted: a shooter handed a different reticle is being shown holds for glass they are not looking through. And because every generator returns FFP, focal_plane / reference_magnification may be supplied ALONGSIDE a generator to make an SFP reticle; the same keys beside a full reticle are refused, since that description already carries its own.

Errors follow true.dsf’s convention: error.code stays command_failed and a stable reason rides in error.details (one per ReticleError variant) with the offending numbers beside it. Additive within api_version 1, and listed by meta.capabilities.

§The true.* truing family

true.fit, true.wind, true.tall_target, true.dsf, true.plan, and true.dial_plan expose the engine’s truing methods. All six are unconditional (no filesystem access, so all six are present on wasm32), but they are not otherwise uniform:

  • true.fit (joint MV+BC truing) is backed by the uncertainty solver, so its result always carries approximation — a required enum that is either Available with intervals for both muzzle velocity and BC, or Unavailable with a reason, never simply absent. There is deliberately no command that returns a bare truing point estimate.
  • true.wind (effective crosswind from an observed miss) is the one exception to that guarantee: solve_wind_truing has no uncertainty model, so its result is a bare point value with no interval. Callers must not present it with true.fit’s confidence. true.wind is also the one command whose wire shape is SI throughout (range_m, miss_right_m, sigma_m) while every other command, including the rest of this family, is imperial; apps convert at the boundary for true.wind specifically.
  • true.tall_target returns a scope’s tracking correction factor from a tall-target test.
  • true.dsf derives a single Mach-keyed drop-scale-factor point from an observed transonic drop; it never persists into a profile’s DSF table, which is the caller’s job. It established this family’s structured-error.details convention: a machine-readable reason (invalid_input, supersonic, out_of_range, degenerate_drop, forward_model) alongside the message; error.code stays command_failed for all commands, so existing callers are unaffected.
  • true.plan recommends which candidate ranges to shoot for a joint MV/BC truing experiment (crate::truing_plan::plan_truing_experiment_v1, wired directly — no new service function). Its error also carries structured error.details, under the same reason key as every other command in this family (invalid_request, insufficient_reachable_candidates, no_feasible_design) plus the rejected_candidates diagnostics the typed error itself carries.
  • true.dial_plan turns a TRUE angular correction into ranked dial/hold/hybrid execution plans for an INLINE optic (crate::truing_service::dial_plan_v1, wrapping crate::optic::plan_corrections). Unlike the CLI’s dial-plan --profile mode, there is no profile-loading path here — the optic is supplied inline in the request, since a saved-profile filesystem read must not enter this bridge. Its error also carries structured error.details: a stable reason per OpticError variant.

None of this needed a BRIDGE_API_VERSION bump: the six commands are additive within api_version 1, and meta.capabilities lists all six for feature detection.

Modules§

ffi
C ABI for the JSON command bridge — the entire mobile-facing surface.

Enums§

BridgeErrorCode
Machine-readable bridge error codes. Distinct from any command’s own error vocabulary: a command_failed carries the command’s typed error in details.

Constants§

BRIDGE_API_VERSION
Bridge envelope version. Bumped only for breaking envelope changes.
MAX_A7P_DECODED_BYTES
Hard cap on the DECODED .a7p payload accepted by profile.import_a7p. Real files are a few KiB; this exists purely as a resource bound (the request envelope’s own MAX_REQUEST_BYTES already caps the base64 text).
MAX_PDF_BYTES
Hard cap on the PDF card.pdf will hand back, measured on the RAW document (the base64 text in the response is ~4/3 of it, so this bounds a ~5.6 MiB response body).
MAX_REQUEST_BYTES
Hard cap on request size, matching the solve-json transport.
MAX_RETICLE_DOCUMENT_BYTES
Largest reticle document reticle.import will parse.
MAX_RETICLE_HOLDS
Most holds reticle.holds will place in one call.

Functions§

bridge_call
Process one bridge exchange. Never panics; every failure mode is an in-band error envelope. This is the function the C ABI wraps.