Juno provides a small, allocation-free Internet stack for the Arduino UNO R4 WiFi. Programs can:

  • connect to a Wi-Fi network with Wifi;
  • send plain HTTP or TLS-protected HTTPS GET, POST, DELETE, PATCH, and QUERY requests with HttpClient and HttpsClient; and
  • extract typed values directly from JSON response bytes with Json.

These APIs are compiler intrinsics, like every Juno API — see docs/APIS.md for what that means, how they're built, and the general rules (compile-time-only String arguments, caller-owned buffers) this guide relies on. This is the end-user reference for using them.

Requirements and current limits

Wi-Fi, HTTP, and HTTPS require an UNO R4 WiFi entry point:

import io.github.jabrena.juno.annotations.ArduinoUnoR4WiFi;
import io.github.jabrena.juno.annotations.Board;

@Board(ArduinoUnoR4WiFi.class)
public final class InternetExample {
    // ...
}

Json itself does not touch the network and can parse any local byte[].

The current network layer deliberately stays small:

  • request hosts, paths, and request bodies must be compile-time strings — except HttpsClient.get's byte[] path overload (see Runtime request paths);
  • custom request headers, redirects, cookies, and authentication helpers are not available;
  • HTTPS uses the root CA bundle installed in the board's WiFi firmware; Juno does not yet accept a custom CA certificate from Java source;
  • the response status line and headers are consumed but are not exposed to Java code;
  • each request has a fixed five-second timeout; and
  • the response body may be truncated when it is larger than the supplied buffer.

Prefer HttpsClient for Internet services. Do not send API tokens, personal data, credentials, or other secrets with plain HttpClient.

Supplying Wi-Fi credentials

Do not put Wi-Fi credentials directly in Java source. Either load them at runtime from an SD-card properties file (see docs/STORAGE.md) or pass compile-time environment-variable reads to Wifi.begin — see docs/APIS.md for the full mechanism, including the .env/juno-maven-plugin alternative and its security caveats:

Wifi.begin(
        System.getenv("JUNO_WIFI_SSID"),
        System.getenv("JUNO_WIFI_PASSWORD"));

The same mechanism can supply an HTTP or HTTPS host, path, or request body when necessary:

int responseBytes = HttpClient.get(
        System.getenv("JUNO_API_HOST"),
        80,
        System.getenv("JUNO_API_PATH"),
        response,
        response.length);

Any value supplied this way is still embedded in the generated firmware. HttpsClient protects it in transit; HttpClient sends it without encryption.

Unlike HTTP hosts, paths, and bodies, Wifi.begin also accepts stable runtime strings returned by java.util.Properties.getProperty, so credentials can remain on removable storage rather than in firmware.

Connecting to Wi-Fi

Wifi.begin starts the connection. Poll Wifi.status() until it equals Wifi.STATUS_CONNECTED. A bounded retry loop lets the program report or display a failure instead of waiting forever:

Wifi.begin(
        System.getenv("JUNO_WIFI_SSID"),
        System.getenv("JUNO_WIFI_PASSWORD"));

int attempts = 0;
while (Wifi.status() != Wifi.STATUS_CONNECTED && attempts < 30) {
    Delay.millis(1000);
    attempts = attempts + 1;
}

if (Wifi.status() != Wifi.STATUS_CONNECTED) {
    Serial.println("WiFi connection failed");
}

Call HTTP or HTTPS operations only after the connection succeeds. Applications that run indefinitely should also decide how to handle a later disconnection, for example by waiting for connectivity or calling Wifi.begin again before the next request.

Calling REST APIs

HttpClient.get takes a host name, TCP port, path, response buffer, and buffer capacity:

private static final String HOST = "api.open-meteo.com";
private static final int PORT = 80;
private static final String PATH = "/v1/forecast?latitude=40.4168&longitude=-3.7038"
        + "&current=temperature_2m";

byte[] response = new byte[512];
int responseBytes = HttpClient.get(HOST, PORT, PATH, response, response.length);

Pass the host without http:// and normally begin the path with /. The host and path must be compile-time strings — see docs/APIS.md for exactly what that means.

For TLS, use the equivalent HttpsClient method, conventionally on port 443. Do not include https:// in the host:

int responseBytes = HttpsClient.get(
        "httpbin.org", 443, "/anything", response, response.length);

Juno emits Arduino's WiFiSSLClient, which validates the server certificate against the root CA bundle installed in the UNO R4 WiFi firmware. A missing, expired, or untrusted CA makes the connection return -1. Arduino documents how to update the bundle with Upload SSL root certificates. Juno currently uses that bundle as-is and does not expose setCACert for a per-program custom CA.

The result is:

Result Meaning
-1 The TCP connection failed.
0 No response-body bytes were captured.
1..response.length Number of body bytes stored in the buffer.

The implementation consumes HTTP headers, supports ordinary and chunked response bodies, and puts only body bytes into response. Because the HTTP status code is not exposed, applications must validate the response body rather than assuming every positive result is a successful API response.

If the body exceeds the buffer, only response.length bytes are retained. Choose a buffer large enough for the API response. JSON validation normally reports an incomplete truncated document as Json.TYPE_INVALID, but the HTTP API does not otherwise expose a separate truncation flag.

Allocate reusable response buffers outside long-running loops instead of inside while (true) — see docs/APIS.md for why:

byte[] response = new byte[512];

while (true) {
    int responseBytes = HttpClient.get(HOST, PORT, PATH, response, response.length);
    // Process response[0..responseBytes).
    Delay.millis(60000);
}

Runtime request paths

When a path depends on runtime data — for example coordinates returned by an earlier request — build it in a byte[] and use the HttpsClient.get overload that takes the buffer and its length instead of a String. Runtime Strings are limited to 31 bytes, which is far too short for a typical query string; the buffer overload accepts paths of up to 255 bytes and returns -1 without connecting when pathLength is larger:

byte[] path = new byte[256];
int pathLength = 0;
// ... copy "/v1/forecast?latitude=", the runtime latitude, and so on into path ...
int responseBytes = HttpsClient.get("api.open-meteo.com", 443, path, pathLength,
        response, response.length, headers, headers.length, statusAndHeaders);

The host must still be a compile-time string. Only HTTPS GET has this overload today. WeatherTFT uses it to pass the coordinates returned by ipinfo.io to Open-Meteo.

Choosing an HTTP method

Both HttpClient and HttpsClient provide the following methods. They write the response body into the supplied buffer and return the same result values described above:

Method Request body Typical purpose
get None Read a resource.
post JSON Create a resource or invoke a non-idempotent operation.
delete None Delete a resource.
patch JSON Partially update a resource.
query JSON Perform a safe, idempotent query whose parameters do not fit naturally in a URI.

QUERY is the HTTP method standardized by RFC 10008. The remote server must explicitly support it; many existing REST services do not. Juno sends Content-Type: application/json for POST, PATCH, and QUERY, and currently provides no way to select a different media type.

Sending JSON with POST, PATCH, or QUERY

The body-carrying methods send a compile-time JSON body. For example, post can create a resource:

private static final String BODY = "{\"sensor\":\"outside\",\"value\":23}";

int responseBytes = HttpClient.post(
        "example.com",
        80,
        "/api/readings",
        BODY,
        response,
        response.length);

patch uses the same signature to partially update a resource:

int responseBytes = HttpClient.patch(
        "example.com",
        80,
        "/api/readings/7",
        "{\"value\":24}",
        response,
        response.length);

Use query only with a server that implements RFC 10008:

int responseBytes = HttpClient.query(
        "example.com",
        80,
        "/api/readings/search",
        "{\"sensor\":\"outside\"}",
        response,
        response.length);

The body cannot be assembled from runtime sensor readings yet because Juno has no runtime String construction. Custom authorization headers are also unsupported.

Deleting a resource

delete sends no request body:

int responseBytes = HttpClient.delete(
        "example.com",
        80,
        "/api/readings/7",
        response,
        response.length);

Extracting information from JSON responses

Always pass the number returned by HttpClient, not the array capacity, to Json:

int responseBytes = HttpClient.get(HOST, PORT, PATH, response, response.length);
if (responseBytes > 0) {
    double temperature = Json.getDouble(
            response, responseBytes, "current.temperature_2m");
}

The parser validates the complete bounded document and reads values directly from the response buffer. It supports objects, arrays, strings, numbers, booleans, and null, with nesting up to 32 levels.

Paths

Object fields use dots and array elements use zero-based bracket indexes:

JSON Path
{"temperature": 21} "temperature"
{"current":{"temperature":21}} "current.temperature"
{"users":[{"name":"Ada"}]} "users[0].name"
[{"id":7}] "[0].id"
Any root value ""

Paths must be compile-time string literals. A path cannot be assembled dynamically, so a runtime loop cannot generate "items[" + index + "]"; programs currently access known indexes using separate literal paths. Field names containing ., [, or ], and field names encoded with JSON escapes, cannot currently be addressed.

Detecting missing, null, wrong-type, and invalid values

Typed getters return convenient zero values on failure. That means getInt(...) == 0 and getBool(...) == false are ambiguous by themselves. Use Json.type when the distinction matters:

int valueType = Json.type(response, responseBytes, "current.temperature_2m");
if (valueType == Json.TYPE_NUMBER) {
    double temperature = Json.getDouble(
            response, responseBytes, "current.temperature_2m");
} else if (valueType == Json.TYPE_NULL) {
    Serial.println("Temperature is null");
} else if (valueType == Json.TYPE_MISSING) {
    Serial.println("Temperature is missing");
} else if (valueType == Json.TYPE_INVALID) {
    Serial.println("Invalid or truncated JSON");
}

Available type constants are:

Constant Meaning
TYPE_MISSING The path does not resolve.
TYPE_NULL JSON null.
TYPE_BOOLEAN true or false.
TYPE_NUMBER A valid JSON number.
TYPE_STRING A JSON string.
TYPE_OBJECT A JSON object.
TYPE_ARRAY A JSON array.
TYPE_INVALID The document, length, or path syntax is invalid.

Reading numbers and booleans

Use the getter matching the response schema:

int count = Json.getInt(response, responseBytes, "count");
long sequence = Json.getLong(response, responseBytes, "sequence");
double temperature = Json.getDouble(response, responseBytes, "temperature");
boolean enabled = Json.getBool(response, responseBytes, "enabled");

getInt and getLong accept JSON integer tokens and reject fractions, exponents, and values outside their signed range by returning zero. getDouble accepts integer, fractional, and exponent forms such as 12, -0.125, and 1.25e2. Check Json.type first when zero or false is a legitimate value that must be distinguished from a conversion failure.

Reading strings

Juno cannot create a runtime Java String, so Json.getString decodes into a caller-owned UTF-8 byte buffer:

byte[] name = new byte[32];
int nameBytes = Json.getString(
        response, responseBytes, "user.name", name, name.length);

int index = 0;
while (index < nameBytes) {
    int unsignedByte = name[index] & 255;
    // Consume the UTF-8 byte here.
    index = index + 1;
}

The method decodes JSON escapes such as \n, \", and \u00e9, including Unicode surrogate pairs. It returns the number of bytes written, does not append a NUL terminator, and never writes more than the supplied output length. A small output buffer truncates the decoded result.

Inspecting arrays

Use arraySize to validate an expected array shape, and literal indexed paths to read known elements:

int userCount = Json.arraySize(response, responseBytes, "users");
if (userCount > 0) {
    int firstUserId = Json.getInt(response, responseBytes, "users[0].id");
}

arraySize returns -1 when the path is missing, invalid, or not an array. Dynamic traversal of an arbitrary-length array is not yet available because JSON paths must be literals.

Complete REST example

This example connects with build-time credentials, requests Madrid's current temperature, validates the JSON type, and prints an integer temperature over USB serial once a minute:

import io.github.jabrena.juno.annotations.ArduinoUnoR4WiFi;
import io.github.jabrena.juno.annotations.Board;
import io.github.jabrena.juno.api.Delay;
import io.github.jabrena.juno.api.io.serial.BaudRate;
import io.github.jabrena.juno.api.io.serial.Serial;
import io.github.jabrena.juno.api.net.http.HttpClient;
import io.github.jabrena.juno.api.net.http.Json;
import io.github.jabrena.juno.api.net.Wifi;

@Board(ArduinoUnoR4WiFi.class)
public final class InternetExample {
    private static final String HOST = "api.open-meteo.com";
    private static final String PATH = "/v1/forecast?latitude=40.4168&longitude=-3.7038"
            + "&current=temperature_2m";

    public static void main(String[] args) {
        Serial.begin(BaudRate.BAUD_115200);
        Wifi.begin(
                System.getenv("JUNO_WIFI_SSID"),
                System.getenv("JUNO_WIFI_PASSWORD"));

        int attempts = 0;
        while (Wifi.status() != Wifi.STATUS_CONNECTED && attempts < 30) {
            Delay.millis(1000);
            attempts = attempts + 1;
        }

        if (Wifi.status() != Wifi.STATUS_CONNECTED) {
            while (true) {
                Serial.println("WiFi connection failed");
                Delay.millis(5000);
            }
        }

        byte[] response = new byte[512];
        while (true) {
            int responseBytes = HttpClient.get(HOST, 80, PATH, response, response.length);
            if (responseBytes < 0) {
                Serial.println("HTTP connection failed");
            } else if (Json.type(response, responseBytes,
                    "current.temperature_2m") == Json.TYPE_NUMBER) {
                int temperature = (int) Json.getDouble(
                        response, responseBytes, "current.temperature_2m");
                Serial.print("Temperature C: ");
                Serial.println(temperature);
            } else {
                Serial.println("Unexpected JSON response");
            }
            Delay.millis(60000);
        }
    }
}

Place the class in juno-examples/src/main/java/InternetExample.java, then build, compile, upload, and monitor it:

./mvnw package

JUNO_WIFI_SSID='your-network-name' \
JUNO_WIFI_PASSWORD='your-network-password' \
java -jar juno-compiler/target/juno-compiler-0.1.0-SNAPSHOT.jar compile \
  --main InternetExample \
  --classpath juno-examples/target/classes:juno-api/target/classes

arduino-cli compile \
  --fqbn arduino:renesas_uno:unor4wifi \
  build/juno/InternetExample

arduino-cli upload \
  --port /dev/cu.YOUR_PORT \
  --fqbn arduino:renesas_uno:unor4wifi \
  build/juno/InternetExample

arduino-cli monitor \
  --port /dev/cu.YOUR_PORT \
  --config baudrate=115200

Uploading replaces the board's current firmware. Find the correct port first with arduino-cli board list. For the broader build/upload workflow, see docs/ARDUINO.md. The repository includes HttpMethods.java, which verifies all supported methods over plain HTTP, HttpsMethods.java, which repeats the checks with certificate-validated TLS, and MadridWeather.java, which extracts live API data for display on the LED matrix.

Boards talking to each other over UDP

Udp works on both the UNO R4 WiFi and the UNO Q, and unlike the HTTP client its payloads are runtime byte[] buffers. That makes it the way for boards to exchange data they compute. Two games build a small factorization network on it, one centralized and one decentralized (see Games). Code they share, such as SHA-256 and the factorizer, lives in games/blockchain.

Centralized: Ledger

Every board runs LedgerGame and picks its role on the TFT touch screen:

  • LEADER (LeaderFlow), meant for an UNO Q, hands out 40-bit numbers to factor. It checks each answer by multiplying it back and testing every factor for primality, then adds it to a SHA-256 hash chain. Each block hashes the previous block's hash, so changing an accepted result changes every later hash.
  • WORKER (WorkerFlow), meant for one or more UNO R4 WiFi boards, broadcasts JOIN until a leader answers. It then factors each task with trial division and Pollard's rho, and sends back the result.

No addresses are configured: workers find the leader by broadcast and can join at any time. The leader's next task acknowledges a result, so a lost datagram only costs a resend. A worker that stays silent for two minutes loses its task to the next worker that asks. This is a tamper-evident log with a single writer (the leader), not a decentralized blockchain with consensus.

./mvnw -f juno-examples/pom.xml compile juno:upload -Djuno.board=arduino-uno-q \
  -Djuno.main=io.github.jabrena.juno.usecases.ledger.LedgerGame
./mvnw -f juno-examples/pom.xml compile juno:upload -Djuno.board=arduino-uno-r4-wifi \
  -Djuno.main=io.github.jabrena.juno.usecases.ledger.LedgerGame
./mvnw -f juno-examples/pom.xml juno:monitor -Djuno.baudRate=115200

Decentralized: Miner

MinerGame removes the leader. Every board is an equal node that mines, validates and relays blocks.

  • Proof-of-work. A block counts when the SHA-256 hash of its 80-byte, Bitcoin-layout header has at least bits leading zero bits. The miner absorbs the fixed first 64 header bytes once (the midstate), so each attempt costs one compression.
  • Useful work. Each block must also carry the factorization of a 40-bit number derived from its parent's hash, which every node checks. Miners can't choose an easy number.
  • Consensus. The branch with the most cumulative work is the chain. Every eight blocks the difficulty moves one bit toward a block every 30 seconds.
  • Joining late. A node that boots late asks for the current tip, and asks again every 30 seconds. It fetches up to four missing ancestors, asking everyone again if an answer is lost; if it still can't connect them to a block it knows, it adopts the oldest as a checkpoint, much like a pruned Bitcoin node, and validates everything after it.
  • Heartbeats. Every node broadcasts a small HELLO packet every five seconds or so, with its id and the height and work of its tip. A node a peer has not heard from for 15 seconds is suspect, and after 25 seconds it is declared lost and logged over serial; its next heartbeat brings it back. Time is measured on each board's own clock. The screen shows Nodes N mining M, counting the board itself.
  • Catching up. A heartbeat that announces more work than a node's tip holds makes it ask that peer for the tip, and pause mining (SYNC on the screen) instead of building a rival branch. The pause is limited to 30 seconds, so a stale or lying heartbeat cannot stop a node for good. Nodes that predate heartbeats ignore them and keep working.
  • Two cooperative threads. A virtual thread serves the network (it receives and answers packets, and sends the heartbeats) while the main thread mines and shows the tip. They share one block pool guarded by its monitor and switch only where the program already pauses, so mining never waits for the radio.
./mvnw -f juno-examples/pom.xml compile juno:upload -Djuno.board=arduino-uno-r4-wifi \
  -Djuno.Xss=1536 -Djuno.main=io.github.jabrena.juno.usecases.miner.MinerGame

The UNO R4 WiFi needs -Djuno.Xss=1536: the default 2 KiB stack for each of the three extra thread slots takes RAM that the 8 KiB arena would otherwise need to fit, and the miner's network thread needs about 1.4 KB of stack. The UNO Q has the room and takes the defaults.

This is phase 1 of the plan in issue #43: blocks are not signed yet, and block times are not bounded against the future.

Wire format and other boards

The wire formats are plain fixed-size bytes on UDP port 4210 (documented in FactorProtocol and ChainProtocol). A board Juno cannot compile for, such as the 8-bit UNO WiFi Rev2, can still join with a hand-written sketch that speaks the same packets.

Troubleshooting

  • Compilation says an environment variable is not set: export it in the same shell or CI step that invokes juno compile; setting it only for mvn package is not sufficient.
  • Wi-Fi never reaches STATUS_CONNECTED: verify the SSID/password and signal strength, and use a bounded retry loop so the failure is observable.
  • HttpClient returns -1: the TCP connection failed. Confirm Wi-Fi connectivity, the host, port, DNS availability, and that the service accepts plain HTTP.
  • HttpClient returns 0: no body bytes were captured before the connection ended or timed out.
  • HttpsClient returns -1: the TCP or TLS connection failed. Check the host and port, update the board's connectivity firmware, and ensure its root CA bundle trusts the service certificate.
  • Json.type returns TYPE_INVALID: pass the returned HTTP byte count—not response.length—and increase the response buffer if the body may have been truncated.
  • A getter returns zero or false: call Json.type to distinguish a legitimate value from a missing path, wrong type, or invalid document.
  • An API requires bearer headers, a custom per-program CA, a non-JSON request body, or runtime request data: it is outside the current client feature set.