Apparatus Code Writing Guide ​

Audience: Apparatus builders, LLMs writing firmware logic, and anyone creating behavior for a standalone Juvantia Apparatus.


1. How Apparatus Firmware Works ​

Unlike a Robulus (which is a real-time rover), an Apparatus is a standalone, usually stationary "automaton" that interacts with robots over Bluetooth Low Energy (BLE).

The Token-Authentication State Machine ​

An Apparatus is essentially a gatekeeper. It stays in an IDLE state, advertising its presence. When a robot approaches, it serves a Menu of possible actions. When a robot chooses an action, it must present a Verified Token (signed by Juvantia). The Apparatus verifies this token offline using a shared secret and, if valid, executes the requested behavior.

Execution Flow ​

POWER ON
  │
  ├─── System Initialization (ap_ble_init)
  │      • Starts NimBLE Server
  │      • Sets up Service UUID: juva0001-...
  │      • Loads Menu JSON from firmware
  │
  └─── User Code Execution
         • Calls user_setup() ONCE
         • Enters infinite loop calling user_loop()
         • Waits for BLE Events:
              │
              ├─── Robot selects Action → calls on_menu_action(action_id)
              │
              ├─── Robot presents Token → Verification Process
              │      │
              │      ├─── SUCCESS → calls on_auth_granted(action_id, passes_remaining)
              │      │      • System drops "Usage Receipt" for Robot
              │      │
              │      └─── DENIED → calls on_auth_denied(reason)
              │
              └─── Timeout / Connection Lost → calls on_auth_idle()

2. Global Constants & Identity ​

The compiler automatically injects several constants into your build. You can use these in your logic:

ConstantTypeDescription
JUNCTUM_PUBLIC_IDconst char*The ID of your apparatus (e.g., "AP-A7X1")
AP_BLE_MENU_JSONconst char*The raw JSON string of your BLE menu

3. The Hook System (Event Handlers) ​

Your logic lives inside "Hooks". You don't need to implement all of them, but on_auth_granted is usually the most important.

3.1 user_setup() ​

Called once at boot. Use it to initialize your pins and libraries.

cpp
#include <ESP32Servo.h>
Servo gateServo;

void user_setup() {
    pinMode(2, OUTPUT); // Status LED
    gateServo.attach(5);
    gateServo.write(0); // Close gate
}

3.2 user_loop() ​

Called continuously. Avoid using delay(). Use millis() for timing.

cpp
void user_loop() {
    // Pulse a status light or check sensors
}

3.3 on_auth_granted(int action_id, int passes_remaining) ​

The most critical hook. Triggered when a robot successfully pays for an action.

  • action_id: The ID of the action from your BLE Menu.
  • passes_remaining: How many passes the robot has left after this transaction.
cpp
void on_auth_granted(int action_id, int passes_remaining) {
    if (action_id == 1) { // e.g., "Open Barrier"
        gateServo.write(90);
        digitalWrite(2, HIGH);
    }
}

3.4 on_menu_action(int action_id) ​

Triggered when a robot "clicks" a menu item that has Cost = 0. Useful for free info pings or status checks.

3.5 on_auth_denied(String reason) ​

Triggered if verification fails.

  • reason: "invalid_signature", "no_funds", "base64_error", or "replay".

3.6 on_auth_idle() ​

Triggered w### 4. The Economy of Actions (Passes)

Interaction with an Apparatus is based on a credit-based economy. When a robot approaches, it "pays" for specific services using digital credits.

  • Passes Required: The specific number of passes deducted for an action (formerly called "Cost").
  • Passes Available: The total balance carried in the Robot's encrypted token.
  • Trust Model: The Apparatus is the "merchant" and the Robot is the "customer". The Apparatus must have its own internal Passes requirements table to prevent a malicious robot from claiming an action is cheaper than intended.
  • Transaction Logic:
    • If Passes Required = 0: The action is free. The Apparatus triggers on_menu_action immediately upon request.
    • If Passes Required > 0: The action requires payment. The Apparatus verifies the robot's token. If it has enough available balance, the amount is subtracted (locally) and on_auth_granted is triggered.
  • Usage Receipts: After a successful "paid" action, the Apparatus sends a receipt to the Robot. The robot stores it and eventually syncs it with the cloud to permanently update the global balance.

5. Understanding Action IDs and Passes ​

When you define your BLE Menu in Fabrica, you assign each item an ID and the number of Passes it requires.

Menu ItemIDPassesLogic Execution HookDescription
"Check Status"100on_menu_action(10)Immediate info ping. No token or payment required.
"Open Gate"11on_auth_granted(1, ...)Simple access. Requires 1 verified pass from the robot.
"Fast Charge"53on_auth_granted(5, ...)High-power action. Requires 3 passes from the robot's balance.
"Full Repair"710on_auth_granted(7, ...)Premium service. Requires 10 passes from the robot storage.
"Play Sound"200on_menu_action(20)Free interaction. Triggers sound/LED signals without any cost.
"E-Stop"990on_menu_action(99)Emergency broadcast. Free and immediate response for safety.

6. Security: Replay Protection ​

Each Apparatus firmware has a built-in Nonce Cache. When a robot presents a token, it contains a unique nonce. If the same token is presented again (a Replay Attack), the Apparatus will return DENIED ("replay") and trigger on_auth_denied.

IMPORTANT

This protection is offline. The apparatus remembers the last 16 unique nonces in RAM.


Recipe: Simple Barrier (Servo) ​

cpp
#include <ESP32Servo.h>
Servo myservo;

void user_setup() {
    myservo.attach(5);
    myservo.write(0);
}

void on_auth_granted(int action_id, int p_rem) {
    if (action_id == 1) {
        myservo.write(90);
    }
}

void on_auth_idle() {
    myservo.write(0); // Auto-close when robot leaves
}

Recipe: Resource Dispenser (Timed Relay) ​

cpp
#define RELAY_PIN 12
unsigned long openTime = 0;

void user_setup() {
    pinMode(RELAY_PIN, OUTPUT);
    digitalWrite(RELAY_PIN, LOW);
}

void on_auth_granted(int action_id, int p_rem) {
    digitalWrite(RELAY_PIN, HIGH);
    openTime = millis();
}

void user_loop() {
    if (openTime > 0 && millis() - openTime > 5000) {
        digitalWrite(RELAY_PIN, LOW);
        openTime = 0;
    }
}

Recipe: Visual Status (NeoPixels) ​

cpp
#include <FastLED.h>
CRGB leds[1];

void user_setup() {
    FastLED.addLeds<WS2812, 48, GRB>(leds, 1);
    leds[0] = CRGB::Blue; // Idle color
    FastLED.show();
}

void on_auth_granted(int action_id, int p_rem) {
    leds[0] = CRGB::Green;
    FastLED.show();
}

void on_auth_denied(String reason) {
    leds[0] = CRGB::Red;
    FastLED.show();
}

void on_auth_idle() {
    leds[0] = CRGB::Blue;
    FastLED.show();
}

8. Troubleshooting for LLMs ​

  • Pins: Always use the GPIO numbers for your specific board (e.g., ESP32-C3 has different pinouts than S3).
  • Blocking: Ensure user_loop never blocks for more than 50ms, or BLE notifications might be dropped.
  • Libraries: Add libraries in Step 3 (e.g., madhephaestus/ESP32Servo) before referring to them in code. .
  • Libraries: Add libraries in Step 3 (e.g., madhephaestus/ESP32Servo) before referring to them in code.
Maintained by Juvantia Operator