Skip to main content

agrowell-ikh-client

A small, typed Python SDK that bridges the I Know How (IKH) robot with the AGRO-WELL greenhouse platform.

v1 scope:

  1. Authenticate to the platform via Keycloak (client-credentials / machine-to-machine).
  2. Read anchors — resolve a detected AprilTag to its platform anchor (and its AR pose).
  3. Commission (align) — from a reference anchor, compute the anchor-group transform that maps the robot's ROS measurements onto the platform AR scene, write it, and emit a quantitative validation report. The ROS ↔ Three.js conversion happens internally.

Status: v1 boilerplate. Some platform-side prerequisites must be confirmed before live integration — see Open items. Open-source under the BSD 3-Clause License.

Installation

Released under the BSD 3-Clause License. Install from PyPI:

pip install agrowell-ikh-client

Or from a built wheel:

pip install ./agrowell_ikh_client-<version>-py3-none-any.whl

Requires Python 3.10+.

Configuration

Read from AGROWELL_-prefixed environment variables (and an optional .env file), or passed explicitly via Settings. Copy .env.example to .env and fill in the values from the AGRO-WELL platform team.

Variable Required Default Description
AGROWELL_KEYCLOAK_BASE_PATH ✅ — Keycloak base URL
AGROWELL_KEYCLOAK_REALM AGRO-WELL Keycloak realm
AGROWELL_KEYCLOAK_CLIENT_ID ✅ — Service-account client id
AGROWELL_KEYCLOAK_CLIENT_SECRET ✅ — Service-account client secret
AGROWELL_API_BASE_URL ✅ — object-placement REST base URL
AGROWELL_API_PATH_PREFIX /v1 API path prefix (/v1 or /api/v1)
AGROWELL_ORGANIZATION_ID ✅ — Organization the robot belongs to (scopes all anchor reads/writes)
AGROWELL_FACILITY_ID ✅ — Facility the robot is installed at (== platform scene id; scopes every read to that facility)
AGROWELL_DEV_MODE false Collect a validation report during commissioning and emit it on close()
AGROWELL_VERIFY_SSL true TLS certificate verification
AGROWELL_HTTP_TIMEOUT_SECONDS 10.0 Request timeout
AGROWELL_HTTP_MAX_RETRIES 3 Retry budget for idempotent requests

Quickstart

from agrowell_ikh_client import AgroWellClient, ScannedTag

with AgroWellClient.from_env() as client:
    # Discover the anchors visible to your organization (raw platform models, AR frame):
    anchors = client.api.anchors.list()

    # Commission: on each AprilTag detection, pass the robot's measured pose (ROS frame).
    # The SDK resolves the anchor, computes the anchor-group transform, and writes it.
    for detection in detections:                  # your detector's per-frame loop
        client.alignment.update_group(
            ScannedTag(
                apriltag_id=detection.id,
                translation=(1.20, 0.0, 3.45),    # metres, ROS frame
                quaternion=(0.0, 0.0, 0.0, 1.0),  # (x, y, z, w)
            )
        )

The robot needs only its organization (from config) and the AprilTag ids it scans. It never handles anchor UUIDs, anchors-groups, scenes, or raw 4×4 matrices — those are resolved or converted internally. Poses cross the SDK boundary in the ROS frame; the raw reads under client.api.* return platform models in the AR (Three.js) frame.

The rigid-isometry property (why the between-anchor check works)

The internal ROS → Three.js conversion is a rigid isometry — a pure rotation of axes (det = +1, no scale or handedness flip), so distances and angles between anchors are preserved. The commissioning report exploits this: in the relative transform between two anchors the anchor-group transform G cancels, so a residual there can only come from the ROS ↔ AR conversion itself (a handedness/axis flip or a metre/centimetre mix-up), not from a single mis-placed anchor. That is what makes the between-anchor check a clean, objective signal even though there is no visual validation. (See tests/test_simulation.py.)

Commissioning (alignment + validation)

After the 3 anchors are AR-calibrated in the web app, the robot aligns its ROS frame to the scene by writing the anchor-group transform. On each AprilTag detection it calls update_group, which resolves the anchor's AR pose, computes the group transform G from that AR pose and the detected ROS pose (G = T_ar ∘ inverse(convert(T_ros))), and PATCHes the group. This runs many times during a commissioning session.

Because updating that transform moves the whole scene subtree together, there is no visual validation. So, with AGROWELL_DEV_MODE=true, the client collects each anchor's server ("before") and ROS-computed ("after") pose and emits one validation report on close().

from agrowell_ikh_client import AgroWellClient, ScannedTag, ObjectStoreSink

# AGROWELL_DEV_MODE=true enables the report; the sink is where it is uploaded.
# Sinks: ObjectStoreSink (presigned URL), MinioSink.from_settings(settings) (direct MinIO/S3,
# needs the 'minio' extra), or LocalFileSink (disk).
with AgroWellClient.builder().with_report_sink(
    ObjectStoreSink(presign=mint_upload_url)  # mint_upload_url(key) -> presigned PUT URL
).build() as client:
    for tag in detections:                     # the robot's per-detection loop
        client.alignment.update_group(
            ScannedTag(tag.id, tag.translation, tag.quaternion)  # ROS frame
        )
    # on exit: one report (per-anchor errors + between-anchor conversion check) is uploaded

The report (ValidationReport, render with report.to_text()) carries per-anchor position/orientation errors and aggregates, plus a pairwise (between-anchor) check that the ROS → AR conversion holds — in the relative transform between two anchors G cancels, so it isolates conversion errors (handedness/axis flips, metre/centimetre mix-ups) from a single mis-placed anchor. Try it offline: python examples/simulate_commissioning.py.

Usage

# Raw request layer, grouped by resource category (client.api.<category>).

# List the anchors visible to your organization (raw platform models, AR frame).
# No group/scene needed; the org context scopes the result.
anchors = client.api.anchors.list()                  # or .list(registered=False)

# Resolve a single detected tag to its anchor. Raises AnchorNotFoundError if none match,
# AmbiguousAnchorError if more than one does:
anchor = client.api.anchors.resolve_by_tag(42)
if anchor.image_anchor and anchor.image_anchor.transform:
    x, y, z = anchor.image_anchor.transform.translation   # AR (Three.js) frame

# Commission: per detection, compute + write the anchor-group transform from a ROS pose.
client.alignment.update_group(
    ScannedTag(42, translation=(1.2, 0.0, 3.45), quaternion=(0.0, 0.0, 0.0, 1.0))
)

Coordinate frames (internal)

The robot speaks only ROS (REP-103: X-forward, Y-left, Z-up, right-handed, metres, quaternions). The platform stores transforms in the Three.js AR frame (right-handed, Y-up). The SDK converts at the boundary automatically (ROS_X = −AR_Z, ROS_Y = −AR_X, ROS_Z = AR_Y); it is not a user-facing option. If the platform's AR engine ever changes, that is a one-line internal change.

Error handling

All errors derive from AgroWellError. HTTP errors map to typed exceptions (BadRequestError, ForbiddenError, NotFoundError, ConflictError, ServerError, …), each carrying status_code, response_body, and request_url. AnchorNotFoundError is raised when no anchor matches a tag, and AmbiguousAnchorError when more than one does.

Advanced: dependency injection

client = (
    AgroWellClient.builder()
    .with_settings(settings)
    .add_request_hook(my_tracing_hook)
    .with_token_store(my_token_store)
    .build()
)

Architecture

AgroWellClient (facade)
  ├─ api/        ObjectPlacementApi → .anchors, .anchor_groups   (raw request layer, by category)
  └─ alignment   commissioning workflow (composes api: read anchor → compute G → write group)
        │
        └─>  transport/ (httpx)  ──>  auth/ + models/
  • api/ is the single reusable request layer, grouped by resource category (client.api.anchors, client.api.anchor_groups); domain resources like resources/alignment compose it rather than issuing requests directly, so each endpoint lives in exactly one place and a new category is one module plus one line.
  • Transport, AuthStrategy, TokenStore are typing.Protocols (dependency inversion): implementations swap and fake in tests without touching call sites.
  • Sync today, async-ready: the synchronous HttpxTransport sits behind the Transport seam, so an async transport can be added additively later.
  • Logging uses a NullHandler; nothing is emitted unless your application configures it.
  • Pure math (geometry.py, validation.py) has no I/O; the validation report uploads via a pluggable ReportSink (reporting/sinks.py).

Development

make install      # uv sync --extra dev   (or: python3.11 -m venv .venv && .venv/bin/pip install -e ".[dev]")
make check        # ruff + mypy (strict) + pytest
pytest -m integration   # live smoke test (requires AGROWELL_* env)

Tests use respx to mock httpx and a FakeTransport (the Transport Protocol) for pure unit tests — no network and no ROS install required.

Open items

The full read + commissioning-write flow has been verified live against the homelab deployment; the notes below are operational requirements, not blockers.

  1. Organization + facility are mandatory: every read/write is scoped by Grpc-Metadata-organization (AGROWELL_ORGANIZATION_ID) and sceneId (AGROWELL_FACILITY_ID, the platform scene == facility). For the robot's machine token the header is the org scope; only user tokens additionally need a matching organization claim.
  2. Group WRITE works with the machine token (resolved & verified live — core-object-placement v1.4.7 / u2m-go-utils v1.2.0): reads and UpdateAnchorsGroup accept the robot's client-credentials token. Caveat: machine clients must not carry an email attribute in Keycloak, or they are classified as user tokens and hit the strict checks.
  3. Report upload: with AGROWELL_DEV_MODE=true and AGROWELL_MINIO_* set, the client auto-wires MinioSink and uploads the PDF on close() — AgroWellClient.from_env() is all the robot needs. ObjectStoreSink is the credential-free alternative (PUT to a backend-minted presigned URL) for callers that prefer to inject their own sink.
  4. Conventions confirmed live: the PATCH targets localization_provider_id (== anchor.anchors_group_id); the platform may serialize a TransformMatrix in either row_first or column_first layout, which the SDK handles transparently.
  5. Anchors must pre-exist and be AR-calibrated (with apriltag_ids) for the organization.
  6. API prefix is /v1 (AGROWELL_API_PATH_PREFIX).
  7. Keycloak client: a service-account client in realm AGRO-WELL (+ secret).
  8. Distribution: released under the BSD 3-Clause License and publishable to public PyPI. Scrub the repo (and git history) for secrets / internal hosts before any public release — see .env (gitignored) and examples/.

License

BSD 3-Clause License. Copyright (c) 2026 up2metric P.C. See LICENSE. Redistribution and use in source and binary forms, with or without modification, are permitted provided the copyright notice and the conditions in LICENSE are retained. For inquiries, contact info@up2metric.com.

Acknowledgement

This project has received funding from the European Union's Horizon Europe research and innovation programme under Grant Agreement No. 101182923 (AGRO-WELL).

Developed and maintained by up2metric P.C., Athens, Greece.

Metadata

Release files for agrowell-ikh-client 0.1.6

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for agrowell-ikh-client 0.1.6
File Size Uploaded Provenance
agrowell_ikh_client-0.1.6.tar.gz 51.3 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for agrowell-ikh-client 0.1.6
File Interpreter ABI Platform
agrowell_ikh_client-0.1.6-py3-none-any.whl Python 3 none any Details

Total release size: 117.5 kB

Release files / agrowell_ikh_client-0.1.6.tar.gz

Download URL agrowell_ikh_client-0.1.6.tar.gz
Size 51.3 kB
Tags Source
SHA-256 checksum
How to use checksums
515c4d8586b51b3009b8e90144783522ea67f25845b4d0ffb0c42e72a36f7185
BLAKE2b-256 checksum
How to use checksums
c90181baa87d49867761400fba7687e50fb1dab48b5a0f5411d303aecc345658
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.6

Release files / agrowell_ikh_client-0.1.6-py3-none-any.whl

Download URL agrowell_ikh_client-0.1.6-py3-none-any.whl
Size 66.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f7663aebf8f48ab0d561da6fbca96181cd2a4a3185f9ad424da43587a412aed1
BLAKE2b-256 checksum
How to use checksums
313f23d230e0a68638770716ee52ddb7b6c5d2ab8e545506ddfdc03c4b23fb7f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.11.6

Security for agrowell-ikh-client 0.1.6

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page