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:
| Constant | Type | Description |
|---|---|---|
JUNCTUM_PUBLIC_ID | const char* | The ID of your apparatus (e.g., "AP-A7X1") |
AP_BLE_MENU_JSON | const 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.
#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.
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.
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_actionimmediately 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_grantedis triggered.
- If Passes Required = 0: The action is free. The Apparatus triggers
- 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 Item | ID | Passes | Logic Execution Hook | Description |
|---|---|---|---|---|
| "Check Status" | 10 | 0 | on_menu_action(10) | Immediate info ping. No token or payment required. |
| "Open Gate" | 1 | 1 | on_auth_granted(1, ...) | Simple access. Requires 1 verified pass from the robot. |
| "Fast Charge" | 5 | 3 | on_auth_granted(5, ...) | High-power action. Requires 3 passes from the robot's balance. |
| "Full Repair" | 7 | 10 | on_auth_granted(7, ...) | Premium service. Requires 10 passes from the robot storage. |
| "Play Sound" | 20 | 0 | on_menu_action(20) | Free interaction. Triggers sound/LED signals without any cost. |
| "E-Stop" | 99 | 0 | on_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.
7. Popular Recipes
Recipe: Simple Barrier (Servo)
#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)
#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)
#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_loopnever 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.