On this page

Running on Linux

Run aWOT on Linux to develop an HTTP API or browser interface without a board connected. The example in .extras/LinuxNative shares the POSIX socket adapter and Arduino compatibility layer with the macOS example. LinuxClient and LinuxServer are aliases for those existing client and server classes; the aWOT routes and request handlers use the same library sources.

This builds a regular Linux command-line program with GCC or Clang. You do not need Arduino IDE, a board package, or a network shield. For a Windows computer, follow the native Windows guide.

Get the tools and source

Install a C++11 compiler, CMake 3.18 or newer, Ninja, Git, and Python 3. Python is needed for the HTTP test. On Debian or Ubuntu, install the tools and curl with:

sudo apt update
sudo apt install build-essential cmake ninja-build python3 git curl

Clone the aWOT repository and stay in the repository root:

git clone https://github.com/lasselukkari/aWOT.git
cd aWOT

If you already have a checkout containing .extras/LinuxNative, use its root directory instead. Use a Git checkout: .extras is omitted from library release archives and Arduino Library Manager packages.

Keep .extras/MacOSNative, .extras/WindowsNative, and .extras/ArduinoCore-API in the checkout too. The Linux build reuses the POSIX adapter from the macOS directory and the shared ArduinoCore-API sources, and its HTTP test uses the shared Python script in the Windows directory. No separate Arduino download or installation is required.

Hello world example

HelloWorld.cpp registers a route and moves each connection into its own thread:

/*
  HelloWorld.cpp - aWOT example using native Linux POSIX sockets

  Run ./HelloWorld, then visit http://localhost:8080/.
*/

#include <aWOT.h>

#include <cstdlib>
#include <thread>
#include <utility>

#include "LinuxSocket.h"

App app;

void indexCmd(Request&, Response& res) {
  res.set("Content-Type", "text/html");
  res.print("<html>\n");
  res.print("<head>\n");
  res.print("<title>Hello World!</title>\n");
  res.print("</head>\n");
  res.print("<body>\n");
  res.print("<h1>Greetings middle earth!</h1>\n");
  res.print("</body>\n");
  res.print("</html>");
}

void handleClient(LinuxClient client) {
  app.process(&client);
  client.stop();
}

int main() {
  Serial.begin(115200);
  app.get("/", &indexCmd);
  LinuxServer server(8080);
  if (!server.begin()) return EXIT_FAILURE;
  Serial.println("Server listening on port 8080 (multithreaded)");
  Serial.flush();

  for (;;) {
    // Wait in poll until a connection arrives, with no idle polling loop.
    LinuxClient client = server.available(-1);
    if (client.connected()) {
      std::thread(handleClient, std::move(client)).detach();
    }
  }
}

Build and run

From the repository root, run:

cmake -S .extras/LinuxNative -B .extras/LinuxNative/build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build .extras/LinuxNative/build --parallel 4
./.extras/LinuxNative/build/HelloWorld

CMake builds the example and its tests as C++11 programs. The server prints:

Server listening on port 8080 (multithreaded)

Leave this terminal running and open http://127.0.0.1:8080/ in your browser. The page displays Greetings middle earth!. The server listens on all IPv4 interfaces; 127.0.0.1 connects from the same Linux computer.

Inspect the responses from another terminal:

curl -i http://127.0.0.1:8080/
curl -i http://127.0.0.1:8080/missing

The first request returns 200 OK, a Content-Type of text/html, and the HTML page. Only GET / is registered, so /missing returns 404 Not Found. Press Control-C in the server terminal to stop it.

Use your own routes

Edit .extras/LinuxNative/HelloWorld.cpp. Register additional handlers in main(), before server.begin(), using app.get(), app.post(), or the other routing methods. Stop the running server, rebuild with cmake --build .extras/LinuxNative/build --parallel 4, and start it again after editing.

Use middleware and request data with the same aWOT API as on a board. The compatibility layer supplies Arduino types, timing, formatting, and terminal output through Serial. Keep operations using board peripherals separate when sharing handlers with a board.

How the native server works

The Linux native source contains the application and CMake build. LinuxSocket.h aliases the shared MacOSClient and MacOSServer classes as LinuxClient and LinuxServer; Arduino.h forwards the shared Arduino compatibility header. CMake compiles the existing POSIX implementation and bundled ArduinoCore-API with aWOT.

server.available(-1) waits in poll until a connection arrives. Each accepted client moves into a worker thread, which calls app.process(&client) and then client.stop(). The example handles one request per connection and closes it afterward. See Connections before changing that lifecycle.

Register routes before accepting clients. Synchronize shared mutable state with a mutex or atomics, give each concurrent request its own header-capture buffers and custom request and response storage, and synchronize access to shared context.

Client copies share the socket descriptor, connection status, and unread receive buffer. Destroying or reassigning a handle releases its reference; the last reference closes the socket. Calling stop() on any copy closes the connection and clears buffered input for every copy. Reconnecting a handle closes its old shared connection and attaches that handle to a new one. Synchronize concurrent access to copies of the same connection.

Socket reads return promptly when no data is available, allowing Arduino and aWOT read timeouts to expire. Each shared connection has a 1 KiB receive buffer, including for raw request.client() reads. Linux sends suppress SIGPIPE with MSG_NOSIGNAL, so writing to a disconnected peer reports failure through the client API.

Outgoing connections accept numeric IPv4 addresses; DNS and IPv6 connections are not implemented.

Use WsChannel<LinuxClient, N> or SseChannel<LinuxClient, N> to retain native connections. Adapt the Hello World worker to check ProcessResult.responseOpen: after a successful WebSocket upgrade, let the local client leave scope without calling stop(). For SSE, call events.add(client) after process() returns an open response, then let the local client leave scope. The channel’s copy keeps the connection alive. Close connections that were not handed to a channel.

Run the Linux checks

From the repository root, run:

ctest --test-dir .extras/LinuxNative/build --output-on-failure

The suite runs three tests:

  • linux-shims: checks timers, concurrent timing calls, integer conversion, and floating-point formatting using the shared POSIX compatibility layer.
  • linux-sockets: checks socket ownership, buffered and nonblocking reads, disconnects, reconnection, timeouts, writes after peer close, listener conflicts, and aWOT request processing with POSIX and loopback TCP sockets.
  • linux-http: starts the built executable on port 8080 and checks status codes, content type, the Hello World body, missing routes, and concurrent requests. It also checks startup failure on an occupied port and terminates the test server.

Stop the example server before running the tests; port 8080 must be free. Each test has a bounded runtime. A successful run reports 100% tests passed, 0 tests failed out of 3. Configure with -DBUILD_TESTING=OFF to build only the application without Python or the tests.

The native Unit Tests workflow includes a Linux native example job that builds and runs these checks on Ubuntu. Library revision 26cc31a4 passed all three checks on 25 October 2026, including the shared client ownership and channel regressions, in the hosted native CI run. On 11 October 2026, the example built with GCC 14.4 and passed all three checks in a Debian Linux Docker container using Python 3.13. The exact program above also compiled, linked, and passed its HTTP smoke test in that Linux environment. Compilation on macOS and building this documentation do not establish Linux runtime support.

Troubleshooting

  • The example directory is missing: use a Git checkout containing .extras/LinuxNative; release archives omit .extras.
  • CMake rejects the platform: this project requires Linux. Use the macOS guide or Windows guide on those systems.
  • Python is missing during configuration: install Python 3 or configure with -DBUILD_TESTING=OFF for an application-only build.
  • The server exits at startup: check whether another process uses the fixed port 8080. Stop that process before starting the example.
  • You still see the old response: stop the old process before rebuilding and restarting. Recompiling does not replace an already running server.

Continue with routing to organize a larger application, or Hello world on a board to use Wi-Fi or Ethernet.