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.holdis 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, andcrate::reticlekeeps its no-physics property precisely because nothing here re-derives them.reticle.describeresolves 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 carriesfocal_planeandmagnification_dependentso a UI knows whether a magnification control changes anything.reticle.importreads the two third-party formats the crate already parses (Ventum JSON,.reticleXML), each with its report. Both are TEXT and travel inline, so unlikeprofile.import_a7pthere is no base64 step.reticle.cataloglists 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 itssource, 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 carriesapproximation— a required enum that is eitherAvailablewith intervals for both muzzle velocity and BC, orUnavailablewith 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_truinghas no uncertainty model, so its result is a bare point value with no interval. Callers must not present it withtrue.fit’s confidence.true.windis 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 fortrue.windspecifically.true.tall_targetreturns a scope’s tracking correction factor from a tall-target test.true.dsfderives 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.detailsconvention: a machine-readablereason(invalid_input,supersonic,out_of_range,degenerate_drop,forward_model) alongside the message;error.codestayscommand_failedfor all commands, so existing callers are unaffected.true.planrecommends 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 structurederror.details, under the samereasonkey as every other command in this family (invalid_request,insufficient_reachable_candidates,no_feasible_design) plus therejected_candidatesdiagnostics the typed error itself carries.true.dial_planturns a TRUE angular correction into ranked dial/hold/hybrid execution plans for an INLINE optic (crate::truing_service::dial_plan_v1, wrappingcrate::optic::plan_corrections). Unlike the CLI’sdial-plan --profilemode, 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 structurederror.details: a stablereasonperOpticErrorvariant.
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§
- Bridge
Error Code - Machine-readable bridge error codes. Distinct from any command’s own error
vocabulary: a
command_failedcarries the command’s typed error indetails.
Constants§
- BRIDGE_
API_ VERSION - Bridge envelope version. Bumped only for breaking envelope changes.
- MAX_
A7P_ DECODED_ BYTES - Hard cap on the DECODED
.a7ppayload accepted byprofile.import_a7p. Real files are a few KiB; this exists purely as a resource bound (the request envelope’s ownMAX_REQUEST_BYTESalready caps the base64 text). - MAX_
PDF_ BYTES - Hard cap on the PDF
card.pdfwill 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.importwill parse. - MAX_
RETICLE_ HOLDS - Most holds
reticle.holdswill 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.