Python SDK bridging the I Know How (IKH) robot with the Agro-Well greenhouse platform.
Project description
agrowell-ikh-client
A small, typed Python SDK that bridges the I Know How (IKH) robot with the Agro-Well greenhouse platform.
v1 scope:
- Authenticate to the platform via Keycloak (client-credentials / machine-to-machine).
- Read anchors — resolve a detected AprilTag to its platform anchor (and its AR pose).
- 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 likeresources/alignmentcompose 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,TokenStorearetyping.Protocols (dependency inversion): implementations swap and fake in tests without touching call sites.- Sync today, async-ready: the synchronous
HttpxTransportsits behind theTransportseam, 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 pluggableReportSink(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
These do not block using the SDK, but must be resolved for live integration.
- Organization is mandatory: every anchor read/write is scoped to an organization. The
SDK sends
Grpc-Metadata-organization(AGROWELL_ORGANIZATION_ID, required) — and the Keycloak token must carry a matchingorganizationclaim, or the platform returnsErrOrgHeaderMissing/ErrOrgMembershipDenied. - Group WRITE needs a backend change (blocker): anchor reads accept the robot's
client-credentials token, but
UpdateAnchorsGroup(the commissioning write) currently requires a user token. The platform must relax that RPC to accept machine tokens (org kept) before the robot can write the group transform. - Report upload:
ObjectStoreSinkPUTs the report to a presigned URL; a backend endpoint must mint it (so no object-store credentials live on the robot). - Confirm conventions:
localization_provider_idvsanchor.anchors_group_idfor the PATCH, and theTransformMatrixorientation / multiplication order (verify with one real anchor round-trip). - Anchors must pre-exist and be AR-calibrated (with
apriltag_ids) for the organization. - API prefix: confirm
/v1vs/api/v1(AGROWELL_API_PATH_PREFIX). - Keycloak client: a service-account client in realm
AGRO-WELL(+ secret). - 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) andexamples/.
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.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file agrowell_ikh_client-0.1.0.tar.gz.
File metadata
- Download URL: agrowell_ikh_client-0.1.0.tar.gz
- Upload date:
- Size: 38.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3eb02f81f869dc75f497a5966621b6f1853791bcf8c146fdbcd391e4ab31be49
|
|
| MD5 |
f36dfe9647ea852d8fa2ed7b0cd039d2
|
|
| BLAKE2b-256 |
9a9947d42909af52a4a6054adddb80138ab296b8e575b404e74b077055c985c7
|
File details
Details for the file agrowell_ikh_client-0.1.0-py3-none-any.whl.
File metadata
- Download URL: agrowell_ikh_client-0.1.0-py3-none-any.whl
- Upload date:
- Size: 51.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
79f9fd3ff066bee6f724cf270474eb53d03e1ad3c045092ee47bae65d040cb15
|
|
| MD5 |
703877d7bfef46d73fa74d231b5e731b
|
|
| BLAKE2b-256 |
b2c7557ae6c371cd281c988a83f62c6693da84c124d01144395851ceda1cdb4c
|