TacticalAPI

The TacticalAPI plugin connects Bowire to Rheinmetall's situational-awareness gRPC interface: bundled-schema discovery (no Server Reflection on the target needed), typed unary invoke for GetSituationObjects / AddOrUpdateSituationObjects / DeleteSituationObjects, the server-streaming pump for SubscribeSituationObjectEvents, URL-scheme normalisation (tacticalapi@host:port, grpc(s)://, bare host:port), and mTLS via the shared __bowireMtls__ marker alongside the legacy _bowire:client-cert-pfx keys.

The TacticalAPI plugin connects Bowire to Rheinmetall's TacticalAPI — a gRPC interface for situational-awareness systems. The plugin ships the upstream service schema bundled with the package, so Bowire can render the Situation service tree against any TacticalAPI server even when the server does not expose gRPC Server Reflection.

Package: Kuestenlogik.Bowire.Protocol.TacticalApi (sibling repo, not bundled with the CLI)

Install

dotnet add package Kuestenlogik.Bowire.Protocol.TacticalApi

Bowire discovers the plugin automatically via assembly scanning — no extra registration code.

Use

bowire --url tacticalapi@my-situation-server:50051

Open the workbench, pick the TacticalAPI tab, and the four methods of the Situation service (SubscribeSituationObjectEvents, GetSituationObjects, AddOrUpdateSituationObjects, DeleteSituationObjects) appear in the sidebar. Click Execute on any unary method and the call goes out to the server; pick the streaming method and the workbench's Wireshark-style frame pane starts filling with SituationObject events as they're emitted.

URL forms

The plugin normalises the URL Bowire passes through:

You type What the plugin connects to
tacticalapi@host:4267 https://host:4267 (default — TacticalAPI in the field is mTLS)
grpc://host:50051 http://host:50051 (plaintext gRPC)
grpcs://host:50051 https://host:50051
host:4267 https://host:4267

mTLS / TLS settings via metadata

TacticalAPI servers in production almost always run behind mTLS. The plugin reads two configuration keys from the workbench metadata bag — these never reach the wire (they're filtered out before the gRPC Metadata is built):

Metadata key Purpose
_bowire:tls-skip-validation=true Accept any server certificate. For staging / self-signed only — same opt-in semantics as the core gRPC plugin's --allow-self-signed-certs.
_bowire:client-cert-pfx=<path> Path to a PFX file with the client certificate + private key.
_bowire:client-cert-password=<pw> Password for the PFX (omit for unprotected PFX).

Set both via the workbench's metadata panel before clicking Execute, or via --metadata _bowire:client-cert-pfx=... from the CLI.

Pairing with the gRPC plugin's gRPC-Web transport

TacticalAPI servers commonly expose two ports: a native HTTP/2 gRPC endpoint (typically :4267) and a gRPC-Web endpoint (typically :4268) for browser-fronted clients. Bowire's gRPC plugin speaks both — point at the native port for full bidirectional access, or at the gRPC-Web port behind an L7 proxy. Discovery works against either transport because the proto schema is bundled, not fetched at connect time.

Licensing — please read

The plugin code, generated bindings package, and documentation are Apache-2.0. The upstream TacticalAPI .proto files at https://github.com/Rheinmetall/tacticalapi are EPL-2.0 OR BSD-3-Clause (Rheinmetall Electronics GmbH). Vendoring those files into an Apache-2.0 repository would be redistribution under a different license, which EPL-2.0 does not permit. To honour both:

  • The .proto files are downloaded at build time from the upstream repository (pinned commit e68546809d981cd649325dba4a9702c1a77a1a0b) into obj/tacticalapi-protos/ — gitignored, never committed.
  • Grpc.Tools compiles them into the assembly; only the generated C# bindings ship in the NuGet package.
  • The .proto source itself never enters the plugin's source tree or its published package.

The pin will move to a released tag once Rheinmetall cuts one. A scheduled GitHub Action in the sibling repo (.github/workflows/check-upstream-protos.yml) runs weekly, compares the pinned commit against Rheinmetall/tacticalapi@main, and opens a tracking issue when upstream drifts — so the bump cadence stays visible without manual polling.

Build requirements

Building the plugin requires outbound internet access to raw.githubusercontent.com so the proto-fetch target can reach the upstream repo. GitHub Actions runners have this by default; air-gapped CI does not.

Air-gapped builds

Pre-populate the proto cache before invoking dotnet build:

<repo-root>/artifacts/obj/Kuestenlogik.Bowire.Protocol.TacticalApi/<Configuration>/tacticalapi-protos/rheinmetall/tactical_api/v0/

Drop the six upstream .proto files (same filenames as on Rheinmetall/tacticalapi) into that directory and the build's DownloadFile target short-circuits because the files already exist. Consumers of the published NuGet package don't need network access — only contributors and CI building from source do.

Try it — upstream test client as data populator

Rheinmetall ships an official C# test client alongside the proto set. It's a small CLI that exercises every operation in Situation--observesituation (server-streaming), --printsituation (unary polling), --sendsymbol (create), --changesymbolname (update), --deletesymbol (delete). For Bowire users the test client is the fastest way to populate a server with realistic data so the workbench has something interesting to render.

End-to-end demo, against either a live TacticalAPI server or the upstream TacNet instance:

# 1. Build the upstream test client (one-time)
git clone https://github.com/Rheinmetall/tacticalapi
cd tacticalapi/testclient/csharp
dotnet build TacticalApi.TestClient.csproj

# 2. Place a handful of symbols at WGS84 coordinates near Hamburg
#    (the test client takes lat / lon as positional args)
dotnet TacticalApi.TestClient.dll --sendsymbol 53.5 9.9
dotnet TacticalApi.TestClient.dll --sendsymbol 53.55 10.0
dotnet TacticalApi.TestClient.dll --sendsymbol 53.6 10.05

# 3. Add the optional map-widget extension so SituationObjectLocation
#    pins land on a MapLibre canvas next to the streaming-frames pane.
#    (Skip this step if you only want the raw JSON view — Bowire still
#    auto-detects the lat/lon fields, it just falls back to a
#    "Install Kuestenlogik.Bowire.Map" placeholder card instead of a map.)
dotnet add package Kuestenlogik.Bowire.Map

# 4. Point Bowire at the same server — native HTTP/2 transport
bowire --url grpc@https://localhost:4267
#    …or gRPC-Web over HTTP/1.1 if the server exposes :4268 too
bowire --url grpcweb@https://localhost:4268

# 5. In the workbench, pick Situation → SubscribeSituationObjectEvents.
#    Click Execute. With the Frame-Semantics Framework live in
#    Bowire 1.3.0+, the workbench auto-detects the lat/lon fields on
#    every SituationObjectLocation and mounts a Map tab next to the
#    streaming-frames pane — every symbol the test client created
#    appears as a pin, every new --sendsymbol from a parallel
#    terminal lights up live.

Why this demo carries weight: no Bowire-side configuration was involved. No bowire.schema-hints.json, no IBowireSchemaHints implementation, no manual right-click on a field. The TacticalAPI plugin ships transport-only; the framework recognises coordinate.latitude / coordinate.longitude from the field names + WGS84 ranges and routes the data into the map widget on its own. The pgAdmin pattern: shape-of-data drives viewer choice, not protocol-author opt-in. The map widget itself rides its own NuGet package (Kuestenlogik.Bowire.Map) so Bowire core stays ~870 KB lighter for users who never need geographic rendering — same plugin model as the protocol packages.

A companion walkthrough in the mock-server docs uses the same test client to validate bowire mock — a useful inverse comparison if you ever want to verify Bowire reproduces a real server's behaviour faithfully.

Roadmap

  • v1.0.0 (shipped 2026-05-26) — bundled-schema discovery, typed unary CRUD (GetSituationObjects / AddOrUpdateSituationObjects / DeleteSituationObjects), the server-streaming pump for SubscribeSituationObjectEvents, URL-scheme normalisation (tacticalapi@, grpc(s)://, bare host:port), mTLS via the shared __bowireMtls__ marker (legacy _bowire:client-cert-pfx keys still honoured for back-compat), IBowireMockEmitter for recording replay, in-process Kestrel-hosted integration suite. The current stable line.
  • post-1.0 — MIL-STD-2525 / APP-6 symbol renderer (the schema's SymbolIdentifier field is already wired through; the map widget side needs a milsymbol.js-style renderer to turn the SIDC into the correct tactical-affiliation glyph). Service-Bus / Artemis-flavoured AMQP 1.0 discovery (parallel item on the AMQP plugin's side) could similarly land as a vendor-specific follow-on.

Sample

A canonical mini-server lives at Bowire.Samples/protocols/TacticalApi.RadarSweep — three MIL-2525C contacts orbit a radar centre at 54.00°N / 11.50°E on HTTP/2 port 5191. dotnet run brings it up; point Bowire at http://localhost:5191 and pick the Situation service to exercise GetSituationObjects (unary) + SubscribeSituationObjectEvents (server-streaming). For a full Harbor Control Center scene with the AddOrUpdate / Delete RPCs and gRPC-Web on a second port, see the harbor-demo sibling Bowire.Samples/harbor-demo/src/Kuestenlogik.Bowire.Samples.TacticalApi.

Acknowledgements

The TacticalAPI specification, including every .proto file this plugin compiles against, is the work of Rheinmetall Electronics GmbH. Used in accordance with the upstream EPL-2.0 / BSD-3-Clause licensing.