Skip to content

Network (Net::HTTP / WebSocket / TLS)

New in 2.0. Apps can talk to the internet with the API you already know from CRuby: Net::HTTP, WebSocket, and TLS underneath both.

Available on both machines — Modern reaches the network through the ESP32-C6, Retro through the ESP32-S3's own radio — and in the Linux simulator, which uses the host's network and OpenSSL.

Before you start

The device needs Wi-Fi credentials in /etc/wifi.toml. See Connecting to Wi-Fi for how to write that file on the device.

Retro runs one radio at a time

The ESP32-S3 has a single radio, so Wi-Fi and BLE cannot both be up. If the BLE web console is running, Wi-Fi will not start; set ble_auto_start to false in Config and reboot. On Modern the ESP32-C6 handles both at once and there is no conflict.

Checking the connection

if FmrbApp.wifi_connected?
  info = FmrbApp.wifi_info
  # => { connected: true, ip: "192.168.10.16", ssid: "my-ap", hostname: "fmruby" }
  Log.info("address: #{info[:ip]}")
end
Method Returns
FmrbApp.wifi_connected? true / false. Allocates nothing, so it is safe to call in a loop
FmrbApp.wifi_info A Hash with :connected, :ip, :ssid, :hostname, or nil on a machine with no Wi-Fi

An app that needs the network should wait for it rather than assume it, because Wi-Fi comes up after the desktop does:

def on_create
  @ready = false
end

def on_update
  unless @ready
    return unless FmrbApp.wifi_connected?
    @ready = true
    fetch_data
  end
end

HTTP

require 'net/http'
require 'json'

# One-shot GET
body = Net::HTTP.get(URI.parse("http://example.com/api/status"))

# With the response object
res = Net::HTTP.get_response(URI.parse("https://example.com/data.json"))
if res.code == "200"
  data = ::JSON.parse(res.body)
end

# Form POST
res = Net::HTTP.post_form(URI.parse("https://example.com/post"), { "key" => "value" })

# Reusing a session
http = Net::HTTP.new("example.com", 443)
http.use_ssl = true
http.start do |h|
  res = h.get("/index.html")
  res = h.post("/api", '{"a":1}', { "Content-Type" => "application/json" })
end

HTTPS works out of the box: certificates are verified against the bundled CA set, so a plain https:// URL needs no extra setup.

Write ::JSON, not JSON

Inside a class body, a bare constant like JSON is looked up as YourClass::JSON and fails. Prefix it with ::.

Fetching without stopping (FmrbNet.request)

New in 2.1. Net::HTTP waits for the server, which is fine for a tool that only ever runs on a board. An app that should also work in the browser asks this way instead: the request is made once, and its state is read from on_update.

def on_create
  @req = FmrbNet.request("https://example.org/thing.json")
end

def on_update
  if @req && @req.done?
    @req.ok? ? use(@req.body) : Log.warn(@req.error)
    @req = nil
  end
  50
end
FmrbNet.request(url) Starts one fetch and returns a request object
done? Whether the answer has arrived. Ask it from on_update, do not loop on it
ok? True when it arrived and the status says so
status / body / error The HTTP status, the body, and why it failed

It is deliberately not a callback: a block stored now and called later is the shape that breaks on the Spinel engine when it captures a local variable.

What differs between the machines is where the waiting happens, not what the app writes. On a board the fetch happens inside request, so that app's own task waits there and done? is true immediately — the rest of the machine keeps running, because those tasks are preemptive. In the browser nothing waits: the page fetches while the machine carries on drawing, which is why an app that blocks cannot be written there at all.

In the browser the page's rules apply

The fetch is the browser's, so a server that does not allow cross-origin requests cannot be read from Studio, and Net::HTTP and WebSocket — sockets — are not there at all. The App Store is written this way, which is why it works on both boards and in a browser tab.

WebSocket

require 'net/websocket'

Net::WebSocket::Client.connect("wss://echo.example.com/ws") do |ws|
  ws.send_text("hello")
  msg = ws.receive(timeout: 5)
  ws.close
end

receive polls rather than blocking outright, so it will not freeze the VM for the whole timeout.

Differences from CRuby

The API is shaped like CRuby's, but this is a microcontroller. The limits that actually bite:

TLS version TLS 1.2 only. There is no TLS 1.3
Blocking The calls are synchronous. While a request is in flight, that app processes no events — its window will not repaint and its input is queued. Keep requests short, and set timeouts
Response size The whole body lands in your app's memory pool (512 KB or 1 MB depending on the slot). A large download will exhaust it
HTTP features Chunked transfer, redirects and keep-alive are handled to the extent picoruby's net/http implements them, not to the extent CRuby does
Certificates Custom CA or client certificates are passed as PEM strings, not file paths

Because a request stalls the app that makes it, the usual shape is: fetch once at startup or on a timer, keep the result, and draw from the copy.

A worked example

The bundled Weather app (/app/demo/weather.app.rb) fetches a forecast over HTTPS from Open-Meteo, parses the JSON and draws it. It waits for the network, retries until the first success, and is a good starting point to copy.

The Net Test app (/app/test/net_test.app.rb) exercises the API piece by piece and is useful when something does not work. It is hidden from the launcher with the rest of /app/test, so start it from the editor or the shell.