> ## 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 Java Application Agent Using the JVM Probe

> Step-by-step guide to building a Python agent for JVM applications — JVMTI auto-attach, reading Java state, and invoking methods using JVMSessionProxy.

NSP's JVM probe uses the standard JVMTI (JVM Tool Interface) — the same mechanism used by production profilers such as YourKit and JProfiler — to attach to running Java processes without restarting them. Once attached, you can read live field values and invoke methods from Python. This guide uses ArduPilot GCS (a Java-based ground control station) as the primary example, but the same patterns work for any supported JVM application.

## How the JVMTI Probe Works

When the NSP daemon detects a JVM process, it uses the standard JVM Attach API to load a JVMTI agent into the running JVM. From that point on, the agent runs on a JVM thread — the target JVM is never paused. Field reads use JNI reflection (`GetFieldID` → `GetObjectField`) and method calls use `CallObjectMethod`. Results flow back to the daemon over a local socket.

<Info>
  The first time the JVMTI probe attaches to a JVM process it must enumerate all loaded classes, which takes **30–90 seconds** (the "cold probe"). Subsequent daemon restarts reuse the persisted schema registry and attach in under 5 seconds.
</Info>

## Prerequisites

<CardGroup cols={2}>
  <Card title="Java App Running" icon="java">
    The target JVM application must already be running. The daemon scans for new processes every 5 seconds.
  </Card>

  <Card title="NSP Daemon Running" icon="server">
    Run `axon-daemon.exe run` or start the Windows Service. The daemon and target JVM must run as the same OS user for the attach to succeed.
  </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, recompile, or restart the Java application.
  </Card>
</CardGroup>

## Supported Applications

The JVMTI probe works with any JVM process, but NSP ships built-in semantic mappings for these applications:

| Application      | Package Prefix      | Notes                          |
| ---------------- | ------------------- | ------------------------------ |
| ArduPilot GCS    | `com.ardupilot`     | Full MAVLink telemetry state   |
| Eclipse IDE      | `org.eclipse`       | Project and editor state       |
| IntelliJ IDEA    | `com.intellij`      | Project / open file state      |
| Jenkins          | `hudson`, `jenkins` | Build pipeline and job state   |
| Apache Kafka     | `org.apache.kafka`  | Topic and consumer group state |
| Elasticsearch    | `org.elasticsearch` | Index and cluster health state |
| Spring Boot apps | `com.yourapp`       | Via field/method introspection |

For apps not on this list, NSP still attaches and extracts state — you'll just use `session.jvm.read_field()` and `session.jvm.invoke()` directly rather than relying on pre-built semantic mappings.

## Step 1 — Verify Detection

```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())
# ArduPilot GCS | runtime=jvm | conf=0.95
```

If you see `conf=0.40` and the runtime is `jvm`, the cold probe is still in progress. Wait 30–90 seconds and list again.

## Step 2 — Connect and Read Normalized State

Use `wait_for_app` to handle the cold probe window gracefully:

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

async def main():
    async with nsp.NSPClient(api_key="sk_nsp_...") as client:
        session = await client.wait_for_app("ArduPilot GCS", timeout=120.0)
        print(f"Connected. Confidence: {session.confidence:.2f}")

        # Normalized SSF state — standard typed accessors
        alt   = session.get_float("flight.altitude_m")
        mode  = session.get_str("flight.mode")
        armed = session.get_bool("flight.is_armed")
        sats  = session.get_int("flight.gps.satellites")
        lat   = session.get_float("flight.gps.latitude")
        lon   = session.get_float("flight.gps.longitude")

        print(f"Position: ({lat:.6f}, {lon:.6f})  Alt: {alt:.1f}m")
        print(f"Mode: {mode}  Armed: {armed}  Satellites: {sats}")

asyncio.run(main())
```

## Step 3 — Read Java Fields Directly via `session.jvm`

`session.jvm` is a `JVMSessionProxy` that lets you bypass the normalized state and read fields or call methods directly on live Java objects:

<Tabs>
  <Tab title="Read a field">
    ```python theme={null}
    # Read a field from a specific class
    heading = await session.jvm.read_field("com.ardupilot.GCS", "heading")
    gs      = await session.jvm.read_field("com.ardupilot.GCS", "groundspeed")
    mode    = await session.jvm.read_field("com.ardupilot.GCS", "flightMode")

    print(f"Heading: {heading}°  GS: {gs:.1f}m/s  Mode: {mode}")
    ```
  </Tab>

  <Tab title="Invoke a method">
    ```python theme={null}
    # Parameterless method
    status = await session.jvm.invoke("com.ardupilot.GCS", "getStatusText")
    print(f"Status: {status}")  # "OK: 3D GPS Fix"

    # Method with arguments — Python types auto-coerced to JVM types
    await session.jvm.invoke("com.ardupilot.GCS", "setMode", ["GUIDED"])

    # Method with multiple doubles
    await session.jvm.invoke(
        "com.ardupilot.WaypointManager",
        "addWaypoint",
        [34.0522, -118.2437, 50.0],  # lat (double), lon (double), alt (double)
    )
    ```
  </Tab>
</Tabs>

## Step 4 — Set a Flight Mode

```python theme={null}
# Set mode via JVM invoke
await session.jvm.invoke("com.ardupilot.GCS", "setMode", ["GUIDED"])

# Wait for the state to reflect the change
await session.wait_for(
    lambda s: s.get_str("flight.mode") == "GUIDED",
    timeout=10.0,
)
print("Mode set to GUIDED ✓")
```

## Step 5 — Manage Waypoints

```python theme={null}
# Add a waypoint (lat, lon, alt_m)
await session.jvm.invoke(
    "com.ardupilot.WaypointManager",
    "addWaypoint",
    [34.0522, -118.2437, 50.0],
)

# Confirm the waypoint was registered
count = await session.jvm.invoke(
    "com.ardupilot.WaypointManager",
    "getWaypointCount",
)
print(f"Total waypoints: {count}")
```

## Step 6 — Arm and Take Off

```python theme={null}
# Arm via NSP action — reversible, no verify required
result = await session.execute(
    "arm_motors",
    parameters={},
    verify=True,
    verify_expression="flight.is_armed == true",
    verify_timeout_ms=15000,
)
print(f"Armed: {result.success}")

# Take off to 50 m — irreversible; always use verify
result = await session.execute(
    "take_off",
    parameters={"altitude_m": 50.0},
    verify=True,
    verify_expression="flight.altitude_m > 45.0",
    verify_timeout_ms=30000,
)
print(f"Airborne: {result.success}")
```

## Step 7 — Stream Telemetry

```python theme={null}
async def stream_telemetry():
    async with nsp.NSPClient(api_key="sk_nsp_...") as client:
        session = await client.wait_for_app("ArduPilot GCS", timeout=120.0)

        print("Streaming telemetry (Ctrl+C to stop)...")
        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")
                batt = session.get_float("flight.battery_remaining")
                lat  = session.get_float("flight.gps.latitude")
                lon  = session.get_float("flight.gps.longitude")

                print(
                    f"[{event.captured_at}] "
                    f"Pos=({lat:.4f},{lon:.4f})  "
                    f"Alt={alt:.1f}m  Mode={mode}  Batt={batt:.0f}%"
                )

                # Safety: command RTL on critical battery
                if batt < 15.0:
                    print("⚠️  Critical battery — commanding RTL")
                    await session.jvm.invoke("com.ardupilot.GCS", "setMode", ["RTL"])

asyncio.run(stream_telemetry())
```

## JVM Proxy Method Reference

<AccordionGroup>
  <Accordion title="session.jvm.read_field(class_name, field_name)">
    Reads a field value from the first matching instance of `class_name` in the JVM heap. Uses JNI `GetFieldID` → `GetObjectField` internally.

    ```python theme={null}
    value = await session.jvm.read_field("com.ardupilot.GCS", "altitude")
    # Returns: bool | int | float | str | None
    ```
  </Accordion>

  <Accordion title="session.jvm.invoke(class_name, method_name, args=[])">
    Invokes a method on the first matching instance of `class_name`. Arguments are auto-coerced from Python to JVM types. Returns the method's return value coerced back to Python.

    ```python theme={null}
    result = await session.jvm.invoke("com.ardupilot.GCS", "getStatusText")
    # Returns: str | int | float | bool | None
    ```
  </Accordion>
</AccordionGroup>

## JVM Error Reference

| Error Code             | Meaning                                         | Fix                                               |
| ---------------------- | ----------------------------------------------- | ------------------------------------------------- |
| `jvm_not_attached`     | JVMTI agent not loaded yet                      | Wait for cold probe to complete (up to 90s)       |
| `jvm_class_not_found`  | Class is not loaded in this JVM                 | Verify the fully-qualified class name             |
| `jvm_method_not_found` | Method name or signature mismatch               | Check the exact method name, including overloads  |
| `jvm_invoke_timeout`   | Method didn't return within `timeout_ms`        | Increase timeout or check for blocked JVM threads |
| `jvm_type_error`       | Python argument can't coerce to target JVM type | Check the table below                             |

### Python → JVM Type Coercion

| Python Type | JVM Type                       |
| ----------- | ------------------------------ |
| `bool`      | `boolean` / `Boolean`          |
| `int`       | `int`, `long`, `short`, `byte` |
| `float`     | `double`, `float`              |
| `str`       | `String`                       |
| `list`      | `List<T>`, `T[]`               |
| `dict`      | `Map<String, Object>`          |
| `None`      | `null`                         |
