Skip to main content

Robotics and embedded systems

This guide shows how to turn a robotics domain contract into an owned edge stack:

  • ThorAPI uses one OpenAPI contract to generate the repeatable Spring Boot foundation: models, API boundaries, persistence structure, and aligned clients.
  • Your engineering team supplies the hardware adapter, safety policy, device-specific logic, and operational limits.
  • ValkyrAI coordinates deterministic workflows and can call a local Ollama- or llama.cpp-compatible model when the node is disconnected.
  • A local database holds bounded device state and receipts. A fleet system remains a separate concern.

The goal is not to let an LLM drive a machine. The goal is to give a governed workflow useful local intelligence while deterministic code, physical interlocks, and explicit policy remain in charge of every actuator.

Reference architecture

The command path must continue to work safely when the model is slow, unavailable, or wrong. The control API should accept domain commands such as STOP, RETURN_HOME, or SET_SPEED; it should never expose arbitrary shell execution, raw SQL, or unrestricted GPIO access.

1. Define the control contract

Treat OpenAPI as the durable boundary between operators, automation, and the machine. Model desired state separately from observed state, make commands idempotent, and place physical limits directly in the schema.

robot-control.yaml
openapi: 3.1.0
info:
title: Edge Robot Control API
version: 1.0.0
paths:
/devices/{deviceId}:
get:
operationId: getDeviceState
parameters:
- $ref: "#/components/parameters/DeviceId"
responses:
"200":
description: Current desired and observed device state
content:
application/json:
schema:
$ref: "#/components/schemas/DeviceState"
/devices/{deviceId}/commands:
post:
operationId: submitDeviceCommand
parameters:
- $ref: "#/components/parameters/DeviceId"
- name: Idempotency-Key
in: header
required: true
schema:
type: string
minLength: 16
maxLength: 128
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/DeviceCommand"
responses:
"202":
description: Command accepted for policy evaluation
content:
application/json:
schema:
$ref: "#/components/schemas/CommandReceipt"
"409":
description: Command conflicts with current device state
components:
parameters:
DeviceId:
name: deviceId
in: path
required: true
schema:
type: string
pattern: "^[a-zA-Z0-9-]{3,64}$"
schemas:
DeviceCommand:
type: object
required: [command, requestedAt]
additionalProperties: false
properties:
command:
type: string
enum: [STOP, RETURN_HOME, SET_SPEED, SET_TARGET]
speedMetersPerSecond:
type: number
minimum: 0
maximum: 1.5
target:
$ref: "#/components/schemas/Target"
requestedAt:
type: string
format: date-time
Target:
type: object
required: [xMeters, yMeters]
additionalProperties: false
properties:
xMeters: { type: number, minimum: -100, maximum: 100 }
yMeters: { type: number, minimum: -100, maximum: 100 }
DeviceState:
type: object
required: [deviceId, mode, observedAt]
properties:
deviceId: { type: string }
mode:
type: string
enum: [IDLE, RUNNING, DEGRADED, ESTOP]
batteryPercent: { type: number, minimum: 0, maximum: 100 }
observedAt: { type: string, format: date-time }
CommandReceipt:
type: object
required: [commandId, status]
properties:
commandId: { type: string, format: uuid }
status:
type: string
enum: [ACCEPTED, REJECTED, EXECUTING, COMPLETED, FAILED]

The OpenAPI document describes the transport contract, not the complete safety case. A maximum in JSON Schema is useful input validation, but the hardware controller must independently enforce speed, torque, travel, temperature, and emergency-stop limits.

See ThorAPI code generation and the OpenAPI specification for the full contract workflow.

2. Generate the owned backend

Use the standard ThorAPI generation flow or the ValorIDE application wizard with the control specification. The generated Spring stack supplies the repeatable API foundation; keep hardware integration behind a narrow interface that generated controllers or services call.

RobotHardware.java
public interface RobotHardware {
ObservedState readState();
CommandResult stop();
CommandResult returnHome();
CommandResult setSpeed(BigDecimal metersPerSecond);
CommandResult setTarget(Target target);
}

Use separate implementations for real hardware and simulation:

RobotHardware
├── GpioRobotHardware production device adapter
├── CanBusRobotHardware industrial bus adapter
└── SimulatedRobotHardware tests and operator training

This boundary keeps generated web and persistence code independent of GPIO libraries, serial ports, CAN drivers, and vendor SDKs. It also allows the complete API and workflow path to be exercised without a physical machine.

3. Choose an edge persistence profile

Spring Boot can auto-configure H2 when the dependency is present. H2 is a useful single-process or single-device store when the write rate, durability requirement, and dataset fit the node. An in-memory URL is ephemeral; use a file URL for state that must survive a restart.

Maven dependencies

pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>

Use the JDK required by your generated project. Current Spring Boot system requirements specify Java 17 or newer, but the exact requirement follows the Spring Boot version in your generated artifact.

Production edge configuration

application-edge.properties
# Put persistent data on the node's durable data volume.
spring.datasource.url=jdbc:h2:file:${VALKYR_DATA_DIR:/var/lib/valkyr}/robotics;AUTO_SERVER=FALSE;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=${VALKYR_DB_USER}
spring.datasource.password=${VALKYR_DB_PASSWORD}

# Apply reviewed migrations rather than mutating production schemas implicitly.
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.open-in-view=false

# The H2 browser console is not a production administration surface.
spring.h2.console.enabled=false

management.endpoints.web.exposure.include=health,info,prometheus
management.endpoint.health.probes.enabled=true

Do not use spring.jpa.hibernate.ddl-auto=update as a production migration strategy. Use reviewed Flyway or Liquibase migrations when schema evolution matters.

Development-only console

Spring Boot's documentation describes the H2 console as a development feature and warns that it does not implement normal production security protections. Keep it disabled in the edge profile. If a developer needs it temporarily, create a local profile:

application-local.properties
spring.h2.console.enabled=true
spring.h2.console.settings.web-allow-others=false

Bind the application to loopback or reach it through an SSH tunnel:

ssh -L 8080:127.0.0.1:8080 edge-admin@robot-gateway

Then open the /h2-console path on local port 8080 from the developer workstation. Do not set spring.h2.console.settings.web-allow-others=true on a production device.

Know when to replace H2

Move to PostgreSQL, a time-series database, or the fleet's durable store when any of these become true:

  • several processes or devices need to write the same database;
  • telemetry retention or query volume outgrows the edge node;
  • replication, multi-node failover, or fleet-wide transactions are required;
  • regulatory durability or recovery objectives exceed a local file database;
  • the database is being used as a message bus.

H2 can remain a local cache or bounded state store after fleet synchronization is introduced.

4. Package for a Raspberry Pi-class node

A Spring Boot service can run on ARM64 hardware when a compatible JDK is installed and the workload fits the device. Treat every memory number as a starting point to benchmark, not a guarantee.

java -Xms128m -Xmx512m -XX:MaxRAMPercentage=65 \
-jar robot-control-service.jar \
--spring.profiles.active=edge

Avoid reserving more heap than the device can afford. Leave memory for the operating system, database cache, hardware drivers, telemetry, and local model runtime. On a node that also runs inference, the model usually dominates RAM; choose the model and context window first, then size the JVM from measured peak usage.

systemd service

/etc/systemd/system/valkyr-robot.service
[Unit]
Description=Valkyr robot control service
After=network-online.target local-fs.target ollama.service
Wants=network-online.target

[Service]
Type=simple
User=valkyr
Group=valkyr
WorkingDirectory=/opt/valkyr/robot
EnvironmentFile=/etc/valkyr/robot.env
Environment="JAVA_TOOL_OPTIONS=-Xms128m -Xmx512m -XX:MaxRAMPercentage=65"
ExecStart=/usr/bin/java -jar /opt/valkyr/robot/robot-control-service.jar --spring.profiles.active=edge
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/lib/valkyr /var/log/valkyr

[Install]
WantedBy=multi-user.target

Give the service user access only to the exact device files and groups its adapter needs. Do not run the API as root just to reach a GPIO, serial, USB, or CAN device.

Protect storage

Frequent writes shorten the useful life of low-end removable flash. For sustained telemetry or receipt traffic:

  • use an appropriate USB SSD or NVMe device supported by the target Raspberry Pi;
  • keep the database, model files, and write-heavy logs on the data volume;
  • batch or sample high-frequency telemetry instead of committing every sensor tick;
  • rotate logs and cap retention;
  • test recovery by restoring a backup onto replacement hardware;
  • provide enough power for the Pi and attached storage.

Raspberry Pi's official documentation describes supported USB mass-storage boot options and calls out power requirements. Hardware compatibility still depends on the exact board, bootloader, enclosure, and storage adapter.

5. Run ValkyrAI with a local model

ValkyrAI's LLM adapter can route an AI workflow module to an Ollama-compatible base URL. The adapter calls the OpenAI-compatible /v1/chat/completions path; a local service can therefore remain entirely on the node or private edge network.

Ollama on ARM64 Linux

Follow the current official Ollama Linux instructions for the ARM64 package and service setup. Ollama binds to 127.0.0.1:11434 by default. Keep that default when ValkyrAI runs on the same machine:

ValkyrAI AI Chat module
provider: OLLAMA
ollamaModelUrl: ${OLLAMA_BASE_URL}
temperature: 0.1

Set OLLAMA_BASE_URL to the HTTP loopback interface at 127.0.0.1 and Ollama's default port 11434.

Do not set OLLAMA_HOST=0.0.0.0 merely for convenience. If another edge service must call the model, place it behind a private service interface, firewall rules, and authenticated proxy. Model APIs are not actuator APIs.

Choose a quantized instruction model that fits the target's available memory and latency budget. Validate the exact model license, architecture support, context size, and peak memory on the actual device.

llama.cpp alternative

llama.cpp supports ARM processors, GGUF quantized models, and an OpenAI-compatible HTTP server. A loopback-only service can be started as follows:

llama-server \
-m /opt/valkyr/models/edge-instruct.gguf \
--host 127.0.0.1 \
--port 8081 \
-c 2048

Configure the ValkyrAI-compatible base URL with the HTTP loopback interface at 127.0.0.1 and port 8081; the workflow adapter appends /v1/chat/completions.

Keep the LLM outside the safety loop

A safe workflow separates interpretation from execution:

Require structured output, reject unknown properties, clamp nothing silently, and fail closed. An emergency stop must work even if Java, the network, the database, the workflow engine, and the model all fail at once.

6. Secure remote control

Remote operation is a security boundary, not just another REST endpoint.

ConcernRecommended boundary
TransportTLS, preferably a private overlay network or mutual TLS for managed devices
IdentityShort-lived workload or user identity; no shared factory password
AuthorizationRBAC and device-level ACLs for each command family
ReplayRequired idempotency key, command expiry, nonce or sequence checks
Unsafe stateState-machine rejection plus a local hardware interlock
ObservabilityAccepted, rejected, started, completed, and failed receipts
Connectivity lossLocal policy remains authoritative; remote commands expire
UpdatesSigned artifacts, staged rollout, health gate, and recoverable rollback

Separate command traffic from bulk telemetry. An operator's request should enter a queue or state machine, not block an HTTP thread while a physical motion completes. Return a command receipt immediately, then expose progress through state, events, or a narrow subscription channel.

7. Disconnected and fleet operation

Design the edge node so connectivity changes synchronization, not safety:

  1. Continue local control with the most recent approved configuration.
  2. Record commands, policy decisions, model proposals, and outcomes locally.
  3. Expire remote requests that cannot be validated within their allowed window.
  4. Buffer a bounded amount of telemetry with an explicit eviction policy.
  5. Reconcile receipts and observed state when the fleet channel returns.
  6. Treat conflicts as domain events; do not silently overwrite newer local state.

For a fully air-gapped deployment, pre-stage the JDK, application artifact, database migrations, model, model runtime, signatures, SBOM, and rollback artifact. The install must not need a package registry or model download at startup.

8. Verification matrix

Test the generated service and the physical boundary separately, then together.

LayerRequired tests
OpenAPI contractSchema validation, enum and range boundaries, incompatible-change detection
Generated backendController/service/repository tests, migrations, auth failures, idempotency
Hardware adapterSimulator parity, timeouts, invalid state, device disconnect, emergency stop
WorkflowStructured-output rejection, model timeout, no-model mode, approval path
PersistencePower-loss recovery, full disk, corrupted file, restore drill
NetworkOffline boot, reconnect, replay, expired command, partitioned fleet
Resource profileCold start, steady RAM, peak heap, storage growth, thermal throttling
ReleaseSigned artifact, staged health gate, failed-upgrade rollback

Run the exact release artifact under the same JVM flags, storage medium, model, and power constraints used in the field. Desktop success is not edge readiness.

Production checklist

  • OpenAPI exposes bounded domain commands, not arbitrary execution.
  • Every command requires identity, authorization, idempotency, and expiry.
  • Hardware limits and emergency stop do not depend on the LLM or network.
  • LLM output is structured, validated, auditable, and treated as a proposal.
  • H2 is file-backed on durable storage and the console is disabled.
  • Schema changes use reviewed migrations.
  • JVM and model memory were measured together on the target hardware.
  • The system boots, operates safely, and records receipts without a network.
  • Logs and telemetry have retention limits.
  • Backups and full-device replacement were tested.
  • Updates are signed, staged, health-checked, and recoverable.

Primary references

Next: generate the backend with ThorAPI, configure the ValkyrAI LLM adapter, and keep the first hardware integration behind a simulator-tested adapter.