> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nelieo.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build a .NET Application Agent Using the CLR Probe

> Build a Python agent for .NET apps with NSP — CLR probe auto-attach, reading WinForms/WPF state, and invoking .NET methods via CLRSessionProxy.

NSP's CLR probe attaches to any running .NET process — WinForms apps, WPF apps, .NET 4.x / .NET 6+ binaries — using the Windows CLR Hosting API. No code changes to the target application are required. This guide uses Mission Planner (a C# ground control station) as the worked example, but the same patterns apply to any .NET app.

## How the CLR Probe Works

When the NSP daemon detects a .NET process, it injects a managed C# bridge assembly into the target AppDomain via the CLR Hosting API. The bridge runs inside the target process and communicates back to the daemon through a Memory-Mapped File (MMF). This design means:

* **No process restart required** — injection happens while the app is running.
* **Zero-copy communication** — the MMF is shared memory; no serialization overhead.
* **Non-intrusive field reads** — field values are read through managed reflection without pausing the JIT.

## Prerequisites

<CardGroup cols={2}>
  <Card title=".NET App Running" icon="windows">
    The target .NET application must already be running. The daemon detects it automatically within one scan cycle (default: 5 seconds).
  </Card>

  <Card title="NSP Daemon Running" icon="server">
    Run `axon-daemon.exe run` or start the Windows Service. Ensure the daemon has permission to inject into the target process (run both as the same user, or as Administrator).
  </Card>

  <Card title="SDK Installed" icon="python">
    `pip install nelieo-nsp`
  </Card>

  <Card title="No App Changes" icon="check">
    You do not need to modify, rebuild, or restart the target .NET application.
  </Card>
</CardGroup>

## Step 1 — Verify Detection

List tracked apps and check for your .NET process. The `runtime` field will be `clr` for any managed .NET target:

```python theme={null}
import asyncio
import nelieo_nsp as nsp

async def main():
    async with nsp.NSPClient(api_key="sk_nsp_...") as client:
        apps = await client.list_apps()
        for app in apps:
            print(f"{app.app_name} | runtime={app.runtime} | conf={app.confidence:.2f}")

asyncio.run(main())
# Mission Planner | runtime=clr | conf=0.95
```

<Info>
  CLR confidence baselines typically start at `0.70–0.80` during the first probe while the bridge enumerates assembly types. Confidence reaches `0.95+` once the semantic engine has mapped the major state fields. This usually takes 15–45 seconds after the process is first detected.
</Info>

## Step 2 — Attach and Read Normalized State

The CLR probe normalizes extracted fields into the standard NSP state tree (SSF format). Use the same typed accessors you would with any other app:

```python theme={null}
async with nsp.NSPClient(api_key="sk_nsp_...") as client:
    session = await client.attach("Mission Planner")

    # Normalized state — available via standard accessors
    alt     = session.get_float("flight.altitude_m")
    lat     = session.get_float("flight.gps.latitude")
    lon     = session.get_float("flight.gps.longitude")
    mode    = session.get_str("flight.mode")
    armed   = session.get_bool("flight.is_armed")
    battery = session.get_float("flight.battery_voltage")

    print(f"Position: {lat:.6f}, {lon:.6f}  Alt: {alt:.1f}m")
    print(f"Mode: {mode}  Armed: {armed}  Battery: {battery:.1f}V")
```

## Step 3 — Read .NET Fields Directly via `session.clr`

When normalized state doesn't cover what you need, use `session.clr` — a `CLRSessionProxy` that gives you direct access to the CLR heap:

<Tabs>
  <Tab title="Read a single field">
    ```python theme={null}
    # Read a static or instance field from a specific .NET type
    armed      = await session.clr.read_field("MAVLink.MAVLinkInterface", "IsArmed")
    gps_status = await session.clr.read_field("MAVLink.MAVLinkInterface", "GPSStatus")
    heading    = await session.clr.read_field("MissionPlanner.FlightData", "heading")

    print(f"Armed: {armed}  GPS: {gps_status}  Heading: {heading}°")
    ```
  </Tab>

  <Tab title="Read multiple fields at once">
    ```python theme={null}
    # Batch read — one MMF round-trip for all fields
    telemetry = await session.clr.read_fields(
        "MissionPlanner.FlightData",
        "alt", "groundspeed", "airspeed", "heading",
        "battery_voltage", "battery_remaining",
    )
    print(f"Alt={telemetry['alt']:.1f}m  GS={telemetry['groundspeed']:.1f}m/s")
    print(f"Batt={telemetry['battery_remaining']:.0f}%")
    ```
  </Tab>

  <Tab title="Heap snapshot">
    ```python theme={null}
    # Walk the full CLR heap for a type namespace
    objects = await session.clr.heap_snapshot(
        include_prefix="MAVLink",  # Only types whose name starts with "MAVLink"
        max_instances=32,
        timeout_ms=30_000,
    )
    for obj in objects:
        print(obj["type_name"], obj["fields"])
        # MAVLink.MAVLinkInterface  {'IsArmed': True, 'alt': 42.7, ...}
    ```
  </Tab>
</Tabs>

## Step 4 — Invoke .NET Methods via `session.clr`

Call any public .NET method with typed arguments. Python values are automatically coerced to the matching .NET parameter types:

```python theme={null}
# Parameterless method call
status = await session.clr.invoke("MissionPlanner.MainV2", "GetStatusText")
print(f"GCS Status: {status}")

# String argument — coerced to System.String
await session.clr.invoke("MissionPlanner.GCS", "setMode", ["GUIDED"])

# Boolean argument — coerced to System.Boolean
await session.clr.invoke("MissionPlanner.MainV2", "doArm", [True])

# Double arguments — coerced to System.Double
await session.clr.invoke(
    "MissionPlanner.NavController",
    "SetWaypoint",
    [34.0522, -118.2437, 50.0],  # lat, lon, alt_m
)
```

<Tip>
  Python `bool`, `int`, `float`, `str`, and `list` all coerce automatically to their .NET equivalents. Strings in ISO 8601 format coerce to `DateTime`; GUID-formatted strings coerce to `Guid`. Enums can be passed by name as a string.
</Tip>

## Step 5 — Execute NSP Actions

The CLR probe also exposes higher-level NSP actions discovered from the app's schema. Use `session.execute()` for these:

```python theme={null}
# List what actions are available
for name, schema in session.actions.items():
    print(f"{name}: reversibility={schema.reversibility}")

# Arm motors — reversible, no verify required
result = await session.execute("arm_motors", parameters={})
print(f"Arm command sent: {result.success}")

# Take off — irreversible; verify altitude reaches target
result = await session.execute(
    "take_off",
    parameters={"altitude_m": 50.0},
    verify=True,
    verify_expression="flight.altitude_m > 45.0",
    verify_timeout_ms=30000,  # 30s for takeoff to settle
)
print(f"Airborne: {result.success}")
```

## Step 6 — Stream Telemetry in Real Time

```python theme={null}
async def monitor_flight():
    async with nsp.NSPClient(api_key="sk_nsp_...") as client:
        session = await client.attach("Mission Planner")
        print(f"Connected. Confidence: {session.confidence:.2f}")

        async with session.watch(auto_refresh_on_event=True) as stream:
            async for event in stream:
                if not any(k.startswith("flight.") for k in event.changed_keys):
                    continue

                alt   = session.get_float("flight.altitude_m")
                mode  = session.get_str("flight.mode")
                armed = session.get_bool("flight.is_armed")
                gs    = session.get_float("flight.groundspeed")
                batt  = session.get_float("flight.battery_remaining")

                print(f"  Alt={alt:.1f}m  GS={gs:.1f}m/s  Mode={mode}  Armed={armed}")

                # Safety: return to launch if battery is low
                if batt < 20.0 and armed:
                    print("⚠️  Battery low — commanding RTL")
                    await session.execute(
                        "set_mode",
                        parameters={"mode": "RTL"},
                        verify=True,
                        verify_expression="flight.mode == 'RTL'",
                    )

asyncio.run(monitor_flight())
```

## Complete Example — Autonomous Waypoint Mission

```python theme={null}
import asyncio
import nelieo_nsp as nsp

WAYPOINTS = [
    (34.0522, -118.2437, 50.0),  # Start
    (34.0600, -118.2400, 60.0),  # WP1
    (34.0650, -118.2350, 60.0),  # WP2
    (34.0522, -118.2437, 50.0),  # Return home
]


async def run_mission():
    async with nsp.NSPClient(api_key="sk_nsp_...") as client:
        session = await client.attach("Mission Planner")
        print(f"Connected. Confidence: {session.confidence:.2f}")

        # 1. Arm the vehicle
        await session.clr.invoke("MissionPlanner.MainV2", "doArm", [True])
        await session.wait_for(
            lambda s: s.get_bool("flight.is_armed"),
            timeout=15.0,
        )
        print("Armed ✓")

        # 2. Switch to GUIDED mode
        await session.clr.invoke("MissionPlanner.GCS", "setMode", ["GUIDED"])

        # 3. Take off to 50 m
        await session.execute(
            "take_off",
            parameters={"altitude_m": 50.0},
            verify=True,
            verify_expression="flight.altitude_m > 45.0",
            verify_timeout_ms=30000,
        )
        print("Airborne ✓")

        # 4. Fly each waypoint in sequence
        for i, (lat, lon, alt) in enumerate(WAYPOINTS):
            print(f"→ Waypoint {i + 1}: ({lat:.4f}, {lon:.4f}, {alt}m)")
            await session.clr.invoke(
                "MissionPlanner.NavController",
                "SetWaypoint",
                [lat, lon, alt],
            )
            # Wait until altitude is within 5 m of target
            await session.wait_for(
                lambda s, a=alt: abs(s.get_float("flight.altitude_m") - a) < 5.0,
                timeout=60.0,
                fresh_probe=True,
            )

        # 5. Land
        print("Mission complete — landing")
        await session.clr.invoke("MissionPlanner.GCS", "setMode", ["LAND"])


asyncio.run(run_mission())
```

## CLR Proxy Method Reference

<AccordionGroup>
  <Accordion title="session.clr.read_field(type_name, field_name)">
    Reads a single field from the first matching instance of `type_name` on the CLR heap.

    ```python theme={null}
    value = await session.clr.read_field("MAVLink.MAVLinkInterface", "IsArmed")
    # Returns: bool | int | float | str | None
    ```
  </Accordion>

  <Accordion title="session.clr.read_fields(type_name, *field_names)">
    Reads multiple fields in a single MMF round-trip. Returns a `dict[str, Any]`.

    ```python theme={null}
    vals = await session.clr.read_fields(
        "MissionPlanner.FlightData",
        "alt", "groundspeed", "battery_voltage",
    )
    ```
  </Accordion>

  <Accordion title="session.clr.invoke(type_name, method_name, args=[])">
    Invokes a method on the first matching instance. Returns the method's return value, coerced back to a Python type.

    ```python theme={null}
    result = await session.clr.invoke("MissionPlanner.GCS", "setMode", ["AUTO"])
    ```
  </Accordion>

  <Accordion title="session.clr.heap_snapshot(include_prefix, max_instances, timeout_ms)">
    Walks the CLR heap and returns a list of objects whose type names start with `include_prefix`. Each object contains `type_name` and `fields` keys. This is an expensive operation — use sparingly.

    ```python theme={null}
    objects = await session.clr.heap_snapshot(include_prefix="MAVLink", max_instances=32)
    ```
  </Accordion>
</AccordionGroup>
